diff --git a/apps/sim/app/_shell/application-loading.tsx b/apps/sim/app/_shell/application-loading.tsx
new file mode 100644
index 00000000000..41d6ff7a4b6
--- /dev/null
+++ b/apps/sim/app/_shell/application-loading.tsx
@@ -0,0 +1,36 @@
+import { cn, SimWordmark } from '@sim/emcn'
+import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
+import { type BrandConfig, getBrandConfig } from '@/ee/whitelabeling'
+
+interface ApplicationLoadingProps {
+ brand?: BrandConfig
+ fullScreen?: boolean
+}
+
+/** Shared loading surface for app entry, shell prefetch, and workflow navigation. */
+export function ApplicationLoading({
+ brand = getBrandConfig(),
+ fullScreen = true,
+}: ApplicationLoadingProps) {
+ const wordmarkUrl = brand.wordmarkUrl || brand.logoUrl
+
+ return (
+
+ {fullScreen &&
}
+ {wordmarkUrl ? (
+

+ ) : brand.isWhitelabeled ? (
+
{brand.name}
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/apps/sim/app/account/loading.tsx b/apps/sim/app/account/loading.tsx
new file mode 100644
index 00000000000..4d1e787708c
--- /dev/null
+++ b/apps/sim/app/account/loading.tsx
@@ -0,0 +1,5 @@
+import { ApplicationLoading } from '@/app/_shell/application-loading'
+
+export default function Loading() {
+ return
+}
diff --git a/apps/sim/app/home/loading.tsx b/apps/sim/app/home/loading.tsx
new file mode 100644
index 00000000000..4d1e787708c
--- /dev/null
+++ b/apps/sim/app/home/loading.tsx
@@ -0,0 +1,5 @@
+import { ApplicationLoading } from '@/app/_shell/application-loading'
+
+export default function Loading() {
+ return
+}
diff --git a/apps/sim/app/o/loading.tsx b/apps/sim/app/o/loading.tsx
new file mode 100644
index 00000000000..4d1e787708c
--- /dev/null
+++ b/apps/sim/app/o/loading.tsx
@@ -0,0 +1,5 @@
+import { ApplicationLoading } from '@/app/_shell/application-loading'
+
+export default function Loading() {
+ return
+}
diff --git a/apps/sim/app/selfhost/loading.tsx b/apps/sim/app/selfhost/loading.tsx
new file mode 100644
index 00000000000..4d1e787708c
--- /dev/null
+++ b/apps/sim/app/selfhost/loading.tsx
@@ -0,0 +1,5 @@
+import { ApplicationLoading } from '@/app/_shell/application-loading'
+
+export default function Loading() {
+ return
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/layout.test.tsx b/apps/sim/app/workspace/[workspaceId]/layout.test.tsx
index b3f50d1d568..0d1a53f8b5a 100644
--- a/apps/sim/app/workspace/[workspaceId]/layout.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/layout.test.tsx
@@ -4,7 +4,7 @@ import { createSessionPrincipal } from '@sim/testing/factories/principal.factory
import { emcnMock } from '@sim/testing/mocks/emcn.mock'
import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock'
import { reactQueryMock } from '@sim/testing/mocks/react-query.mock'
-import { renderToStaticMarkup } from 'react-dom/server'
+import { renderToReadableStream } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
@@ -178,7 +178,7 @@ describe('WorkspaceLayout host context', () => {
children: Workspace child
,
params: Promise.resolve({ workspaceId: 'workspace-b' }),
})
- renderToStaticMarkup(element)
+ await new Response(await renderToReadableStream(element)).text()
expect(mockGetOrgWhitelabelSettings).toHaveBeenCalledWith('org-b')
expect(mockGetOrgWhitelabelSettings).not.toHaveBeenCalledWith('org-a')
@@ -211,7 +211,7 @@ describe('WorkspaceLayout host context', () => {
children: Secret workspace child
,
params: Promise.resolve({ workspaceId: 'workspace-denied' }),
})
- const html = renderToStaticMarkup(element)
+ const html = await new Response(await renderToReadableStream(element)).text()
expect(html).toContain('Workspace access denied')
expect(html).not.toContain('Secret workspace child')
@@ -226,7 +226,7 @@ describe('WorkspaceLayout host context', () => {
children: Workspace child
,
params: Promise.resolve({ workspaceId: 'workspace-b' }),
})
- renderToStaticMarkup(element)
+ await new Response(await renderToReadableStream(element)).text()
return mockWorkspaceChrome.mock.calls[0][0].sidebar
}
diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx
index a7cf24ce571..e8c733e1647 100644
--- a/apps/sim/app/workspace/[workspaceId]/layout.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx
@@ -1,7 +1,9 @@
-import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
+import { Suspense } from 'react'
+import { dehydrate, HydrationBoundary, type QueryClient } from '@tanstack/react-query'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider'
+import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
import { getSession } from '@/lib/auth'
import { getActiveOrganizationId } from '@/lib/auth/session-response'
import { isChangelogEnabled } from '@/lib/changelog/feature-flag'
@@ -13,6 +15,7 @@ import {
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
import { resolveOrganizationEntryPath } from '@/lib/navigation/resolve-app-entry'
import { isWorkflowTestsEnabled } from '@/lib/workflow-tests/feature-flag'
+import { ApplicationLoading } from '@/app/_shell/application-loading'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner'
import { SessionExpired } from '@/app/workspace/[workspaceId]/components/session-expired'
@@ -38,16 +41,33 @@ import { WorkspaceHostProvider } from '@/app/workspace/[workspaceId]/providers/w
import { WorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { WorkspaceScopeSync } from '@/app/workspace/[workspaceId]/providers/workspace-scope-sync'
import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
+import {
+ getBrandConfig,
+ mergeOrgBrandConfig,
+ type OrganizationWhitelabelSettings,
+} from '@/ee/whitelabeling'
import { BrandingProvider } from '@/ee/whitelabeling/components/branding-provider'
import { getOrgWhitelabelSettings } from '@/ee/whitelabeling/org-branding'
-export default async function WorkspaceLayout({
- children,
- params,
-}: {
+interface WorkspaceLayoutProps {
children: React.ReactNode
params: Promise<{ workspaceId: string }>
-}) {
+}
+
+interface WorkspaceContentProps {
+ children: React.ReactNode
+ workspaceId: string
+ session: NonNullable>>
+ queryClient: QueryClient
+ hostContext: WorkspaceHostContext
+ orgSettings: Promise
+}
+
+interface WorkspaceLoadingProps {
+ orgSettings: Promise
+}
+
+export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) {
const session = await getSession()
if (!session?.user) {
redirect('/login')
@@ -60,6 +80,38 @@ export default async function WorkspaceLayout({
return
}
+ const orgSettings = hostContext.hostOrganizationId
+ ? getOrgWhitelabelSettings(hostContext.hostOrganizationId)
+ : Promise.resolve(null)
+
+ return (
+ }>
+
+ {children}
+
+
+ )
+}
+
+async function WorkspaceLoading({ orgSettings }: WorkspaceLoadingProps) {
+ const brand = mergeOrgBrandConfig(await orgSettings, getBrandConfig())
+ return
+}
+
+async function WorkspaceContent({
+ children,
+ workspaceId,
+ session,
+ queryClient,
+ hostContext,
+ orgSettings,
+}: WorkspaceContentProps) {
const activeOrganizationId = getActiveOrganizationId(session)
const principal = {
kind: 'session',
@@ -79,9 +131,7 @@ export default async function WorkspaceLayout({
desktopExecutorRegistered,
] = await Promise.all([
cookies(),
- hostContext.hostOrganizationId
- ? getOrgWhitelabelSettings(hostContext.hostOrganizationId)
- : Promise.resolve(null),
+ orgSettings,
prefetchWorkspaceSidebar(
queryClient,
workspaceId,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/page.tsx b/apps/sim/app/workspace/[workspaceId]/w/page.tsx
index 5aafcc4815b..3ab2796c8e3 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/page.tsx
@@ -5,29 +5,17 @@ import { Chip } from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { ReactFlowProvider } from '@xyflow/react'
import { useParams, useRouter } from 'next/navigation'
+import { ApplicationLoading } from '@/app/_shell/application-loading'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { Panel, Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
import { useWorkflowOperations } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
+import { useOrgBrandConfig } from '@/ee/whitelabeling/components/branding-provider'
import { useWorkflows } from '@/hooks/queries/workflows'
const logger = createLogger('WorkflowsPage')
-function Spinner() {
- return (
-
- )
-}
-
export default function WorkflowsPage() {
+ const brand = useOrgBrandConfig()
const router = useRouter()
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -104,7 +92,7 @@ export default function WorkflowsPage() {
)}
) : (
-
+
)}
diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx
index 4c74c22deb9..bcaa5552339 100644
--- a/apps/sim/app/workspace/page.tsx
+++ b/apps/sim/app/workspace/page.tsx
@@ -16,6 +16,7 @@ import {
isUpgradeReason,
UPGRADE_REASON_PARAM,
} from '@/lib/billing/upgrade-reasons'
+import { ApplicationLoading } from '@/app/_shell/application-loading'
import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
import { useWorkspacesWithMetadata } from '@/hooks/queries/workspace'
@@ -213,21 +214,7 @@ export default function WorkspacePage() {
)
}
- return (
-
- )
+ return
}
async function handleWorkflowRedirect(
diff --git a/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx b/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx
index 88f0e848de7..da58774967d 100644
--- a/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx
+++ b/packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx
@@ -7,18 +7,22 @@ const WORDMARK_FILLS = {
'muted-inverse': 'var(--text-muted-inverse)',
} as const
+const WORDMARK_SIZES = {
+ nav: { width: 37, height: 18, className: '-translate-y-[1.5px] h-[18px] w-auto' },
+ tag: { width: 21, height: 10, className: 'h-[10px] w-auto' },
+ loading: { width: 67, height: 32, className: 'h-8 w-auto' },
+} as const
+
export interface SimWordmarkProps {
- /** Navbar mark or compact mark sized for a 20px ChipTag. */
- size?: 'nav' | 'tag'
+ /** Navbar, compact 20px ChipTag, or centered application loading mark. */
+ size?: keyof typeof WORDMARK_SIZES
/** Body ink, inherited foreground, muted brand ink, or light ink for inverse surfaces. */
tone?: keyof typeof WORDMARK_FILLS
}
/** Canonical Sim logotype, shared by browser and bundled desktop pages. */
export function SimWordmark({ size = 'nav', tone = 'body' }: SimWordmarkProps) {
- const compact = size === 'tag'
- const width = compact ? 21 : 37
- const height = compact ? 10 : 18
+ const { width, height, className } = WORDMARK_SIZES[size]
const fill = WORDMARK_FILLS[tone]
return (
@@ -28,7 +32,7 @@ export function SimWordmark({ size = 'nav', tone = 'body' }: SimWordmarkProps) {
height={height}
fill='none'
aria-hidden='true'
- className={compact ? 'h-[10px] w-auto' : '-translate-y-[1.5px] h-[18px] w-auto'}
+ className={className}
>
{WORDMARK_PATHS.map((d) => (
diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json
index c16098ee35e..75a5ed504e1 100644
--- a/scripts/check-unused-exports.baseline.json
+++ b/scripts/check-unused-exports.baseline.json
@@ -956,11 +956,8 @@
"apps/sim/ee/whitelabeling/branding.ts#ThemeColors",
"apps/sim/ee/whitelabeling/hooks/whitelabel.ts#WHITELABEL_STALE_TIME",
"apps/sim/ee/whitelabeling/hooks/whitelabel.ts#whitelabelKeys",
- "apps/sim/ee/whitelabeling/index.ts#BrandConfig",
- "apps/sim/ee/whitelabeling/index.ts#OrganizationWhitelabelSettings",
"apps/sim/ee/whitelabeling/index.ts#ThemeColors",
"apps/sim/ee/whitelabeling/index.ts#generateOrgThemeCSS",
- "apps/sim/ee/whitelabeling/index.ts#mergeOrgBrandConfig",
"apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx#ForkResourcePickerItem",
"apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts#MappableMappingKind",
"apps/sim/ee/workspace-forking/hooks/background-work.ts#BACKGROUND_WORK_STALE_TIME",