@elytracms/studio 0.0.61 → 0.0.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/components/account/account-settings.js +6 -1
  2. package/dist/components/account/account-settings.js.map +1 -1
  3. package/dist/components/account/credential-panel.js +6 -1
  4. package/dist/components/account/credential-panel.js.map +1 -1
  5. package/dist/components/account/self-account-schema.js +4 -1
  6. package/dist/components/account/self-account-schema.js.map +1 -1
  7. package/dist/components/auth-context.d.ts +14 -0
  8. package/dist/components/auth-context.js +32 -2
  9. package/dist/components/auth-context.js.map +1 -1
  10. package/dist/components/builder/blocks-model.d.ts +7 -0
  11. package/dist/components/builder/blocks-model.js +5 -0
  12. package/dist/components/builder/blocks-model.js.map +1 -1
  13. package/dist/components/builder/blocks-panel.js +2 -2
  14. package/dist/components/builder/blocks-panel.js.map +1 -1
  15. package/dist/components/builder/fixture-thumbnail.d.ts +15 -0
  16. package/dist/components/builder/fixture-thumbnail.js +14 -0
  17. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  18. package/dist/components/builder/property-inspector.d.ts +21 -0
  19. package/dist/components/builder/property-inspector.js +24 -8
  20. package/dist/components/builder/property-inspector.js.map +1 -1
  21. package/dist/components/cms/cms-index.js +6 -3
  22. package/dist/components/cms/cms-index.js.map +1 -1
  23. package/dist/components/cms/detail-editor.d.ts +12 -1
  24. package/dist/components/cms/detail-editor.js +23 -23
  25. package/dist/components/cms/detail-editor.js.map +1 -1
  26. package/dist/components/cms/prop-fields.d.ts +1 -1
  27. package/dist/components/cms/prop-fields.js +14 -0
  28. package/dist/components/cms/prop-fields.js.map +1 -1
  29. package/dist/components/deployment-setup-gate.js +40 -8
  30. package/dist/components/deployment-setup-gate.js.map +1 -1
  31. package/dist/components/operation-error.d.ts +7 -5
  32. package/dist/components/operation-error.js +19 -36
  33. package/dist/components/operation-error.js.map +1 -1
  34. package/dist/components/project-list.js +17 -1
  35. package/dist/components/project-list.js.map +1 -1
  36. package/dist/components/project-switcher.d.ts +10 -0
  37. package/dist/components/project-switcher.js +13 -3
  38. package/dist/components/project-switcher.js.map +1 -1
  39. package/dist/components/session-expired-overlay.d.ts +27 -0
  40. package/dist/components/session-expired-overlay.js +51 -0
  41. package/dist/components/session-expired-overlay.js.map +1 -0
  42. package/dist/components/sign-in-view.d.ts +13 -1
  43. package/dist/components/sign-in-view.js +27 -13
  44. package/dist/components/sign-in-view.js.map +1 -1
  45. package/dist/components/studio-project-screen.js +2 -2
  46. package/dist/components/studio-project-screen.js.map +1 -1
  47. package/dist/components/studio-shell.js +40 -3
  48. package/dist/components/studio-shell.js.map +1 -1
  49. package/dist/components/users/users-schema.d.ts +11 -1
  50. package/dist/components/users/users-schema.js +17 -1
  51. package/dist/components/users/users-schema.js.map +1 -1
  52. package/dist/customization.d.ts +4 -1
  53. package/dist/customization.js +3 -0
  54. package/dist/customization.js.map +1 -1
  55. package/dist/lib/auth-errors.d.ts +58 -28
  56. package/dist/lib/auth-errors.js +93 -15
  57. package/dist/lib/auth-errors.js.map +1 -1
  58. package/dist/lib/i18n/index.d.ts +1 -0
  59. package/dist/lib/i18n/index.js.map +1 -1
  60. package/dist/lib/i18n/messages.de.d.ts +24 -0
  61. package/dist/lib/i18n/messages.de.js +1019 -0
  62. package/dist/lib/i18n/messages.de.js.map +1 -0
  63. package/dist/lib/i18n/messages.en.d.ts +880 -0
  64. package/dist/lib/i18n/messages.en.js +71 -3
  65. package/dist/lib/i18n/messages.en.js.map +1 -1
  66. package/dist/lib/operations/studio-operations.js +7 -0
  67. package/dist/lib/operations/studio-operations.js.map +1 -1
  68. package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
  69. package/dist/lib/persistence/deployment-readiness.js +16 -1
  70. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  71. package/dist/lib/session-expiry.d.ts +67 -0
  72. package/dist/lib/session-expiry.js +72 -0
  73. package/dist/lib/session-expiry.js.map +1 -0
  74. package/dist/lib/tab-title.d.ts +53 -0
  75. package/dist/lib/tab-title.js +77 -0
  76. package/dist/lib/tab-title.js.map +1 -0
  77. package/dist/lib/workspace/index.d.ts +1 -1
  78. package/dist/lib/workspace/index.js +1 -1
  79. package/dist/lib/workspace/index.js.map +1 -1
  80. package/dist/lib/workspace/switcher-source.d.ts +27 -0
  81. package/dist/lib/workspace/switcher-source.js +30 -0
  82. package/dist/lib/workspace/switcher-source.js.map +1 -1
  83. package/dist/router/index.d.ts +6 -0
  84. package/dist/router/index.js +6 -0
  85. package/dist/router/index.js.map +1 -1
  86. package/dist/router/launcher-visit.d.ts +13 -0
  87. package/dist/router/launcher-visit.js +34 -0
  88. package/dist/router/launcher-visit.js.map +1 -0
  89. package/dist/router/root.js +12 -2
  90. package/dist/router/root.js.map +1 -1
  91. package/dist/ui/dialog.d.ts +15 -0
  92. package/dist/ui/dialog.js +17 -0
  93. package/dist/ui/dialog.js.map +1 -1
  94. package/package.json +6 -2
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
2
+ import { launcherVisitState, STUDIO_PROJECT_ROUTE, studioPath, useNavigate, } from '@elytracms/studio/router';
3
3
  import { Check, ChevronsUpDown, FolderOpen } from 'lucide-react';
4
4
  import { hasProjectSession } from '@elytracms/studio/lib/auth';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
- import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
6
+ import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Heading, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
7
7
  import { useStudio } from './studio-context.js';
8
8
  import { useRecentProjects, useSwitcherEntries } from './workspace-projects.js';
9
9
  import { SignInNeededBadge } from './sign-in-needed-badge.js';
@@ -24,7 +24,17 @@ export function ProjectSwitcher({ activeProject }) {
24
24
  studio.activate(project.id);
25
25
  navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } });
26
26
  }
27
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: activeProject.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate font-medium", children: activeProject.name }), _jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsxs(DropdownMenuContent, { className: "w-80", align: "start", children: [recents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuLabel, { children: translate('shell.projectSwitcher.recent') }), recents.map((p) => (_jsx(ProjectMenuItem, { project: p, active: false, onSelect: () => open(p) }, p.id))), _jsx(DropdownMenuSeparator, {})] })) : null, _jsx(DropdownMenuLabel, { children: translate('shell.projects') }), activeEntries.map((entry) => entry.kind === 'project' ? (_jsx(ProjectMenuItem, { project: entry.project, active: entry.project.id === activeProject.id, onSelect: () => open(entry.project) }, entry.project.id)) : (_jsx(UnprovisionedMenuItem, { config: entry.config }, `config:${entry.config.slug}`))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => navigate({ to: '/' }), children: [_jsx(FolderOpen, {}), " ", translate('shell.projectSwitcher.browseAll')] })] })] }));
27
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: activeProject.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate font-medium", children: activeProject.name }), _jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" })] }) }), _jsxs(DropdownMenuContent, { className: "w-80", align: "start", children: [recents.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuLabel, { children: translate('shell.projectSwitcher.recent') }), recents.map((p) => (_jsx(ProjectMenuItem, { project: p, active: false, onSelect: () => open(p) }, p.id))), _jsx(DropdownMenuSeparator, {})] })) : null, _jsx(DropdownMenuLabel, { children: translate('shell.projects') }), activeEntries.map((entry) => entry.kind === 'project' ? (_jsx(ProjectMenuItem, { project: entry.project, active: entry.project.id === activeProject.id, onSelect: () => open(entry.project) }, entry.project.id)) : (_jsx(UnprovisionedMenuItem, { config: entry.config }, `config:${entry.config.slug}`))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => navigate({ to: '/', state: launcherVisitState }), children: [_jsx(FolderOpen, {}), " ", translate('shell.projectSwitcher.browseAll')] })] })] }));
28
+ }
29
+ /**
30
+ * The switcher slot's single-project face (EC-401): the active project's name
31
+ * as a plain heading — same identity mark as the switcher trigger, but no
32
+ * button, no chevron, no dropdown. `StudioShell` renders this instead of
33
+ * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the
34
+ * switcher returns the moment a second workspace entry exists.
35
+ */
36
+ export function ActiveProjectName({ project }) {
37
+ return (_jsxs(Heading, { level: 1, size: "h4", className: "flex h-8 min-w-0 items-center gap-2 px-2.5", children: [_jsx("span", { className: "grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: project.name.charAt(0).toUpperCase() }), _jsx("span", { className: "max-w-[12rem] truncate", children: project.name })] }));
28
38
  }
29
39
  function ProjectMenuItem({ project, active, onSelect, }) {
30
40
  // Per-backend sign-in state (EC-179): the active project is signed-in by
@@ -1 +1 @@
1
- {"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`builder init`), not here.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n <DropdownMenuItem onSelect={() => navigate({ to: '/' })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n builder init --project {config.slug}\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
1
+ {"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n launcherVisitState,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useNavigate,\n} from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Heading,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`builder init`), not here.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n {/* EC-401: an explicit \"all projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <DropdownMenuItem onSelect={() => navigate({ to: '/', state: launcherVisitState })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The switcher slot's single-project face (EC-401): the active project's name\n * as a plain heading — same identity mark as the switcher trigger, but no\n * button, no chevron, no dropdown. `StudioShell` renders this instead of\n * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the\n * switcher returns the moment a second workspace entry exists.\n */\nexport function ActiveProjectName({ project }: { project: Project }) {\n return (\n <Heading level={1} size=\"h4\" className=\"flex h-8 min-w-0 items-center gap-2 px-2.5\">\n <span className=\"grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate\">{project.name}</span>\n </Heading>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n builder init --project {config.slug}\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The blocking in-place re-auth overlay (EC-403). Mounted once in the studio
3
+ * root (inside `StudioProvider`, over every route) and shown while the auth
4
+ * store reports `sessionExpired`: the backend session died mid-flight, so all
5
+ * editing is blocked behind a non-dismissible modal that mounts the SAME
6
+ * `SignInScreen` composition the `/sign-in` route uses — **without any
7
+ * navigation**. The React tree beneath stays alive, so every unsaved editor
8
+ * state survives; a successful sign-in applies the fresh session (clearing the
9
+ * flag in the auth store) and editing resumes exactly where it stopped.
10
+ * Pending failed writes keep their normal retry paths (a `persistError`
11
+ * clears on the next successful save).
12
+ *
13
+ * Re-auth is strictly sign-in (no create-account toggle — a brand-new account
14
+ * cannot resume an existing editing session). On success the studio query
15
+ * cache is invalidated wholesale so reads that failed or staled while the
16
+ * session was dead recover without a reload.
17
+ *
18
+ * Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,
19
+ * focus-trapped, Escape/outside interaction consumed, studio beneath inert.
20
+ */
21
+ export declare function SessionExpiredOverlay(): import("react").JSX.Element | null;
22
+ /**
23
+ * Why the sign-in screen appeared in place (EC-403): the session-expired copy
24
+ * rendered above the form via the `SignInScreen` notice slot. Pure — exported
25
+ * for the provider-free static-render smoke test.
26
+ */
27
+ export declare function SessionExpiredNotice(): import("react").JSX.Element;
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CircleAlert } from 'lucide-react';
3
+ import { LOCAL_DEV_CREDENTIALS, authMode } from '@elytracms/studio/lib/auth';
4
+ import { useT } from '@elytracms/studio/lib/i18n';
5
+ import { useOperationCache } from '@elytracms/studio/lib/query';
6
+ import { Dialog, DialogBlockingContent, DialogTitle, Row, Stack, Text } from '@elytracms/studio/ui';
7
+ import { useAuth } from './auth-context.js';
8
+ import { SignInScreen, activeProjectName } from './sign-in-view.js';
9
+ /**
10
+ * The blocking in-place re-auth overlay (EC-403). Mounted once in the studio
11
+ * root (inside `StudioProvider`, over every route) and shown while the auth
12
+ * store reports `sessionExpired`: the backend session died mid-flight, so all
13
+ * editing is blocked behind a non-dismissible modal that mounts the SAME
14
+ * `SignInScreen` composition the `/sign-in` route uses — **without any
15
+ * navigation**. The React tree beneath stays alive, so every unsaved editor
16
+ * state survives; a successful sign-in applies the fresh session (clearing the
17
+ * flag in the auth store) and editing resumes exactly where it stopped.
18
+ * Pending failed writes keep their normal retry paths (a `persistError`
19
+ * clears on the next successful save).
20
+ *
21
+ * Re-auth is strictly sign-in (no create-account toggle — a brand-new account
22
+ * cannot resume an existing editing session). On success the studio query
23
+ * cache is invalidated wholesale so reads that failed or staled while the
24
+ * session was dead recover without a reload.
25
+ *
26
+ * Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,
27
+ * focus-trapped, Escape/outside interaction consumed, studio beneath inert.
28
+ */
29
+ export function SessionExpiredOverlay() {
30
+ const auth = useAuth();
31
+ const t = useT();
32
+ const cache = useOperationCache();
33
+ if (!auth.sessionExpired)
34
+ return null;
35
+ return (_jsx(Dialog, { open: true, children: _jsxs(DialogBlockingContent, { "aria-describedby": undefined, children: [_jsx(DialogTitle, { className: "sr-only", children: t('error.sessionExpiredTitle') }), _jsx(SignInScreen, { notice: _jsx(SessionExpiredNotice, {}), projectName: activeProjectName(), showModeToggle: false, devCredentials: authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null, authenticate: (_mode, email, password) => auth.signIn(email, password), onAuthenticated: () =>
36
+ // The session was dead for an unknown span: reads may have failed
37
+ // (retry: false) or gone stale. One wholesale invalidation lets
38
+ // every active query refetch under the fresh session — resume,
39
+ // not reload. (The empty key prefix matches all queries.)
40
+ cache.invalidate([]) })] }) }));
41
+ }
42
+ /**
43
+ * Why the sign-in screen appeared in place (EC-403): the session-expired copy
44
+ * rendered above the form via the `SignInScreen` notice slot. Pure — exported
45
+ * for the provider-free static-render smoke test.
46
+ */
47
+ export function SessionExpiredNotice() {
48
+ const t = useT();
49
+ return (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2", children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(CircleAlert, { className: "mt-0.5 size-4 shrink-0 text-destructive" }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "font-medium text-destructive", children: t('error.sessionExpiredTitle') }), _jsx(Text, { size: "sm", tone: "muted", children: t('error.sessionExpired') })] })] }) }));
50
+ }
51
+ //# sourceMappingURL=session-expired-overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-expired-overlay.js","sourceRoot":"","sources":["../../src/components/session-expired-overlay.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAEhE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IAEjC,IAAI,CAAC,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAA;IAErC,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,kBACV,MAAC,qBAAqB,wBAAmB,SAAS,aAChD,KAAC,WAAW,IAAC,SAAS,EAAC,SAAS,YAAE,CAAC,CAAC,2BAA2B,CAAC,GAAe,EAC/E,KAAC,YAAY,IACX,MAAM,EAAE,KAAC,oBAAoB,KAAG,EAChC,WAAW,EAAE,iBAAiB,EAAE,EAChC,cAAc,EAAE,KAAK,EACrB,cAAc,EAAE,QAAQ,EAAE,KAAK,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,EAC1E,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,EACtE,eAAe,EAAE,GAAG,EAAE;oBACpB,kEAAkE;oBAClE,gEAAgE;oBAChE,+DAA+D;oBAC/D,0DAA0D;oBAC1D,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,GAEtB,IACoB,GACjB,CACV,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;IAChB,OAAO,CACL,cACE,IAAI,EAAC,OAAO,EACZ,SAAS,EAAC,qEAAqE,YAE/E,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,WAAW,IAAC,SAAS,EAAC,yCAAyC,GAAG,EACnE,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACvC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,8BAA8B,YACrD,CAAC,CAAC,2BAA2B,CAAC,GAC1B,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,CAAC,CAAC,sBAAsB,CAAC,GACrB,IACD,IACJ,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { CircleAlert } from 'lucide-react'\nimport { LOCAL_DEV_CREDENTIALS, authMode } from '@elytracms/studio/lib/auth'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { useOperationCache } from '@elytracms/studio/lib/query'\nimport { Dialog, DialogBlockingContent, DialogTitle, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from './auth-context'\nimport { SignInScreen, activeProjectName } from './sign-in-view'\n\n/**\n * The blocking in-place re-auth overlay (EC-403). Mounted once in the studio\n * root (inside `StudioProvider`, over every route) and shown while the auth\n * store reports `sessionExpired`: the backend session died mid-flight, so all\n * editing is blocked behind a non-dismissible modal that mounts the SAME\n * `SignInScreen` composition the `/sign-in` route uses — **without any\n * navigation**. The React tree beneath stays alive, so every unsaved editor\n * state survives; a successful sign-in applies the fresh session (clearing the\n * flag in the auth store) and editing resumes exactly where it stopped.\n * Pending failed writes keep their normal retry paths (a `persistError`\n * clears on the next successful save).\n *\n * Re-auth is strictly sign-in (no create-account toggle — a brand-new account\n * cannot resume an existing editing session). On success the studio query\n * cache is invalidated wholesale so reads that failed or staled while the\n * session was dead recover without a reload.\n *\n * Stacking: `DialogBlockingContent` (EC-392 discipline) — topmost modal layer,\n * focus-trapped, Escape/outside interaction consumed, studio beneath inert.\n */\nexport function SessionExpiredOverlay() {\n const auth = useAuth()\n const t = useT()\n const cache = useOperationCache()\n\n if (!auth.sessionExpired) return null\n\n return (\n <Dialog open>\n <DialogBlockingContent aria-describedby={undefined}>\n <DialogTitle className=\"sr-only\">{t('error.sessionExpiredTitle')}</DialogTitle>\n <SignInScreen\n notice={<SessionExpiredNotice />}\n projectName={activeProjectName()}\n showModeToggle={false}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n authenticate={(_mode, email, password) => auth.signIn(email, password)}\n onAuthenticated={() =>\n // The session was dead for an unknown span: reads may have failed\n // (retry: false) or gone stale. One wholesale invalidation lets\n // every active query refetch under the fresh session — resume,\n // not reload. (The empty key prefix matches all queries.)\n cache.invalidate([])\n }\n />\n </DialogBlockingContent>\n </Dialog>\n )\n}\n\n/**\n * Why the sign-in screen appeared in place (EC-403): the session-expired copy\n * rendered above the form via the `SignInScreen` notice slot. Pure — exported\n * for the provider-free static-render smoke test.\n */\nexport function SessionExpiredNotice() {\n const t = useT()\n return (\n <div\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2\"\n >\n <Row gap=\"2\" align=\"start\">\n <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"font-medium text-destructive\">\n {t('error.sessionExpiredTitle')}\n </Text>\n <Text size=\"sm\" tone=\"muted\">\n {t('error.sessionExpired')}\n </Text>\n </Stack>\n </Row>\n </div>\n )\n}\n"]}
@@ -14,6 +14,12 @@ export interface SignInViewProps {
14
14
  /** Product name in the "Sign in to …" heading. Default: `'Elytra'`. */
15
15
  productName?: string;
16
16
  }
17
+ /**
18
+ * The project whose backend this sign-in authenticates against (EC-179).
19
+ * Exported for the session-expired re-auth overlay (EC-403), which mounts
20
+ * {@link SignInScreen} with the same subtext.
21
+ */
22
+ export declare function activeProjectName(): string | null;
17
23
  /**
18
24
  * The `/sign-in` route component. Container: reads the router (post-sign-in
19
25
  * redirect target from the route search) + auth seams and hands the pure
@@ -26,6 +32,12 @@ interface SignInScreenProps {
26
32
  showModeToggle: boolean;
27
33
  /** Seeded dev accounts to list (playground mode), or null to hide the hint. */
28
34
  devCredentials: readonly DevCredential[] | null;
35
+ /**
36
+ * Contextual notice rendered above the heading — the session-expired
37
+ * re-auth overlay (EC-403) explains WHY the screen appeared here. The
38
+ * normal `/sign-in` route passes none.
39
+ */
40
+ notice?: ReactNode;
29
41
  brandMark?: ReactNode;
30
42
  productName?: string;
31
43
  /** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */
@@ -37,5 +49,5 @@ interface SignInScreenProps {
37
49
  * Provider-free presentational sign-in screen. Owns only local form state and
38
50
  * calls injected callbacks — no context hooks — so it static-renders in tests.
39
51
  */
40
- export declare function SignInScreen({ projectName, showModeToggle, devCredentials, brandMark, productName, authenticate, onAuthenticated, }: SignInScreenProps): import("react").JSX.Element;
52
+ export declare function SignInScreen({ projectName, showModeToggle, devCredentials, notice, brandMark, productName, authenticate, onAuthenticated, }: SignInScreenProps): import("react").JSX.Element;
41
53
  export {};
@@ -4,9 +4,15 @@ import { useNavigate, useSearch } from '@tanstack/react-router';
4
4
  import { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui';
5
5
  import { useAuth } from '@elytracms/studio/components/auth-context';
6
6
  import { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth';
7
+ import { signInErrorMessageKey } from '@elytracms/studio/lib/auth-errors';
8
+ import { useT } from '@elytracms/studio/lib/i18n';
7
9
  import { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace';
8
- /** The project whose backend this sign-in authenticates against (EC-179). */
9
- function activeProjectName() {
10
+ /**
11
+ * The project whose backend this sign-in authenticates against (EC-179).
12
+ * Exported for the session-expired re-auth overlay (EC-403), which mounts
13
+ * {@link SignInScreen} with the same subtext.
14
+ */
15
+ export function activeProjectName() {
10
16
  const state = getWorkspaceState();
11
17
  if (state.kind !== 'config')
12
18
  return null;
@@ -32,7 +38,8 @@ export function SignInView({ brandMark, productName }) {
32
38
  * Provider-free presentational sign-in screen. Owns only local form state and
33
39
  * calls injected callbacks — no context hooks — so it static-renders in tests.
34
40
  */
35
- export function SignInScreen({ projectName, showModeToggle, devCredentials, brandMark, productName = 'Elytra', authenticate, onAuthenticated, }) {
41
+ export function SignInScreen({ projectName, showModeToggle, devCredentials, notice, brandMark, productName = 'Elytra', authenticate, onAuthenticated, }) {
42
+ const t = useT();
36
43
  const [email, setEmail] = useState('');
37
44
  const [password, setPassword] = useState('');
38
45
  const [phase, setPhase] = useState('idle');
@@ -50,26 +57,33 @@ export function SignInScreen({ projectName, showModeToggle, devCredentials, bran
50
57
  // A fresh backend has no account yet: nudge sign-in → create, and vice versa.
51
58
  if (result.reason === 'account-exists')
52
59
  setMode('signin');
53
- setError(result.message);
60
+ // EC-397: engine-classified failures (EC-362/EC-363 reasons) localize
61
+ // through the message catalog; an unrecognized shape renders the adapter's
62
+ // raw English message — honest, never a stale mapping.
63
+ const errorKey = signInErrorMessageKey(result);
64
+ setError(errorKey ? t(errorKey) : result.message);
54
65
  setPhase('idle');
55
66
  }
56
67
  const submitting = phase === 'submitting';
57
68
  const creating = mode === 'signup';
58
- return (_jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: _jsxs(Stack, { gap: "4", className: "w-full max-w-sm", children: [_jsxs(Stack, { gap: "1", align: "center", children: [brandMark ?? _jsx("span", { className: "size-8 rounded-lg bg-primary shadow-sm", "aria-hidden": true }), _jsx(Heading, { size: "h2", children: creating ? 'Create your account' : `Sign in to ${productName}` }), _jsx(Text, { size: "sm", tone: "muted", children: projectName
59
- ? `${creating ? 'Register on' : 'Continue to'} ${projectName}.`
60
- : 'Use your studio account to continue.' })] }), _jsx(Card, { children: _jsx(CardContent, { className: "pt-5", children: _jsx("form", { onSubmit: handleSubmit, noValidate: true, children: _jsxs(Stack, { gap: "4", children: [_jsx(Field, { label: "Email", htmlFor: "sign-in-email", required: true, children: _jsx(Input, { id: "sign-in-email", type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: "you@example.com", disabled: submitting }) }), _jsx(Field, { label: "Password", htmlFor: "sign-in-password", required: true, error: error, children: _jsx(Input, { id: "sign-in-password", type: "password", autoComplete: creating ? 'new-password' : 'current-password', value: password, onChange: (e) => setPassword(e.target.value), disabled: submitting }) }), _jsx(Button, { type: "submit", disabled: submitting, className: "w-full", children: submitting
69
+ return (_jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: _jsxs(Stack, { gap: "4", className: "w-full max-w-sm", children: [notice, _jsxs(Stack, { gap: "1", align: "center", children: [brandMark ?? _jsx("span", { className: "size-8 rounded-lg bg-primary shadow-sm", "aria-hidden": true }), _jsx(Heading, { size: "h2", children: creating ? t('auth.signIn.headingCreate') : t('auth.signIn.heading', { productName }) }), _jsx(Text, { size: "sm", tone: "muted", children: projectName
70
+ ? t(creating ? 'auth.signIn.subtext.register' : 'auth.signIn.subtext.continue', {
71
+ project: projectName,
72
+ })
73
+ : t('auth.signIn.subtext.generic') })] }), _jsx(Card, { children: _jsx(CardContent, { className: "pt-5", children: _jsx("form", { onSubmit: handleSubmit, noValidate: true, children: _jsxs(Stack, { gap: "4", children: [_jsx(Field, { label: t('auth.signIn.emailLabel'), htmlFor: "sign-in-email", required: true, children: _jsx(Input, { id: "sign-in-email", type: "email", autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: t('auth.signIn.emailPlaceholder'), disabled: submitting }) }), _jsx(Field, { label: t('auth.signIn.passwordLabel'), htmlFor: "sign-in-password", required: true, error: error, children: _jsx(Input, { id: "sign-in-password", type: "password", autoComplete: creating ? 'new-password' : 'current-password', value: password, onChange: (e) => setPassword(e.target.value), disabled: submitting }) }), _jsx(Button, { type: "submit", disabled: submitting, className: "w-full", children: submitting
61
74
  ? creating
62
- ? 'Creating account…'
63
- : 'Signing in…'
75
+ ? t('auth.signIn.submittingCreate')
76
+ : t('auth.signIn.submitting')
64
77
  : creating
65
- ? 'Create account'
66
- : 'Sign in' })] }) }) }) }), showModeToggle ? (_jsxs(Text, { size: "sm", tone: "muted", className: "text-center", children: [creating ? 'Already have an account on this backend? ' : 'First time on this backend? ', _jsx("button", { type: "button", className: "font-medium text-primary underline-offset-2 hover:underline", onClick: () => {
78
+ ? t('auth.signIn.submitCreate')
79
+ : t('auth.signIn.submit') })] }) }) }) }), showModeToggle ? (_jsxs(Text, { size: "sm", tone: "muted", className: "text-center", children: [creating ? t('auth.signIn.toggle.alreadyHave') : t('auth.signIn.toggle.firstTime'), ' ', _jsx("button", { type: "button", className: "font-medium text-primary underline-offset-2 hover:underline", onClick: () => {
67
80
  setMode(creating ? 'signin' : 'signup');
68
81
  setError(null);
69
- }, children: creating ? 'Sign in' : 'Create an account' })] })) : null, devCredentials ? _jsx(DevCredentialsHint, { entries: devCredentials }) : null] }) }));
82
+ }, children: creating ? t('auth.signIn.toggle.signIn') : t('auth.signIn.toggle.createAccount') })] })) : null, devCredentials ? _jsx(DevCredentialsHint, { entries: devCredentials }) : null] }) }));
70
83
  }
71
84
  /** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */
72
85
  function DevCredentialsHint({ entries }) {
73
- return (_jsx(Card, { className: "border-dashed", children: _jsx(CardContent, { className: "pt-4", children: _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", tone: "muted", className: "font-medium", children: "Playground mode \u2014 seeded dev accounts" }), entries.map((entry) => (_jsxs(Text, { size: "xs", tone: "muted", className: "font-mono", children: [entry.user.email, " / ", entry.password] }, entry.user.id)))] }) }) }));
86
+ const t = useT();
87
+ return (_jsx(Card, { className: "border-dashed", children: _jsx(CardContent, { className: "pt-4", children: _jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", tone: "muted", className: "font-medium", children: t('auth.signIn.devAccounts.title') }), entries.map((entry) => (_jsxs(Text, { size: "xs", tone: "muted", className: "font-mono", children: [entry.user.email, " / ", entry.password] }, entry.user.id)))] }) }) }));
74
88
  }
75
89
  //# sourceMappingURL=sign-in-view.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sign-in-view.js","sourceRoot":"","sources":["../../src/components/sign-in-view.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from '@elytracms/studio/components/auth-context'\nimport { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth'\nimport type { SignInResult } from '@elytracms/studio/lib/auth'\nimport { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace'\n\n/**\n * Engine-shipped `/sign-in` screen (EC-150; consolidated from the copy-pasted\n * consumer route by EC-375, EC-369 finding #8). The consumer route file keeps\n * only the route object — `createFileRoute('/sign-in')({ validateSearch,\n * component: SignInView })` — because TanStack Router requires the route object\n * to live in the consumer's route tree; the ~140-line body lives here so a\n * copy never drifts.\n *\n * Deterministic states: idle → submitting → (signed-in redirect | inline\n * structured failure). Invalid credentials are a rendered state, never a silent\n * failure or crash. In playground (in-memory) mode the seeded dev credentials\n * are listed so the whole flow works offline.\n *\n * Split into a `SignInView` container (owns the router + auth seams) and a\n * provider-free `SignInScreen` (pure local form state + injected callbacks) so\n * the screen static-renders in a node smoke test without the full provider tree.\n */\n\ntype FormPhase = 'idle' | 'submitting'\ntype AuthMode = 'signin' | 'signup'\n\n/** A seeded dev-credential entry surfaced in playground mode (fixtures, not secrets). */\ntype DevCredential = (typeof LOCAL_DEV_CREDENTIALS)[number]\n\nexport interface SignInViewProps {\n /**\n * Brand mark rendered above the heading. Defaults to the neutral studio\n * placeholder square — a future logo just passes a node here (e.g.\n * `component: () => <SignInView brandMark={<Logo />} />`), no engine change.\n */\n brandMark?: ReactNode\n /** Product name in the \"Sign in to …\" heading. Default: `'Elytra'`. */\n productName?: string\n}\n\n/** The project whose backend this sign-in authenticates against (EC-179). */\nfunction activeProjectName(): string | null {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return null\n const slug = getActiveProjectSlug()\n const project = (slug && state.projects.find((p) => p.slug === slug)) || state.projects[0]\n return project?.name ?? null\n}\n\n/**\n * The `/sign-in` route component. Container: reads the router (post-sign-in\n * redirect target from the route search) + auth seams and hands the pure\n * presentational screen its data plus an `authenticate` callback.\n */\nexport function SignInView({ brandMark, productName }: SignInViewProps) {\n const auth = useAuth()\n const navigate = useNavigate()\n // Self-source the redirect (loose search — the consumer route's `validateSearch`\n // guarantees `{ redirect?: string }`), so the route file needs no wrapper.\n const search = useSearch({ strict: false }) as { redirect?: string }\n const redirect = search.redirect\n\n return (\n <SignInScreen\n projectName={activeProjectName()}\n showModeToggle={authMode() === 'convex'}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n brandMark={brandMark}\n productName={productName}\n authenticate={(mode, email, password) =>\n mode === 'signup' ? auth.signUp(email, password) : auth.signIn(email, password)\n }\n onAuthenticated={() => navigate({ to: postSignInTarget(redirect) })}\n />\n )\n}\n\ninterface SignInScreenProps {\n projectName: string | null\n /** Whether to offer the sign-in ↔ create-account toggle (convex mode only). */\n showModeToggle: boolean\n /** Seeded dev accounts to list (playground mode), or null to hide the hint. */\n devCredentials: readonly DevCredential[] | null\n brandMark?: ReactNode\n productName?: string\n /** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */\n authenticate: (mode: AuthMode, email: string, password: string) => Promise<SignInResult>\n /** Called once, after a successful authentication (the container navigates). */\n onAuthenticated: () => void | Promise<void>\n}\n\n/**\n * Provider-free presentational sign-in screen. Owns only local form state and\n * calls injected callbacks — no context hooks — so it static-renders in tests.\n */\nexport function SignInScreen({\n projectName,\n showModeToggle,\n devCredentials,\n brandMark,\n productName = 'Elytra',\n authenticate,\n onAuthenticated,\n}: SignInScreenProps) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [phase, setPhase] = useState<FormPhase>('idle')\n const [mode, setMode] = useState<AuthMode>('signin')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setPhase('submitting')\n setError(null)\n const result = await authenticate(mode, email, password)\n if (result.ok) {\n await onAuthenticated()\n return\n }\n // A fresh backend has no account yet: nudge sign-in → create, and vice versa.\n if (result.reason === 'account-exists') setMode('signin')\n setError(result.message)\n setPhase('idle')\n }\n\n const submitting = phase === 'submitting'\n const creating = mode === 'signup'\n\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background p-6\">\n <Stack gap=\"4\" className=\"w-full max-w-sm\">\n <Stack gap=\"1\" align=\"center\">\n {brandMark ?? <span className=\"size-8 rounded-lg bg-primary shadow-sm\" aria-hidden />}\n <Heading size=\"h2\">\n {creating ? 'Create your account' : `Sign in to ${productName}`}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {projectName\n ? `${creating ? 'Register on' : 'Continue to'} ${projectName}.`\n : 'Use your studio account to continue.'}\n </Text>\n </Stack>\n\n <Card>\n <CardContent className=\"pt-5\">\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"4\">\n <Field label=\"Email\" htmlFor=\"sign-in-email\" required>\n <Input\n id=\"sign-in-email\"\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"you@example.com\"\n disabled={submitting}\n />\n </Field>\n <Field label=\"Password\" htmlFor=\"sign-in-password\" required error={error}>\n <Input\n id=\"sign-in-password\"\n type=\"password\"\n autoComplete={creating ? 'new-password' : 'current-password'}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Button type=\"submit\" disabled={submitting} className=\"w-full\">\n {submitting\n ? creating\n ? 'Creating account…'\n : 'Signing in…'\n : creating\n ? 'Create account'\n : 'Sign in'}\n </Button>\n </Stack>\n </form>\n </CardContent>\n </Card>\n\n {showModeToggle ? (\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\">\n {creating ? 'Already have an account on this backend? ' : 'First time on this backend? '}\n <button\n type=\"button\"\n className=\"font-medium text-primary underline-offset-2 hover:underline\"\n onClick={() => {\n setMode(creating ? 'signin' : 'signup')\n setError(null)\n }}\n >\n {creating ? 'Sign in' : 'Create an account'}\n </button>\n </Text>\n ) : null}\n\n {devCredentials ? <DevCredentialsHint entries={devCredentials} /> : null}\n </Stack>\n </div>\n )\n}\n\n/** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */\nfunction DevCredentialsHint({ entries }: { entries: readonly DevCredential[] }) {\n return (\n <Card className=\"border-dashed\">\n <CardContent className=\"pt-4\">\n <Stack gap=\"1\">\n <Text size=\"xs\" tone=\"muted\" className=\"font-medium\">\n Playground mode — seeded dev accounts\n </Text>\n {entries.map((entry) => (\n <Text key={entry.user.id} size=\"xs\" tone=\"muted\" className=\"font-mono\">\n {entry.user.email} / {entry.password}\n </Text>\n ))}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
1
+ {"version":3,"file":"sign-in-view.js","sourceRoot":"","sources":["../../src/components/sign-in-view.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { useNavigate, useSearch } from '@tanstack/react-router'\nimport { Button, Card, CardContent, Field, Heading, Input, Stack, Text } from '@elytracms/studio/ui'\nimport { useAuth } from '@elytracms/studio/components/auth-context'\nimport { LOCAL_DEV_CREDENTIALS, authMode, postSignInTarget } from '@elytracms/studio/lib/auth'\nimport type { SignInResult } from '@elytracms/studio/lib/auth'\nimport { signInErrorMessageKey } from '@elytracms/studio/lib/auth-errors'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { getActiveProjectSlug, getWorkspaceState } from '@elytracms/studio/lib/workspace'\n\n/**\n * Engine-shipped `/sign-in` screen (EC-150; consolidated from the copy-pasted\n * consumer route by EC-375, EC-369 finding #8). The consumer route file keeps\n * only the route object — `createFileRoute('/sign-in')({ validateSearch,\n * component: SignInView })` — because TanStack Router requires the route object\n * to live in the consumer's route tree; the ~140-line body lives here so a\n * copy never drifts.\n *\n * Deterministic states: idle → submitting → (signed-in redirect | inline\n * structured failure). Invalid credentials are a rendered state, never a silent\n * failure or crash. In playground (in-memory) mode the seeded dev credentials\n * are listed so the whole flow works offline.\n *\n * Split into a `SignInView` container (owns the router + auth seams) and a\n * provider-free `SignInScreen` (pure local form state + injected callbacks) so\n * the screen static-renders in a node smoke test without the full provider tree.\n *\n * All copy resolves through the studio message catalog (`auth.signIn.*`,\n * EC-397), including the engine-classified auth failures — see\n * `signInErrorMessageKey`; an unrecognized failure renders `result.message`\n * raw (honest fallback).\n */\n\ntype FormPhase = 'idle' | 'submitting'\ntype AuthMode = 'signin' | 'signup'\n\n/** A seeded dev-credential entry surfaced in playground mode (fixtures, not secrets). */\ntype DevCredential = (typeof LOCAL_DEV_CREDENTIALS)[number]\n\nexport interface SignInViewProps {\n /**\n * Brand mark rendered above the heading. Defaults to the neutral studio\n * placeholder square — a future logo just passes a node here (e.g.\n * `component: () => <SignInView brandMark={<Logo />} />`), no engine change.\n */\n brandMark?: ReactNode\n /** Product name in the \"Sign in to …\" heading. Default: `'Elytra'`. */\n productName?: string\n}\n\n/**\n * The project whose backend this sign-in authenticates against (EC-179).\n * Exported for the session-expired re-auth overlay (EC-403), which mounts\n * {@link SignInScreen} with the same subtext.\n */\nexport function activeProjectName(): string | null {\n const state = getWorkspaceState()\n if (state.kind !== 'config') return null\n const slug = getActiveProjectSlug()\n const project = (slug && state.projects.find((p) => p.slug === slug)) || state.projects[0]\n return project?.name ?? null\n}\n\n/**\n * The `/sign-in` route component. Container: reads the router (post-sign-in\n * redirect target from the route search) + auth seams and hands the pure\n * presentational screen its data plus an `authenticate` callback.\n */\nexport function SignInView({ brandMark, productName }: SignInViewProps) {\n const auth = useAuth()\n const navigate = useNavigate()\n // Self-source the redirect (loose search — the consumer route's `validateSearch`\n // guarantees `{ redirect?: string }`), so the route file needs no wrapper.\n const search = useSearch({ strict: false }) as { redirect?: string }\n const redirect = search.redirect\n\n return (\n <SignInScreen\n projectName={activeProjectName()}\n showModeToggle={authMode() === 'convex'}\n devCredentials={authMode() === 'playground' ? LOCAL_DEV_CREDENTIALS : null}\n brandMark={brandMark}\n productName={productName}\n authenticate={(mode, email, password) =>\n mode === 'signup' ? auth.signUp(email, password) : auth.signIn(email, password)\n }\n onAuthenticated={() => navigate({ to: postSignInTarget(redirect) })}\n />\n )\n}\n\ninterface SignInScreenProps {\n projectName: string | null\n /** Whether to offer the sign-in ↔ create-account toggle (convex mode only). */\n showModeToggle: boolean\n /** Seeded dev accounts to list (playground mode), or null to hide the hint. */\n devCredentials: readonly DevCredential[] | null\n /**\n * Contextual notice rendered above the heading — the session-expired\n * re-auth overlay (EC-403) explains WHY the screen appeared here. The\n * normal `/sign-in` route passes none.\n */\n notice?: ReactNode\n brandMark?: ReactNode\n productName?: string\n /** Attempt auth for the chosen mode. The container maps this to signIn/signUp. */\n authenticate: (mode: AuthMode, email: string, password: string) => Promise<SignInResult>\n /** Called once, after a successful authentication (the container navigates). */\n onAuthenticated: () => void | Promise<void>\n}\n\n/**\n * Provider-free presentational sign-in screen. Owns only local form state and\n * calls injected callbacks — no context hooks — so it static-renders in tests.\n */\nexport function SignInScreen({\n projectName,\n showModeToggle,\n devCredentials,\n notice,\n brandMark,\n productName = 'Elytra',\n authenticate,\n onAuthenticated,\n}: SignInScreenProps) {\n const t = useT()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [phase, setPhase] = useState<FormPhase>('idle')\n const [mode, setMode] = useState<AuthMode>('signin')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setPhase('submitting')\n setError(null)\n const result = await authenticate(mode, email, password)\n if (result.ok) {\n await onAuthenticated()\n return\n }\n // A fresh backend has no account yet: nudge sign-in → create, and vice versa.\n if (result.reason === 'account-exists') setMode('signin')\n // EC-397: engine-classified failures (EC-362/EC-363 reasons) localize\n // through the message catalog; an unrecognized shape renders the adapter's\n // raw English message — honest, never a stale mapping.\n const errorKey = signInErrorMessageKey(result)\n setError(errorKey ? t(errorKey) : result.message)\n setPhase('idle')\n }\n\n const submitting = phase === 'submitting'\n const creating = mode === 'signup'\n\n return (\n <div className=\"grid min-h-dvh place-items-center bg-background p-6\">\n <Stack gap=\"4\" className=\"w-full max-w-sm\">\n {notice}\n <Stack gap=\"1\" align=\"center\">\n {brandMark ?? <span className=\"size-8 rounded-lg bg-primary shadow-sm\" aria-hidden />}\n <Heading size=\"h2\">\n {creating ? t('auth.signIn.headingCreate') : t('auth.signIn.heading', { productName })}\n </Heading>\n <Text size=\"sm\" tone=\"muted\">\n {projectName\n ? t(creating ? 'auth.signIn.subtext.register' : 'auth.signIn.subtext.continue', {\n project: projectName,\n })\n : t('auth.signIn.subtext.generic')}\n </Text>\n </Stack>\n\n <Card>\n <CardContent className=\"pt-5\">\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"4\">\n <Field label={t('auth.signIn.emailLabel')} htmlFor=\"sign-in-email\" required>\n <Input\n id=\"sign-in-email\"\n type=\"email\"\n autoComplete=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={t('auth.signIn.emailPlaceholder')}\n disabled={submitting}\n />\n </Field>\n <Field\n label={t('auth.signIn.passwordLabel')}\n htmlFor=\"sign-in-password\"\n required\n error={error}\n >\n <Input\n id=\"sign-in-password\"\n type=\"password\"\n autoComplete={creating ? 'new-password' : 'current-password'}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Button type=\"submit\" disabled={submitting} className=\"w-full\">\n {submitting\n ? creating\n ? t('auth.signIn.submittingCreate')\n : t('auth.signIn.submitting')\n : creating\n ? t('auth.signIn.submitCreate')\n : t('auth.signIn.submit')}\n </Button>\n </Stack>\n </form>\n </CardContent>\n </Card>\n\n {showModeToggle ? (\n <Text size=\"sm\" tone=\"muted\" className=\"text-center\">\n {creating ? t('auth.signIn.toggle.alreadyHave') : t('auth.signIn.toggle.firstTime')}{' '}\n <button\n type=\"button\"\n className=\"font-medium text-primary underline-offset-2 hover:underline\"\n onClick={() => {\n setMode(creating ? 'signin' : 'signup')\n setError(null)\n }}\n >\n {creating ? t('auth.signIn.toggle.signIn') : t('auth.signIn.toggle.createAccount')}\n </button>\n </Text>\n ) : null}\n\n {devCredentials ? <DevCredentialsHint entries={devCredentials} /> : null}\n </Stack>\n </div>\n )\n}\n\n/** Seeded dev accounts for the explicit playground mode — fixtures, not secrets. */\nfunction DevCredentialsHint({ entries }: { entries: readonly DevCredential[] }) {\n const t = useT()\n return (\n <Card className=\"border-dashed\">\n <CardContent className=\"pt-4\">\n <Stack gap=\"1\">\n <Text size=\"xs\" tone=\"muted\" className=\"font-medium\">\n {t('auth.signIn.devAccounts.title')}\n </Text>\n {entries.map((entry) => (\n <Text key={entry.user.id} size=\"xs\" tone=\"muted\" className=\"font-mono\">\n {entry.user.email} / {entry.password}\n </Text>\n ))}\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
- import { Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch } from '@elytracms/studio/router';
3
+ import { launcherVisitState, Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch, } from '@elytracms/studio/router';
4
4
  import { matchStudioRoute } from '@elytracms/studio/router/match';
5
5
  import { studioSearch } from '@elytracms/studio/router/search';
6
6
  import { Button, Heading, Stack, Text } from '@elytracms/studio/ui';
@@ -93,7 +93,7 @@ function StudioSectionResolver({ project, match, search, }) {
93
93
  return _jsx(_Fragment, { children: view.render({ project, projectId, deepLink, search }) });
94
94
  }
95
95
  function ProjectNotFound() {
96
- return (_jsx("div", { className: "grid min-h-dvh place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { size: "display", children: translate('shell.projectNotFound.title') }), _jsx(Text, { tone: "muted", children: translate('shell.projectNotFound.body') }), _jsx(Button, { asChild: true, children: _jsx(Link, { to: "/", children: translate('shell.backToProjects') }) })] }) }));
96
+ return (_jsx("div", { className: "grid min-h-dvh place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { size: "display", children: translate('shell.projectNotFound.title') }), _jsx(Text, { tone: "muted", children: translate('shell.projectNotFound.body') }), _jsx(Button, { asChild: true, children: _jsx(Link, { to: "/", state: launcherVisitState, children: translate('shell.backToProjects') }) })] }) }));
97
97
  }
98
98
  function StudioRouteNotFound() {
99
99
  return (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsxs(Stack, { gap: "2", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { level: 2, size: "h2", children: translate('shell.routeNotFound.title') }), _jsx(Text, { tone: "muted", children: translate('shell.routeNotFound.body') })] }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"studio-project-screen.js","sourceRoot":"","sources":["../../src/components/studio-project-screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport { Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch } from '@elytracms/studio/router'\nimport { matchStudioRoute } from '@elytracms/studio/router/match'\nimport type { StudioRouteMatch } from '@elytracms/studio/router/match'\nimport { studioSearch } from '@elytracms/studio/router/search'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\nimport { Button, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { DataProvider } from '@elytracms/studio/lib/data'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useStudio } from './studio-context'\nimport { StudioShell } from './studio-shell'\nimport { AssetsProvider } from './assets/assets-store'\nimport { CollabProvider } from './collab/collab-context'\nimport { ProjectSessionProvider } from './project-session'\nimport { flatPath } from './app-views/module'\nimport { landingViewId, useStudioNav } from './app-views/sidebar'\n\n/**\n * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.\n *\n * The consumer mounts ONE splat route (`/$projectId/$`) whose component is this.\n * Everything that was a per-project file route — the provider stack, the section\n * rail shell, and every section screen — now lives inside the package and is\n * resolved here from the URL. The consumer's route tree shrinks to a single\n * ~10-line mount, which is the whole point of Stage 6: the engine owns its\n * navigation, so an external repo consumes the studio without rebuilding a route\n * file per screen.\n *\n * `projectId` stays a real route param (the `$projectId` segment); the trailing\n * pathname (`_splat`) is matched against the engine route table\n * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)\n * resolves the screen. Every surface already takes its ids as props, so the\n * resolver just hands the module its parsed deep-link.\n */\nexport function StudioProjectScreen() {\n const params = useParams({ strict: false }) as { projectId?: string; _splat?: string }\n // The route's VALIDATED search (the splat's `validateSearch: studioSearch`).\n // Read it through `useSearch`, not `useLocation().search`: the validated search\n // is resolved at route-match time and stable from the component's first render,\n // whereas `location.search` can lag a tick during hydration — long enough for a\n // child `<Navigate>` (the builder redirect) to fire on a momentarily-empty\n // search and drop its `?page=` deep-link target.\n const rawSearch = useSearch({ strict: false }) as Record<string, unknown>\n const studio = useStudio()\n const projectId = params.projectId ?? ''\n const project = studio.getProject(projectId)\n\n // Record the active project once it resolves (preserves recents across sections).\n useEffect(() => {\n if (project) studio.activate(project.id)\n }, [project, studio])\n\n if (!project) return <ProjectNotFound />\n\n const match = matchStudioRoute(params._splat ?? '')\n const search = studioSearch(rawSearch)\n\n return (\n <StudioShell project={project}>\n {/* EC-177 gap 1: in Convex mode the session HYDRATES from persistence\n before any provider mounts — the builder boots from the latest\n persisted revision, publishing reflects the persisted state, and the\n preview content engine serves live documents. Playground renders\n synchronously from fixtures (session: null), exactly as before. */}\n <ProjectSessionProvider key={project.id} projectId={project.id}>\n <ProjectProviders project={project}>\n <StudioSectionResolver project={project} match={match} search={search} />\n </ProjectProviders>\n </ProjectSessionProvider>\n </StudioShell>\n )\n}\n\n/**\n * The per-project provider stack, mounted once the session source is known.\n *\n * EC-287: there is no project-level builder graph anymore — layouts are code and\n * the DB holds content only. The block-field canvas mounts its OWN scoped\n * `BuilderProvider` per document (`composition-field-editor`), so no graph is\n * threaded here. `CollabProvider` stays as the canvas's session-scoped\n * presence/perspective store, seeded graph-lessly.\n */\nfunction ProjectProviders({ project, children }: { project: Project; children: ReactNode }) {\n return (\n // Key by project id so the session data sources reset when switching projects.\n // The data store hydrates/persists this project's `cms-data-sources` records.\n <DataProvider key={project.id} projectId={project.id}>\n {/* EC-195: project-level so the canvas resolves `asset`-typed Image props\n and the inspector picks from the live library — not just the Assets\n section. Asset METADATA only (small); blobs stay lazy. */}\n <AssetsProvider key={project.id} projectId={project.id}>\n <CollabProvider key={project.id} projectId={project.id}>\n {children}\n </CollabProvider>\n </AssetsProvider>\n </DataProvider>\n )\n}\n\n/**\n * The URL→screen switch (EC-223 Stage 6), driven by the view registry DERIVED from\n * the section rail (EC-237/239). The bare project URL redirects to the first\n * navigable rail view; otherwise the matched view id is resolved from the registry\n * (rail views + always-on built-ins) and its `parsePath` interprets the trailing\n * segments into a deep-link the view renders. An unknown view id or an unparseable\n * path is a not-found; rail CONFIG problems (duplicate/invalid views) surface as\n * visible issues in the shell, never silently. Renders inside the shell + provider\n * stack, so every surface sees the same live CMS store and breadcrumb context.\n */\nfunction StudioSectionResolver({\n project,\n match,\n search,\n}: {\n project: Project\n match: StudioRouteMatch\n search: StudioSearch\n}) {\n const projectId = project.id\n const nav = useStudioNav(project)\n\n if (match.section === 'index') {\n // Bare `/$projectId` → the project's first navigable rail view.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: landingViewId(nav, project) }}\n replace\n />\n )\n }\n\n const view = nav.registry.get(match.viewId)\n if (!view) return <StudioRouteNotFound />\n\n // A flat view (no `parsePath`) accepts only the bare path; a nested one\n // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.\n const deepLink = (view.parsePath ?? flatPath)(match.segments)\n if (deepLink === null) return <StudioRouteNotFound />\n\n return <>{view.render({ project, projectId, deepLink, search })}</>\n}\n\nfunction ProjectNotFound() {\n return (\n <div className=\"grid min-h-dvh place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading size=\"display\">{translate('shell.projectNotFound.title')}</Heading>\n <Text tone=\"muted\">{translate('shell.projectNotFound.body')}</Text>\n <Button asChild>\n <Link to=\"/\">{translate('shell.backToProjects')}</Link>\n </Button>\n </Stack>\n </div>\n )\n}\n\nfunction StudioRouteNotFound() {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"2\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n {translate('shell.routeNotFound.title')}\n </Heading>\n <Text tone=\"muted\">{translate('shell.routeNotFound.body')}</Text>\n </Stack>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"studio-project-screen.js","sourceRoot":"","sources":["../../src/components/studio-project-screen.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGjC,OAAO,EACL,kBAAkB,EAClB,IAAI,EACJ,QAAQ,EACR,oBAAoB,EACpB,SAAS,EACT,SAAS,GACV,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AAEjE,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAE9D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEjE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAA4C,CAAA;IACtF,6EAA6E;IAC7E,gFAAgF;IAChF,gFAAgF;IAChF,gFAAgF;IAChF,2EAA2E;IAC3E,iDAAiD;IACjD,MAAM,SAAS,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAA4B,CAAA;IACzE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,EAAE,CAAA;IACxC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;IAE5C,kFAAkF;IAClF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO;YAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC1C,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA;IAErB,IAAI,CAAC,OAAO;QAAE,OAAO,KAAC,eAAe,KAAG,CAAA;IAExC,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IACnD,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,CAAA;IAEtC,OAAO,CACL,KAAC,WAAW,IAAC,OAAO,EAAE,OAAO,YAM3B,KAAC,sBAAsB,IAAkB,SAAS,EAAE,OAAO,CAAC,EAAE,YAC5D,KAAC,gBAAgB,IAAC,OAAO,EAAE,OAAO,YAChC,KAAC,qBAAqB,IAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAI,GACxD,IAHQ,OAAO,CAAC,EAAE,CAId,GACb,CACf,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,gBAAgB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAA6C;IACxF,OAAO,CAGL,KAAC,YAAY,IAAkB,SAAS,EAAE,OAAO,CAAC,EAAE,YAIlD,KAAC,cAAc,IAAkB,SAAS,EAAE,OAAO,CAAC,EAAE,YACpD,KAAC,cAAc,IAAkB,SAAS,EAAE,OAAO,CAAC,EAAE,YACnD,QAAQ,IADU,OAAO,CAAC,EAAE,CAEd,IAHE,OAAO,CAAC,EAAE,CAId,IARA,OAAO,CAAC,EAAE,CASd,CAChB,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,qBAAqB,CAAC,EAC7B,OAAO,EACP,KAAK,EACL,MAAM,GAKP;IACC,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,CAAA;IAC5B,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;IAEjC,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;QAC9B,gEAAgE;QAChE,OAAO,CACL,KAAC,QAAQ,IACP,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,EAC1D,OAAO,SACP,CACH,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAC3C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAC,mBAAmB,KAAG,CAAA;IAEzC,wEAAwE;IACxE,oFAAoF;IACpF,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC7D,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAC,mBAAmB,KAAG,CAAA;IAErD,OAAO,4BAAG,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,GAAI,CAAA;AACrE,CAAC;AAED,SAAS,eAAe;IACtB,OAAO,CACL,cAAK,SAAS,EAAC,uCAAuC,YACpD,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,sBAAsB,aAC5D,KAAC,OAAO,IAAC,IAAI,EAAC,SAAS,YAAE,SAAS,CAAC,6BAA6B,CAAC,GAAW,EAC5E,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,SAAS,CAAC,4BAA4B,CAAC,GAAQ,EACnE,KAAC,MAAM,IAAC,OAAO,kBAGb,KAAC,IAAI,IAAC,EAAE,EAAC,GAAG,EAAC,KAAK,EAAE,kBAAkB,YACnC,SAAS,CAAC,sBAAsB,CAAC,GAC7B,GACA,IACH,GACJ,CACP,CAAA;AACH,CAAC;AAED,SAAS,mBAAmB;IAC1B,OAAO,CACL,cAAK,SAAS,EAAC,oCAAoC,YACjD,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,sBAAsB,aAC5D,KAAC,OAAO,IAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAC,IAAI,YACzB,SAAS,CAAC,2BAA2B,CAAC,GAC/B,EACV,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,SAAS,CAAC,0BAA0B,CAAC,GAAQ,IAC3D,GACJ,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport {\n launcherVisitState,\n Link,\n Navigate,\n STUDIO_PROJECT_ROUTE,\n useParams,\n useSearch,\n} from '@elytracms/studio/router'\nimport { matchStudioRoute } from '@elytracms/studio/router/match'\nimport type { StudioRouteMatch } from '@elytracms/studio/router/match'\nimport { studioSearch } from '@elytracms/studio/router/search'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\nimport { Button, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { DataProvider } from '@elytracms/studio/lib/data'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useStudio } from './studio-context'\nimport { StudioShell } from './studio-shell'\nimport { AssetsProvider } from './assets/assets-store'\nimport { CollabProvider } from './collab/collab-context'\nimport { ProjectSessionProvider } from './project-session'\nimport { flatPath } from './app-views/module'\nimport { landingViewId, useStudioNav } from './app-views/sidebar'\n\n/**\n * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.\n *\n * The consumer mounts ONE splat route (`/$projectId/$`) whose component is this.\n * Everything that was a per-project file route — the provider stack, the section\n * rail shell, and every section screen — now lives inside the package and is\n * resolved here from the URL. The consumer's route tree shrinks to a single\n * ~10-line mount, which is the whole point of Stage 6: the engine owns its\n * navigation, so an external repo consumes the studio without rebuilding a route\n * file per screen.\n *\n * `projectId` stays a real route param (the `$projectId` segment); the trailing\n * pathname (`_splat`) is matched against the engine route table\n * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)\n * resolves the screen. Every surface already takes its ids as props, so the\n * resolver just hands the module its parsed deep-link.\n */\nexport function StudioProjectScreen() {\n const params = useParams({ strict: false }) as { projectId?: string; _splat?: string }\n // The route's VALIDATED search (the splat's `validateSearch: studioSearch`).\n // Read it through `useSearch`, not `useLocation().search`: the validated search\n // is resolved at route-match time and stable from the component's first render,\n // whereas `location.search` can lag a tick during hydration — long enough for a\n // child `<Navigate>` (the builder redirect) to fire on a momentarily-empty\n // search and drop its `?page=` deep-link target.\n const rawSearch = useSearch({ strict: false }) as Record<string, unknown>\n const studio = useStudio()\n const projectId = params.projectId ?? ''\n const project = studio.getProject(projectId)\n\n // Record the active project once it resolves (preserves recents across sections).\n useEffect(() => {\n if (project) studio.activate(project.id)\n }, [project, studio])\n\n if (!project) return <ProjectNotFound />\n\n const match = matchStudioRoute(params._splat ?? '')\n const search = studioSearch(rawSearch)\n\n return (\n <StudioShell project={project}>\n {/* EC-177 gap 1: in Convex mode the session HYDRATES from persistence\n before any provider mounts — the builder boots from the latest\n persisted revision, publishing reflects the persisted state, and the\n preview content engine serves live documents. Playground renders\n synchronously from fixtures (session: null), exactly as before. */}\n <ProjectSessionProvider key={project.id} projectId={project.id}>\n <ProjectProviders project={project}>\n <StudioSectionResolver project={project} match={match} search={search} />\n </ProjectProviders>\n </ProjectSessionProvider>\n </StudioShell>\n )\n}\n\n/**\n * The per-project provider stack, mounted once the session source is known.\n *\n * EC-287: there is no project-level builder graph anymore — layouts are code and\n * the DB holds content only. The block-field canvas mounts its OWN scoped\n * `BuilderProvider` per document (`composition-field-editor`), so no graph is\n * threaded here. `CollabProvider` stays as the canvas's session-scoped\n * presence/perspective store, seeded graph-lessly.\n */\nfunction ProjectProviders({ project, children }: { project: Project; children: ReactNode }) {\n return (\n // Key by project id so the session data sources reset when switching projects.\n // The data store hydrates/persists this project's `cms-data-sources` records.\n <DataProvider key={project.id} projectId={project.id}>\n {/* EC-195: project-level so the canvas resolves `asset`-typed Image props\n and the inspector picks from the live library — not just the Assets\n section. Asset METADATA only (small); blobs stay lazy. */}\n <AssetsProvider key={project.id} projectId={project.id}>\n <CollabProvider key={project.id} projectId={project.id}>\n {children}\n </CollabProvider>\n </AssetsProvider>\n </DataProvider>\n )\n}\n\n/**\n * The URL→screen switch (EC-223 Stage 6), driven by the view registry DERIVED from\n * the section rail (EC-237/239). The bare project URL redirects to the first\n * navigable rail view; otherwise the matched view id is resolved from the registry\n * (rail views + always-on built-ins) and its `parsePath` interprets the trailing\n * segments into a deep-link the view renders. An unknown view id or an unparseable\n * path is a not-found; rail CONFIG problems (duplicate/invalid views) surface as\n * visible issues in the shell, never silently. Renders inside the shell + provider\n * stack, so every surface sees the same live CMS store and breadcrumb context.\n */\nfunction StudioSectionResolver({\n project,\n match,\n search,\n}: {\n project: Project\n match: StudioRouteMatch\n search: StudioSearch\n}) {\n const projectId = project.id\n const nav = useStudioNav(project)\n\n if (match.section === 'index') {\n // Bare `/$projectId` → the project's first navigable rail view.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: landingViewId(nav, project) }}\n replace\n />\n )\n }\n\n const view = nav.registry.get(match.viewId)\n if (!view) return <StudioRouteNotFound />\n\n // A flat view (no `parsePath`) accepts only the bare path; a nested one\n // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.\n const deepLink = (view.parsePath ?? flatPath)(match.segments)\n if (deepLink === null) return <StudioRouteNotFound />\n\n return <>{view.render({ project, projectId, deepLink, search })}</>\n}\n\nfunction ProjectNotFound() {\n return (\n <div className=\"grid min-h-dvh place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading size=\"display\">{translate('shell.projectNotFound.title')}</Heading>\n <Text tone=\"muted\">{translate('shell.projectNotFound.body')}</Text>\n <Button asChild>\n {/* EC-401: an explicit \"back to projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <Link to=\"/\" state={launcherVisitState}>\n {translate('shell.backToProjects')}\n </Link>\n </Button>\n </Stack>\n </div>\n )\n}\n\nfunction StudioRouteNotFound() {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"2\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n {translate('shell.routeNotFound.title')}\n </Heading>\n <Text tone=\"muted\">{translate('shell.routeNotFound.body')}</Text>\n </Stack>\n </div>\n )\n}\n"]}
@@ -1,17 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router';
2
+ import { launcherVisitState, Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router';
3
3
  import { ChevronRight } from 'lucide-react';
4
4
  import { useCallback, useEffect, useState } from 'react';
5
5
  import { Row, Spacer, Toaster } from '@elytracms/studio/ui';
6
6
  import { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail';
7
+ import { composeTabTitle, studioBaseTitle, tabTitleParts } from '@elytracms/studio/lib/tab-title';
8
+ import { isSingleProjectWorkspace } from '@elytracms/studio/lib/workspace';
7
9
  import { translate } from '@elytracms/studio/lib/i18n';
8
10
  import { CurrentUserMenu } from './current-user-menu.js';
9
11
  import { DeliveryHealthBanner } from './delivery-health-banner.js';
10
12
  import { PlaygroundBadge } from './playground-badge.js';
11
13
  import { DevLoginBadge } from './dev-login-badge.js';
12
14
  import { AiPanelToggle } from './ai/ai-panel-toggle.js';
13
- import { ProjectSwitcher } from './project-switcher.js';
15
+ import { ActiveProjectName, ProjectSwitcher } from './project-switcher.js';
14
16
  import { SectionNav } from './section-nav.js';
17
+ import { useSwitcherEntries } from './workspace-projects.js';
15
18
  import { activeViewIdFromPath, useStudioNav } from './app-views/sidebar.js';
16
19
  import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context.js';
17
20
  /**
@@ -24,6 +27,11 @@ import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context.js'
24
27
  export function StudioShell({ project, children }) {
25
28
  const { pathname } = useLocation();
26
29
  const activeViewId = activeViewIdFromPath(pathname);
30
+ // EC-401: the shared single-project predicate — the same one that drives the
31
+ // launcher's auto-forward — decides whether the header slot carries the
32
+ // switcher dropdown or just the project's name.
33
+ const { entries } = useSwitcherEntries();
34
+ const singleProject = isSingleProjectWorkspace(entries);
27
35
  // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the
28
36
  // stored preference is read after mount (no flash script needed for a rail).
29
37
  const [railCollapsed, setRailCollapsed] = useState(false);
@@ -37,7 +45,36 @@ export function StudioShell({ project, children }) {
37
45
  return next;
38
46
  });
39
47
  }, []);
40
- return (_jsxs(BreadcrumbProvider, { children: [_jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", className: "grid size-6 shrink-0 place-items-center", "aria-label": translate('shell.allProjects'), children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), _jsx(ProjectSwitcher, { activeProject: project }), _jsx(HeaderBreadcrumb, { project: project, activeViewId: activeViewId }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(AiPanelToggle, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, { projectId: project.id }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { project: project, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
48
+ return (_jsxs(BreadcrumbProvider, { children: [_jsx(StudioTabTitle, { project: project, activeViewId: activeViewId }), _jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", state: launcherVisitState, className: "grid size-6 shrink-0 place-items-center", "aria-label": translate('shell.allProjects'), children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), singleProject ? (_jsx(ActiveProjectName, { project: project })) : (_jsx(ProjectSwitcher, { activeProject: project })), _jsx(HeaderBreadcrumb, { project: project, activeViewId: activeViewId }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(AiPanelToggle, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, { projectId: project.id }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { project: project, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
49
+ }
50
+ /**
51
+ * Per-view browser tab title (EC-402). The studio is ONE catch-all splat route,
52
+ * so per-view titles cannot come from per-route `head()` — this effect composes
53
+ * `deepest crumb · view label · project name · base title` from the exact
54
+ * sources the header breadcrumb renders (no new data flow, AD-17): the
55
+ * contributed trail, the nav registry's localized view label, `project.name`,
56
+ * and the `createStudioRootRoute` base title. Assignment is effect-only
57
+ * (SSR-safe); unmounting (launcher, sign-in, project switch) restores the base
58
+ * title, so non-project surfaces keep the consumer's branding untouched.
59
+ */
60
+ function StudioTabTitle({ project, activeViewId, }) {
61
+ const nav = useStudioNav(project);
62
+ const trail = useBreadcrumbTrail();
63
+ const view = activeViewId ? nav.registry.get(activeViewId) : undefined;
64
+ const title = composeTabTitle(tabTitleParts({
65
+ breadcrumb: trail,
66
+ viewLabel: view?.label,
67
+ projectName: project.name,
68
+ baseTitle: studioBaseTitle(),
69
+ }));
70
+ useEffect(() => {
71
+ document.title = title;
72
+ }, [title]);
73
+ // Leaving the project shell restores the base title (launcher/sign-in keep it).
74
+ useEffect(() => () => {
75
+ document.title = studioBaseTitle();
76
+ }, []);
77
+ return null;
41
78
  }
42
79
  const CRUMB_LINK = 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground';
43
80
  const CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground';