@elytracms/studio 0.0.32 → 0.0.33

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 (113) hide show
  1. package/dist/components/app-views/default-modules.js +8 -6
  2. package/dist/components/app-views/default-modules.js.map +1 -1
  3. package/dist/components/assets/asset-library.js +21 -13
  4. package/dist/components/assets/asset-library.js.map +1 -1
  5. package/dist/components/assets/asset-thumb.js +3 -2
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/backend-gate.js +4 -3
  8. package/dist/components/backend-gate.js.map +1 -1
  9. package/dist/components/builder/blocks-panel.js +11 -2
  10. package/dist/components/builder/blocks-panel.js.map +1 -1
  11. package/dist/components/builder/canvas.js +6 -5
  12. package/dist/components/builder/canvas.js.map +1 -1
  13. package/dist/components/builder/clipboard-actions.js +12 -6
  14. package/dist/components/builder/clipboard-actions.js.map +1 -1
  15. package/dist/components/builder/composition-field-editor.js +7 -4
  16. package/dist/components/builder/composition-field-editor.js.map +1 -1
  17. package/dist/components/builder/fixture-thumbnail.js +2 -1
  18. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  19. package/dist/components/builder/layout-inspector.d.ts +5 -5
  20. package/dist/components/builder/layout-inspector.js +21 -20
  21. package/dist/components/builder/layout-inspector.js.map +1 -1
  22. package/dist/components/builder/page-tree.js +5 -4
  23. package/dist/components/builder/page-tree.js.map +1 -1
  24. package/dist/components/builder/property-inspector.js +10 -10
  25. package/dist/components/builder/property-inspector.js.map +1 -1
  26. package/dist/components/builder/reuse-context.js +9 -9
  27. package/dist/components/builder/reuse-context.js.map +1 -1
  28. package/dist/components/builder/rich-text-toolbar.js +4 -3
  29. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  30. package/dist/components/builder/spacing-handles.js +4 -3
  31. package/dist/components/builder/spacing-handles.js.map +1 -1
  32. package/dist/components/cms/asset-picker.js +8 -7
  33. package/dist/components/cms/asset-picker.js.map +1 -1
  34. package/dist/components/cms/bulk-bar.js +8 -5
  35. package/dist/components/cms/bulk-bar.js.map +1 -1
  36. package/dist/components/cms/cell.js +9 -5
  37. package/dist/components/cms/cell.js.map +1 -1
  38. package/dist/components/cms/cms-store.js +4 -3
  39. package/dist/components/cms/cms-store.js.map +1 -1
  40. package/dist/components/cms/collection-table.js +18 -7
  41. package/dist/components/cms/collection-table.js.map +1 -1
  42. package/dist/components/cms/detail-editor.js +38 -27
  43. package/dist/components/cms/detail-editor.js.map +1 -1
  44. package/dist/components/cms/editable-cell.js +4 -4
  45. package/dist/components/cms/editable-cell.js.map +1 -1
  46. package/dist/components/cms/field-control.js +29 -21
  47. package/dist/components/cms/field-control.js.map +1 -1
  48. package/dist/components/cms/filter-builder.js +9 -8
  49. package/dist/components/cms/filter-builder.js.map +1 -1
  50. package/dist/components/cms/reference-confirm.js +24 -10
  51. package/dist/components/cms/reference-confirm.js.map +1 -1
  52. package/dist/components/cms/relation-picker.js +5 -4
  53. package/dist/components/cms/relation-picker.js.map +1 -1
  54. package/dist/components/cms/scalar-value-editor.js +5 -5
  55. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  56. package/dist/components/cms/version-compare.js +36 -21
  57. package/dist/components/cms/version-compare.js.map +1 -1
  58. package/dist/components/cms/versions-pane.js +8 -5
  59. package/dist/components/cms/versions-pane.js.map +1 -1
  60. package/dist/components/cms/view-bar.js +3 -2
  61. package/dist/components/cms/view-bar.js.map +1 -1
  62. package/dist/components/components/component-detail.js +3 -2
  63. package/dist/components/components/component-detail.js.map +1 -1
  64. package/dist/components/components/component-status.js +8 -13
  65. package/dist/components/components/component-status.js.map +1 -1
  66. package/dist/components/components/components-index.js +5 -1
  67. package/dist/components/components/components-index.js.map +1 -1
  68. package/dist/components/components/field-inspector.js +4 -4
  69. package/dist/components/components/field-inspector.js.map +1 -1
  70. package/dist/components/components/fixture-runner.js +4 -1
  71. package/dist/components/components/fixture-runner.js.map +1 -1
  72. package/dist/components/components/registry-browser.js +4 -3
  73. package/dist/components/components/registry-browser.js.map +1 -1
  74. package/dist/components/delivery-health-banner.js +2 -1
  75. package/dist/components/delivery-health-banner.js.map +1 -1
  76. package/dist/components/dev-login-badge.js +2 -1
  77. package/dist/components/dev-login-badge.js.map +1 -1
  78. package/dist/components/extensions/extensions-view.js +22 -21
  79. package/dist/components/extensions/extensions-view.js.map +1 -1
  80. package/dist/components/guards.js +3 -2
  81. package/dist/components/guards.js.map +1 -1
  82. package/dist/components/operation-error.js +3 -2
  83. package/dist/components/operation-error.js.map +1 -1
  84. package/dist/components/playground-badge.js +2 -1
  85. package/dist/components/playground-badge.js.map +1 -1
  86. package/dist/components/project-list.js +5 -4
  87. package/dist/components/project-list.js.map +1 -1
  88. package/dist/components/project-session.js +3 -2
  89. package/dist/components/project-session.js.map +1 -1
  90. package/dist/components/project-switcher.js +4 -3
  91. package/dist/components/project-switcher.js.map +1 -1
  92. package/dist/components/redirects/redirects-module.js +3 -2
  93. package/dist/components/redirects/redirects-module.js.map +1 -1
  94. package/dist/components/references/references-panel.js +7 -6
  95. package/dist/components/references/references-panel.js.map +1 -1
  96. package/dist/components/section-nav.js +8 -5
  97. package/dist/components/section-nav.js.map +1 -1
  98. package/dist/components/section-state.js +13 -32
  99. package/dist/components/section-state.js.map +1 -1
  100. package/dist/components/settings-form.js +2 -1
  101. package/dist/components/settings-form.js.map +1 -1
  102. package/dist/components/studio-project-screen.js +3 -2
  103. package/dist/components/studio-project-screen.js.map +1 -1
  104. package/dist/components/studio-shell.js +3 -2
  105. package/dist/components/studio-shell.js.map +1 -1
  106. package/dist/components/users/users-module.js +5 -4
  107. package/dist/components/users/users-module.js.map +1 -1
  108. package/dist/components/workspace-gate.js +2 -1
  109. package/dist/components/workspace-gate.js.map +1 -1
  110. package/dist/lib/i18n/messages.en.d.ts +11 -4
  111. package/dist/lib/i18n/messages.en.js +636 -6
  112. package/dist/lib/i18n/messages.en.js.map +1 -1
  113. package/package.json +2 -2
@@ -3,6 +3,7 @@ import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router
3
3
  import { Globe, LogIn, TerminalSquare } from 'lucide-react';
4
4
  import { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui';
5
5
  import { hasProjectSession } from '@elytracms/studio/lib/auth';
6
+ import { translate } from '@elytracms/studio/lib/i18n';
6
7
  import { PlaygroundBadge } from './playground-badge';
7
8
  import { SectionStateView } from './section-state';
8
9
  import { useStudio } from './studio-context';
@@ -27,11 +28,11 @@ export function ProjectLauncher({ forcedState }) {
27
28
  .filter((p) => entries.some((e) => e.kind === 'project' && e.project.id === p.id))
28
29
  .slice(0, 4);
29
30
  const forced = forcedState && forcedState !== 'working' ? forcedState : null;
30
- return (_jsxs("div", { className: "mx-auto w-full max-w-6xl px-6 py-8", children: [_jsxs(Row, { gap: "3", className: "mb-6", children: [_jsxs(Stack, { gap: "1", children: [_jsxs(Row, { gap: "2", children: [_jsx(Heading, { size: "display", children: "Projects" }), _jsx(PlaygroundBadge, {}), hasConfig ? (
31
+ return (_jsxs("div", { className: "mx-auto w-full max-w-6xl px-6 py-8", children: [_jsxs(Row, { gap: "3", className: "mb-6", children: [_jsxs(Stack, { gap: "1", children: [_jsxs(Row, { gap: "2", children: [_jsx(Heading, { size: "display", children: translate('shell.projects') }), _jsx(PlaygroundBadge, {}), hasConfig ? (
31
32
  // EC-154: this workspace is defined in code.
32
33
  _jsxs(Badge, { variant: "outline", children: [_jsx(TerminalSquare, { className: "size-3" }), " ", workspace.source] })) : null] }), _jsx(Text, { tone: "muted", children: hasConfig
33
34
  ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'
34
- : 'Open and manage your Elytra projects.' })] }), _jsx(Spacer, {})] }), recents.length > 0 && !forced ? (_jsxs(Stack, { gap: "2", className: "mb-6", children: [_jsx(Text, { size: "xs", tone: "muted", mono: true, children: "RECENT" }), _jsx(Row, { gap: "2", wrap: true, children: recents.map((p) => (_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: p.id, _splat: studioPath.index() }, onClick: () => studio.activate(p.id), className: "inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: p.name.charAt(0).toUpperCase() }), p.name] }, p.id))) })] })) : null, forced ? (_jsx(SectionStateView, { state: forced, title: forced === 'empty' ? 'No projects' : undefined })) : visible.length === 0 ? (_jsx(SectionStateView, { state: "empty", title: "No projects", description: hasConfig
35
+ : 'Open and manage your Elytra projects.' })] }), _jsx(Spacer, {})] }), recents.length > 0 && !forced ? (_jsxs(Stack, { gap: "2", className: "mb-6", children: [_jsx(Text, { size: "xs", tone: "muted", mono: true, className: "uppercase", children: translate('shell.projectList.recent') }), _jsx(Row, { gap: "2", wrap: true, children: recents.map((p) => (_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: p.id, _splat: studioPath.index() }, onClick: () => studio.activate(p.id), className: "inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: p.name.charAt(0).toUpperCase() }), p.name] }, p.id))) })] })) : null, forced ? (_jsx(SectionStateView, { state: forced, title: forced === 'empty' ? translate('shell.projectList.empty.title') : undefined })) : visible.length === 0 ? (_jsx(SectionStateView, { state: "empty", title: translate('shell.projectList.empty.title'), description: hasConfig
35
36
  ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'
36
37
  : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: visible.map((entry) => entry.kind === 'project' ? (_jsx(ProjectCard, { project: entry.project }, entry.project.id)) : (_jsx(UnprovisionedCard, { config: entry.config }, `config:${entry.config.slug}`))) }))] }));
37
38
  }
@@ -40,13 +41,13 @@ function ProjectCard({ project }) {
40
41
  // Per-backend sign-in state (EC-179): one project per backend, so a project on
41
42
  // a deployment the user hasn't authenticated into shows "Sign in", not "Open".
42
43
  const authed = hasProjectSession(project.slug);
43
- return (_jsxs(Card, { className: "flex flex-col gap-3 p-4 transition-colors hover:border-primary/30", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground", children: project.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: project.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: project.slug })] }), _jsx(Spacer, {}), authed ? null : (_jsxs(Badge, { variant: "warning", children: [_jsx(LogIn, { className: "size-3" }), " Sign in"] }))] }), _jsxs(Row, { gap: "2", className: "text-muted-foreground", children: [_jsxs(Badge, { variant: "outline", children: [_jsx(Globe, { className: "size-3" }), " ", project.settings.locales.length] }), _jsx(Spacer, {})] }), _jsx(Button, { variant: authed ? 'secondary' : 'default', size: "sm", asChild: true, className: "mt-1", children: _jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() }, onClick: () => studio.activate(project.id), children: authed ? 'Open project' : (_jsxs(_Fragment, { children: [_jsx(LogIn, {}), " Sign in"] })) }) })] }));
44
+ return (_jsxs(Card, { className: "flex flex-col gap-3 p-4 transition-colors hover:border-primary/30", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground", children: project.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: project.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: project.slug })] }), _jsx(Spacer, {}), authed ? null : (_jsxs(Badge, { variant: "warning", children: [_jsx(LogIn, { className: "size-3" }), " ", translate('auth.signIn')] }))] }), _jsxs(Row, { gap: "2", className: "text-muted-foreground", children: [_jsxs(Badge, { variant: "outline", children: [_jsx(Globe, { className: "size-3" }), " ", project.settings.locales.length] }), _jsx(Spacer, {})] }), _jsx(Button, { variant: authed ? 'secondary' : 'default', size: "sm", asChild: true, className: "mt-1", children: _jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() }, onClick: () => studio.activate(project.id), children: authed ? translate('shell.projectCard.open') : (_jsxs(_Fragment, { children: [_jsx(LogIn, {}), " ", translate('auth.signIn')] })) }) })] }));
44
45
  }
45
46
  /**
46
47
  * A configured project that has no backend project yet (EC-154): an explicit,
47
48
  * deterministic state pointing at the CLI provisioning flow — never an error.
48
49
  */
49
50
  function UnprovisionedCard({ config }) {
50
- return (_jsxs(Card, { className: "flex flex-col gap-3 border-dashed p-4", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground", children: config.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: config.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: config.slug })] }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: "not provisioned" })] }), _jsx(Text, { size: "sm", tone: "muted", children: "Declared in the workspace config, but no backend project exists yet. Provision it from your repo:" }), _jsxs(Code, { className: "block w-fit max-w-full truncate text-xs", children: ["builder init --project ", config.slug] })] }));
51
+ return (_jsxs(Card, { className: "flex flex-col gap-3 border-dashed p-4", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground", children: config.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: config.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: config.slug })] }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsx(Text, { size: "sm", tone: "muted", children: "Declared in the workspace config, but no backend project exists yet. Provision it from your repo:" }), _jsxs(Code, { className: "block w-fit max-w-full truncate text-xs", children: ["builder init --project ", config.slug] })] }));
51
52
  }
52
53
  //# sourceMappingURL=project-list.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = studio.context.recentProjectIds\n .map((id) => studio.getProject(id))\n .filter((p): p is Project => Boolean(p))\n .filter((p) => entries.some((e) => e.kind === 'project' && e.project.id === p.id))\n .slice(0, 4)\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">Projects</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono>\n RECENT\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView state={forced} title={forced === 'empty' ? 'No projects' : undefined} />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title=\"No projects\"\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : (\n <Badge variant=\"warning\">\n <LogIn className=\"size-3\" /> Sign in\n </Badge>\n )}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? 'Open project' : (\n <>\n <LogIn /> Sign in\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">not provisioned</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
1
+ {"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = studio.context.recentProjectIds\n .map((id) => studio.getProject(id))\n .filter((p): p is Project => Boolean(p))\n .filter((p) => entries.some((e) => e.kind === 'project' && e.project.id === p.id))\n .slice(0, 4)\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : (\n <Badge variant=\"warning\">\n <LogIn className=\"size-3\" /> {translate('auth.signIn')}\n </Badge>\n )}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
@@ -5,6 +5,7 @@ import { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytr
5
5
  import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
6
6
  import { configureContentSession } from '@elytracms/studio/lib/data';
7
7
  import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
8
+ import { translate } from '@elytracms/studio/lib/i18n';
8
9
  import { OperationErrorBanner } from './operation-error';
9
10
  const ProjectSessionContext = createContext(null);
10
11
  export function ProjectSessionProvider({ projectId, children, }) {
@@ -103,10 +104,10 @@ export function CmsSessionGate({ children, }) {
103
104
  }
104
105
  /** Deterministic loading frame while persisted state is fetched (mirrors BackendGate). */
105
106
  function SessionHydrating() {
106
- return (_jsx("div", { className: "grid h-full min-h-64 flex-1 place-items-center p-8", "data-testid": "session-hydrating", children: _jsx(Text, { size: "sm", tone: "muted", children: "Loading project\u2026" }) }));
107
+ return (_jsx("div", { className: "grid h-full min-h-64 flex-1 place-items-center p-8", "data-testid": "session-hydrating", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('shell.session.loading') }) }));
107
108
  }
108
109
  /** A failed hydration is a visible, retryable state — never a fixture fallback. */
109
110
  function SessionHydrationError({ error, onRetry, }) {
110
- return (_jsx("div", { className: "grid h-full min-h-64 flex-1 place-items-center p-8", children: _jsxs(Stack, { gap: "3", className: "w-full max-w-xl", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Couldn\u2019t load this project\u2019s saved state" }), _jsx(OperationErrorBanner, { error: error }), _jsx(Text, { size: "xs", tone: "muted", children: "The studio never falls back to sample content against a live backend \u2014 your data is safe; retry once the backend is reachable." }), _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, className: "self-start", children: "Retry" })] }) }));
111
+ return (_jsx("div", { className: "grid h-full min-h-64 flex-1 place-items-center p-8", children: _jsxs(Stack, { gap: "3", className: "w-full max-w-xl", children: [_jsx(Text, { size: "sm", weight: "semibold", children: translate('shell.session.error.title') }), _jsx(OperationErrorBanner, { error: error }), _jsx(Text, { size: "xs", tone: "muted", children: translate('shell.session.error.body') }), _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, className: "self-start", children: translate('common.retry') })] }) }));
111
112
  }
112
113
  //# sourceMappingURL=project-session.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Stack, Text } from '@elytracms/studio/ui'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type {\n HydratedCms,\n HydratedProjectSession,\n OperationError,\n OperationResult,\n} from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { OperationErrorBanner } from './operation-error'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `BackendGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst ProjectSessionContext = createContext<ProjectSessionStore | null>(null)\n\ntype Phase =\n | { kind: 'loading' }\n | { kind: 'ready'; session: HydratedProjectSession | null }\n | { kind: 'error'; error: OperationError }\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n // Playground renders children on the FIRST render (no effect round-trip):\n // fixture boots stay synchronous and deterministic.\n const [phase, setPhase] = useState<Phase>(() =>\n playground ? { kind: 'ready', session: null } : { kind: 'loading' },\n )\n const [attempt, setAttempt] = useState(0)\n\n useEffect(() => {\n if (playground) {\n configureContentSession(null)\n return\n }\n let cancelled = false\n setPhase({ kind: 'loading' })\n void getStudioOperations()\n .then(({ operations }) => loadProjectSession(operations, projectId))\n .then((result) => {\n if (cancelled) return\n if (!result.ok) {\n setPhase({ kind: 'error', error: result.error })\n return\n }\n // Point the preview content engine at the hydrated documents before\n // any child renders (panels read it module-level, synchronously). EC-291:\n // include the config-sourced routes so the canvas `resolveLink` derives a\n // rich-text link's URL exactly as delivery does.\n configureContentSession(\n result.value.cms\n ? { ...result.value.cms, routes: resolveDeploymentRoutes() }\n : null,\n )\n setPhase({ kind: 'ready', session: result.value })\n })\n return () => {\n cancelled = true\n }\n }, [projectId, playground, attempt])\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n if (playground) return { ok: true, value: null }\n const { operations } = await getStudioOperations()\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null)\n configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() })\n return result\n }, [playground, projectId])\n\n if (phase.kind === 'loading') return <SessionHydrating />\n if (phase.kind === 'error') {\n return (\n <SessionHydrationError error={phase.error} onRetry={() => setAttempt((n) => n + 1)} />\n )\n }\n return (\n <ProjectSessionContext.Provider value={{ session: phase.session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )\n}\n\nexport function useProjectSession(): ProjectSessionStore {\n const store = useContext(ProjectSessionContext)\n if (!store) {\n throw new Error('useProjectSession must be used within a <ProjectSessionProvider>.')\n }\n return store\n}\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n const [phase, setPhase] = useState<\n | { kind: 'loading' }\n | { kind: 'ready'; cms: HydratedCms | null }\n | { kind: 'error'; error: OperationError }\n >(() => (playground ? { kind: 'ready', cms: null } : { kind: 'loading' }))\n const [attempt, setAttempt] = useState(0)\n\n useEffect(() => {\n if (playground) return\n let cancelled = false\n setPhase({ kind: 'loading' })\n void loadCms().then((result) => {\n if (cancelled) return\n setPhase(result.ok ? { kind: 'ready', cms: result.value } : { kind: 'error', error: result.error })\n })\n return () => {\n cancelled = true\n }\n }, [playground, loadCms, attempt])\n\n if (phase.kind === 'loading') return <SessionHydrating />\n if (phase.kind === 'error') {\n return (\n <SessionHydrationError error={phase.error} onRetry={() => setAttempt((n) => n + 1)} />\n )\n }\n return <>{children(phase.cms)}</>\n}\n\n/** Deterministic loading frame while persisted state is fetched (mirrors BackendGate). */\nfunction SessionHydrating() {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\" data-testid=\"session-hydrating\">\n <Text size=\"sm\" tone=\"muted\">\n Loading project…\n </Text>\n </div>\n )\n}\n\n/** A failed hydration is a visible, retryable state — never a fixture fallback. */\nfunction SessionHydrationError({\n error,\n onRetry,\n}: {\n error: OperationError\n onRetry: () => void\n}) {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\">\n <Stack gap=\"3\" className=\"w-full max-w-xl\">\n <Text size=\"sm\" weight=\"semibold\">\n Couldn’t load this project’s saved state\n </Text>\n <OperationErrorBanner error={error} />\n <Text size=\"xs\" tone=\"muted\">\n The studio never falls back to sample content against a live backend — your data is\n safe; retry once the backend is reachable.\n </Text>\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n Retry\n </Button>\n </Stack>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Stack, Text } from '@elytracms/studio/ui'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type {\n HydratedCms,\n HydratedProjectSession,\n OperationError,\n OperationResult,\n} from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { OperationErrorBanner } from './operation-error'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `BackendGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst ProjectSessionContext = createContext<ProjectSessionStore | null>(null)\n\ntype Phase =\n | { kind: 'loading' }\n | { kind: 'ready'; session: HydratedProjectSession | null }\n | { kind: 'error'; error: OperationError }\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n // Playground renders children on the FIRST render (no effect round-trip):\n // fixture boots stay synchronous and deterministic.\n const [phase, setPhase] = useState<Phase>(() =>\n playground ? { kind: 'ready', session: null } : { kind: 'loading' },\n )\n const [attempt, setAttempt] = useState(0)\n\n useEffect(() => {\n if (playground) {\n configureContentSession(null)\n return\n }\n let cancelled = false\n setPhase({ kind: 'loading' })\n void getStudioOperations()\n .then(({ operations }) => loadProjectSession(operations, projectId))\n .then((result) => {\n if (cancelled) return\n if (!result.ok) {\n setPhase({ kind: 'error', error: result.error })\n return\n }\n // Point the preview content engine at the hydrated documents before\n // any child renders (panels read it module-level, synchronously). EC-291:\n // include the config-sourced routes so the canvas `resolveLink` derives a\n // rich-text link's URL exactly as delivery does.\n configureContentSession(\n result.value.cms\n ? { ...result.value.cms, routes: resolveDeploymentRoutes() }\n : null,\n )\n setPhase({ kind: 'ready', session: result.value })\n })\n return () => {\n cancelled = true\n }\n }, [projectId, playground, attempt])\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n if (playground) return { ok: true, value: null }\n const { operations } = await getStudioOperations()\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null)\n configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() })\n return result\n }, [playground, projectId])\n\n if (phase.kind === 'loading') return <SessionHydrating />\n if (phase.kind === 'error') {\n return (\n <SessionHydrationError error={phase.error} onRetry={() => setAttempt((n) => n + 1)} />\n )\n }\n return (\n <ProjectSessionContext.Provider value={{ session: phase.session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )\n}\n\nexport function useProjectSession(): ProjectSessionStore {\n const store = useContext(ProjectSessionContext)\n if (!store) {\n throw new Error('useProjectSession must be used within a <ProjectSessionProvider>.')\n }\n return store\n}\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n const [phase, setPhase] = useState<\n | { kind: 'loading' }\n | { kind: 'ready'; cms: HydratedCms | null }\n | { kind: 'error'; error: OperationError }\n >(() => (playground ? { kind: 'ready', cms: null } : { kind: 'loading' }))\n const [attempt, setAttempt] = useState(0)\n\n useEffect(() => {\n if (playground) return\n let cancelled = false\n setPhase({ kind: 'loading' })\n void loadCms().then((result) => {\n if (cancelled) return\n setPhase(result.ok ? { kind: 'ready', cms: result.value } : { kind: 'error', error: result.error })\n })\n return () => {\n cancelled = true\n }\n }, [playground, loadCms, attempt])\n\n if (phase.kind === 'loading') return <SessionHydrating />\n if (phase.kind === 'error') {\n return (\n <SessionHydrationError error={phase.error} onRetry={() => setAttempt((n) => n + 1)} />\n )\n }\n return <>{children(phase.cms)}</>\n}\n\n/** Deterministic loading frame while persisted state is fetched (mirrors BackendGate). */\nfunction SessionHydrating() {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\" data-testid=\"session-hydrating\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.session.loading')}\n </Text>\n </div>\n )\n}\n\n/** A failed hydration is a visible, retryable state — never a fixture fallback. */\nfunction SessionHydrationError({\n error,\n onRetry,\n}: {\n error: OperationError\n onRetry: () => void\n}) {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\">\n <Stack gap=\"3\" className=\"w-full max-w-xl\">\n <Text size=\"sm\" weight=\"semibold\">\n {translate('shell.session.error.title')}\n </Text>\n <OperationErrorBanner error={error} />\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.session.error.body')}\n </Text>\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('common.retry')}\n </Button>\n </Stack>\n </div>\n )\n}\n"]}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
3
3
  import { Check, ChevronsUpDown, FolderOpen, LogIn } from 'lucide-react';
4
4
  import { hasProjectSession } from '@elytracms/studio/lib/auth';
5
+ import { translate } from '@elytracms/studio/lib/i18n';
5
6
  import { Badge, Button, Code, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
6
7
  import { useStudio } from './studio-context';
7
8
  import { useSwitcherEntries } from './workspace-projects';
@@ -26,19 +27,19 @@ export function ProjectSwitcher({ activeProject }) {
26
27
  studio.activate(project.id);
27
28
  navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } });
28
29
  }
29
- 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: "Recent" }), recents.map((p) => (_jsx(ProjectMenuItem, { project: p, active: false, onSelect: () => open(p) }, p.id))), _jsx(DropdownMenuSeparator, {})] })) : null, _jsx(DropdownMenuLabel, { children: "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, {}), " Browse all projects"] })] })] }));
30
+ 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')] })] })] }));
30
31
  }
31
32
  function ProjectMenuItem({ project, active, onSelect, }) {
32
33
  // Per-backend sign-in state (EC-179): the active project is signed-in by
33
34
  // definition; others wear a "Sign in" badge when their backend has no session.
34
35
  const needsSignIn = !active && !hasProjectSession(project.slug);
35
- return (_jsx(DropdownMenuItem, { onSelect: onSelect, className: "gap-2", children: _jsxs(Row, { gap: "2", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", children: project.name }), _jsx(Code, { tone: "muted", className: "shrink-0", children: project.slug }), _jsx(Spacer, {}), needsSignIn ? (_jsxs(Badge, { variant: "warning", children: [_jsx(LogIn, { className: "size-3" }), " Sign in"] })) : null, active ? _jsx(Check, { className: "size-4 text-primary" }) : null] }) }));
36
+ return (_jsx(DropdownMenuItem, { onSelect: onSelect, className: "gap-2", children: _jsxs(Row, { gap: "2", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", children: project.name }), _jsx(Code, { tone: "muted", className: "shrink-0", children: project.slug }), _jsx(Spacer, {}), needsSignIn ? (_jsxs(Badge, { variant: "warning", children: [_jsx(LogIn, { className: "size-3" }), " ", translate('auth.signIn')] })) : null, active ? _jsx(Check, { className: "size-4 text-primary" }) : null] }) }));
36
37
  }
37
38
  /**
38
39
  * A configured project with no backend project yet (EC-154): a clear,
39
40
  * non-navigating state pointing at the CLI provisioning flow — never an error.
40
41
  */
41
42
  function UnprovisionedMenuItem({ config }) {
42
- return (_jsx(DropdownMenuItem, { disabled: true, className: "gap-2 opacity-100", children: _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1 py-0.5", children: [_jsxs(Row, { gap: "2", className: "min-w-0", children: [_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: config.name }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: "not provisioned" })] }), _jsxs(Code, { tone: "muted", className: "w-fit max-w-full truncate text-[0.7rem]", children: ["builder init --project ", config.slug] })] }) }));
43
+ return (_jsx(DropdownMenuItem, { disabled: true, className: "gap-2 opacity-100", children: _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1 py-0.5", children: [_jsxs(Row, { gap: "2", className: "min-w-0", children: [_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: config.name }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsxs(Code, { tone: "muted", className: "w-fit max-w-full truncate text-[0.7rem]", children: ["builder init --project ", config.slug] })] }) }));
43
44
  }
44
45
  //# sourceMappingURL=project-switcher.js.map
@@ -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, LogIn } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\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 { useSwitcherEntries } from './workspace-projects'\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 = studio.context.recentProjectIds\n .map((id) => studio.getProject(id))\n .filter((p): p is Project => Boolean(p) && p!.id !== activeProject.id)\n .filter((p) =>\n activeEntries.some((entry) => entry.kind === 'project' && entry.project.id === p.id),\n )\n .slice(0, 4)\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>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>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 /> Browse all projects\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 ? (\n <Badge variant=\"warning\">\n <LogIn className=\"size-3\" /> Sign in\n </Badge>\n ) : 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\">not provisioned</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 { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen, LogIn } 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 { useSwitcherEntries } from './workspace-projects'\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 = studio.context.recentProjectIds\n .map((id) => studio.getProject(id))\n .filter((p): p is Project => Boolean(p) && p!.id !== activeProject.id)\n .filter((p) =>\n activeEntries.some((entry) => entry.kind === 'project' && entry.project.id === p.id),\n )\n .slice(0, 4)\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 ? (\n <Badge variant=\"warning\">\n <LogIn className=\"size-3\" /> {translate('auth.signIn')}\n </Badge>\n ) : 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,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { AlertTriangle, RotateCcw } from 'lucide-react';
3
3
  import { Button, Row, Stack, Text } from '@elytracms/studio/ui';
4
+ import { translate } from '@elytracms/studio/lib/i18n';
4
5
  import { RedirectsProvider, useRedirects } from './redirects-store';
5
6
  import { useRedirectsCollectionViewSource } from './redirects-collection-view-source';
6
7
  import { CollectionViewProvider } from '../collection-view/collection-view-context';
@@ -28,10 +29,10 @@ function RedirectsSurface({ projectId, redirectId, schema, }) {
28
29
  const store = useRedirects();
29
30
  const source = useRedirectsCollectionViewSource(projectId, schema);
30
31
  if (store.status === 'error') {
31
- return (_jsxs(Stack, { gap: "2", className: "p-6", "data-testid": "redirects-error", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { weight: "semibold", children: "Redirects failed to load" })] }), _jsx(Text, { size: "sm", tone: "muted", children: store.errorMessage ?? 'The persistence backend is unreachable.' }), _jsx(Row, { children: _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void store.refresh(), children: [_jsx(RotateCcw, {}), " Retry"] }) })] }));
32
+ return (_jsxs(Stack, { gap: "2", className: "p-6", "data-testid": "redirects-error", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { weight: "semibold", children: translate('redirects.loadFailed') })] }), _jsx(Text, { size: "sm", tone: "muted", children: store.errorMessage ?? translate('common.backendUnreachable') }), _jsx(Row, { children: _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void store.refresh(), children: [_jsx(RotateCcw, {}), " ", translate('common.retry')] }) })] }));
32
33
  }
33
34
  if (store.status === 'loading') {
34
- return (_jsx(Stack, { className: "h-full place-items-center p-8", align: "center", justify: "center", children: _jsx(Text, { size: "sm", tone: "muted", children: "Loading redirects\u2026" }) }));
35
+ return (_jsx(Stack, { className: "h-full place-items-center p-8", align: "center", justify: "center", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('redirects.loading') }) }));
35
36
  }
36
37
  // One redirect's detail opens in the portable edit shell (AD-19); the list otherwise.
37
38
  // The shell mounts its own CollectionViewProvider, so only the list wraps here.
@@ -1 +1 @@
1
- {"version":3,"file":"redirects-module.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-module.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEvD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAA;AACrF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAE3D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,UAAU,GAAG,IAAI,EACjB,MAAM,GAMP;IACC,OAAO,CACL,KAAC,iBAAiB,IAAC,SAAS,EAAE,SAAS,YACrC,KAAC,gBAAgB,IAAC,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAI,GAChE,CACrB,CAAA;AACH,CAAC;AAED,wFAAwF;AACxF,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,UAAU,EACV,MAAM,GAKP;IACC,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,gCAAgC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAElE,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,iBAAa,iBAAiB,aAC1D,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,KAAC,aAAa,IAAC,SAAS,EAAC,yBAAyB,GAAG,EACrD,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,yCAAgC,IACnD,EACN,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,KAAK,CAAC,YAAY,IAAI,yCAAyC,GAC3D,EACP,KAAC,GAAG,cACF,MAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,aACrE,KAAC,SAAS,KAAG,cACN,GACL,IACA,CACT,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,CACL,KAAC,KAAK,IAAC,SAAS,EAAC,+BAA+B,EAAC,KAAK,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,YAC9E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,wCAErB,GACD,CACT,CAAA;IACH,CAAC;IAED,sFAAsF;IACtF,gFAAgF;IAChF,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,KAAC,oBAAoB,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,GAAI,CAAA;IAC/F,CAAC;IAED,OAAO,CACL,KAAC,sBAAsB,IAAC,MAAM,EAAE,MAAM,YACpC,KAAC,qBAAqB,IAAC,SAAS,EAAE,SAAS,GAAI,GACxB,CAC1B,CAAA;AACH,CAAC","sourcesContent":["import { AlertTriangle, RotateCcw } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { RedirectsProvider, useRedirects } from './redirects-store'\nimport { useRedirectsCollectionViewSource } from './redirects-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { LoadedCollectionTable } from '../cms/collection-table'\nimport { PortableDetailEditor } from '../cms/detail-editor'\n\n/**\n * The studio Redirects module (EC-303). Editorial redirects as data — the reuse\n * proof (with the users rail) that the portable interaction surfaces serve any\n * rail: it renders the SAME `LoadedCollectionTable` for the list and the SAME\n * `DetailEditor` body (via {@link PortableDetailEditor}) for one redirect, over a\n * redirects {@link useRedirectsCollectionViewSource}. The redirects capability\n * profile (content-gated, create + remove, no publish/versions/locales) drives\n * which regions appear; the edit shell's explicit-save path (no publish) makes a\n * saved+enabled redirect live.\n *\n * `redirectId === null` is the list; a `redirectId` opens that redirect's detail\n * (routed via `studioPath.redirect`). No admin gate — redirects are content, so\n * viewers read and editors write (enforced at the chokepoint + Convex boundary).\n */\nexport function RedirectsModule({\n projectId,\n redirectId = null,\n schema,\n}: {\n projectId: string\n redirectId?: string | null\n /** EC-311: a mount-customized redirects def (built-in fields overridden by name). */\n schema?: CollectionDef\n}) {\n return (\n <RedirectsProvider projectId={projectId}>\n <RedirectsSurface projectId={projectId} redirectId={redirectId} schema={schema} />\n </RedirectsProvider>\n )\n}\n\n/** The loaded surface: the portable list (or one redirect's detail) over the source. */\nfunction RedirectsSurface({\n projectId,\n redirectId,\n schema,\n}: {\n projectId: string\n redirectId: string | null\n schema?: CollectionDef\n}) {\n const store = useRedirects()\n const source = useRedirectsCollectionViewSource(projectId, schema)\n\n if (store.status === 'error') {\n return (\n <Stack gap=\"2\" className=\"p-6\" data-testid=\"redirects-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text weight=\"semibold\">Redirects failed to load</Text>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? 'The persistence backend is unreachable.'}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> Retry\n </Button>\n </Row>\n </Stack>\n )\n }\n\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n Loading redirects…\n </Text>\n </Stack>\n )\n }\n\n // One redirect's detail opens in the portable edit shell (AD-19); the list otherwise.\n // The shell mounts its own CollectionViewProvider, so only the list wraps here.\n if (redirectId !== null) {\n return <PortableDetailEditor source={source} projectId={projectId} documentId={redirectId} />\n }\n\n return (\n <CollectionViewProvider source={source}>\n <LoadedCollectionTable projectId={projectId} />\n </CollectionViewProvider>\n )\n}\n"]}
1
+ {"version":3,"file":"redirects-module.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-module.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEvD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAA;AACrF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAE3D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,UAAU,GAAG,IAAI,EACjB,MAAM,GAMP;IACC,OAAO,CACL,KAAC,iBAAiB,IAAC,SAAS,EAAE,SAAS,YACrC,KAAC,gBAAgB,IAAC,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAI,GAChE,CACrB,CAAA;AACH,CAAC;AAED,wFAAwF;AACxF,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,UAAU,EACV,MAAM,GAKP;IACC,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,gCAAgC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAElE,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,iBAAa,iBAAiB,aAC1D,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,KAAC,aAAa,IAAC,SAAS,EAAC,yBAAyB,GAAG,EACrD,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,YAAE,SAAS,CAAC,sBAAsB,CAAC,GAAQ,IAC9D,EACN,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,KAAK,CAAC,YAAY,IAAI,SAAS,CAAC,2BAA2B,CAAC,GACxD,EACP,KAAC,GAAG,cACF,MAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,aACrE,KAAC,SAAS,KAAG,OAAE,SAAS,CAAC,cAAc,CAAC,IACjC,GACL,IACA,CACT,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,CACL,KAAC,KAAK,IAAC,SAAS,EAAC,+BAA+B,EAAC,KAAK,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,YAC9E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,mBAAmB,CAAC,GAC1B,GACD,CACT,CAAA;IACH,CAAC;IAED,sFAAsF;IACtF,gFAAgF;IAChF,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,KAAC,oBAAoB,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,GAAI,CAAA;IAC/F,CAAC;IAED,OAAO,CACL,KAAC,sBAAsB,IAAC,MAAM,EAAE,MAAM,YACpC,KAAC,qBAAqB,IAAC,SAAS,EAAE,SAAS,GAAI,GACxB,CAC1B,CAAA;AACH,CAAC","sourcesContent":["import { AlertTriangle, RotateCcw } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { RedirectsProvider, useRedirects } from './redirects-store'\nimport { useRedirectsCollectionViewSource } from './redirects-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { LoadedCollectionTable } from '../cms/collection-table'\nimport { PortableDetailEditor } from '../cms/detail-editor'\n\n/**\n * The studio Redirects module (EC-303). Editorial redirects as data — the reuse\n * proof (with the users rail) that the portable interaction surfaces serve any\n * rail: it renders the SAME `LoadedCollectionTable` for the list and the SAME\n * `DetailEditor` body (via {@link PortableDetailEditor}) for one redirect, over a\n * redirects {@link useRedirectsCollectionViewSource}. The redirects capability\n * profile (content-gated, create + remove, no publish/versions/locales) drives\n * which regions appear; the edit shell's explicit-save path (no publish) makes a\n * saved+enabled redirect live.\n *\n * `redirectId === null` is the list; a `redirectId` opens that redirect's detail\n * (routed via `studioPath.redirect`). No admin gate — redirects are content, so\n * viewers read and editors write (enforced at the chokepoint + Convex boundary).\n */\nexport function RedirectsModule({\n projectId,\n redirectId = null,\n schema,\n}: {\n projectId: string\n redirectId?: string | null\n /** EC-311: a mount-customized redirects def (built-in fields overridden by name). */\n schema?: CollectionDef\n}) {\n return (\n <RedirectsProvider projectId={projectId}>\n <RedirectsSurface projectId={projectId} redirectId={redirectId} schema={schema} />\n </RedirectsProvider>\n )\n}\n\n/** The loaded surface: the portable list (or one redirect's detail) over the source. */\nfunction RedirectsSurface({\n projectId,\n redirectId,\n schema,\n}: {\n projectId: string\n redirectId: string | null\n schema?: CollectionDef\n}) {\n const store = useRedirects()\n const source = useRedirectsCollectionViewSource(projectId, schema)\n\n if (store.status === 'error') {\n return (\n <Stack gap=\"2\" className=\"p-6\" data-testid=\"redirects-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text weight=\"semibold\">{translate('redirects.loadFailed')}</Text>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('common.backendUnreachable')}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> {translate('common.retry')}\n </Button>\n </Row>\n </Stack>\n )\n }\n\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('redirects.loading')}\n </Text>\n </Stack>\n )\n }\n\n // One redirect's detail opens in the portable edit shell (AD-19); the list otherwise.\n // The shell mounts its own CollectionViewProvider, so only the list wraps here.\n if (redirectId !== null) {\n return <PortableDetailEditor source={source} projectId={projectId} documentId={redirectId} />\n }\n\n return (\n <CollectionViewProvider source={source}>\n <LoadedCollectionTable projectId={projectId} />\n </CollectionViewProvider>\n )\n}\n"]}
@@ -3,6 +3,7 @@ import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router
3
3
  import { AlertTriangle, ArrowDownLeft, ArrowUpRight, Link2, RotateCcw } from 'lucide-react';
4
4
  import { useReferencePanel } from '@elytracms/studio/lib/references';
5
5
  import { Badge, Button, Panel, PanelHeader, Row, RowItem, Spacer, Stack, Text } from '@elytracms/studio/ui';
6
+ import { translate } from '@elytracms/studio/lib/i18n';
6
7
  /**
7
8
  * The shared "References / Used by" panel (EC-172) — one generic component,
8
9
  * three hosts (CMS document detail, asset library, component detail). Entries
@@ -14,18 +15,18 @@ import { Badge, Button, Panel, PanelHeader, Row, RowItem, Spacer, Stack, Text }
14
15
  export function ReferencesPanel({ projectId, source, target, className, }) {
15
16
  const panel = useReferencePanel({ projectId, source, target });
16
17
  const total = panel.data === null ? null : panel.data.outbound.length + panel.data.inbound.length;
17
- return (_jsxs(Panel, { className: className, "data-testid": "references-panel", children: [_jsxs(PanelHeader, { children: [_jsx(Link2, { className: "size-4 text-primary" }), _jsx(Text, { size: "sm", weight: "semibold", children: "References" }), total !== null ? _jsx(Badge, { variant: "outline", children: total }) : null, _jsx(Spacer, {})] }), _jsx(Stack, { gap: "4", className: "p-4", children: panel.status === 'loading' ? (_jsx(ReferencesLoading, {})) : panel.status === 'error' ? (_jsx(ReferencesError, { message: panel.errorMessage, onRetry: panel.refresh })) : (_jsxs(_Fragment, { children: [source ? (_jsx(ReferenceSection, { title: "References", icon: _jsx(ArrowUpRight, { className: "size-3.5" }), hint: "What this entry points at \u2014 click to jump to the target, or to the exact field.", direction: "outbound", projectId: projectId, entries: panel.data?.outbound ?? [], error: panel.data?.outboundError ?? null, emptyLabel: "No outbound references." })) : null, target ? (_jsx(ReferenceSection, { title: "Used by", icon: _jsx(ArrowDownLeft, { className: "size-3.5" }), hint: "Everything referencing this entry \u2014 click to jump to the exact field or node.", direction: "inbound", projectId: projectId, entries: panel.data?.inbound ?? [], error: panel.data?.inboundError ?? null, emptyLabel: "Not referenced anywhere." })) : null] })) })] }));
18
+ return (_jsxs(Panel, { className: className, "data-testid": "references-panel", children: [_jsxs(PanelHeader, { children: [_jsx(Link2, { className: "size-4 text-primary" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate('references.title') }), total !== null ? _jsx(Badge, { variant: "outline", children: total }) : null, _jsx(Spacer, {})] }), _jsx(Stack, { gap: "4", className: "p-4", children: panel.status === 'loading' ? (_jsx(ReferencesLoading, {})) : panel.status === 'error' ? (_jsx(ReferencesError, { message: panel.errorMessage, onRetry: panel.refresh })) : (_jsxs(_Fragment, { children: [source ? (_jsx(ReferenceSection, { title: translate('references.title'), icon: _jsx(ArrowUpRight, { className: "size-3.5" }), hint: translate('references.hintOutbound'), direction: "outbound", projectId: projectId, entries: panel.data?.outbound ?? [], error: panel.data?.outboundError ?? null, emptyLabel: translate('references.emptyOutbound') })) : null, target ? (_jsx(ReferenceSection, { title: translate('references.usedBy'), icon: _jsx(ArrowDownLeft, { className: "size-3.5" }), hint: translate('references.hintInbound'), direction: "inbound", projectId: projectId, entries: panel.data?.inbound ?? [], error: panel.data?.inboundError ?? null, emptyLabel: translate('references.emptyInbound') })) : null] })) })] }));
18
19
  }
19
20
  /** Named loading state, consistent with existing studio panels. */
20
21
  function ReferencesLoading() {
21
- return (_jsx(Text, { size: "sm", tone: "muted", "data-testid": "references-loading", children: "Loading references\u2026" }));
22
+ return (_jsx(Text, { size: "sm", tone: "muted", "data-testid": "references-loading", children: translate('references.loading') }));
22
23
  }
23
24
  /** Named error state with retry — never a silent failure. */
24
25
  function ReferencesError({ message, onRetry, }) {
25
- return (_jsxs(Stack, { gap: "2", "data-testid": "references-error", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { size: "sm", weight: "semibold", children: "References failed to load" })] }), _jsx(Text, { size: "xs", tone: "muted", children: message ?? 'The reference index is unreachable.' }), _jsx(Row, { children: _jsxs(Button, { variant: "outline", size: "sm", onClick: onRetry, children: [_jsx(RotateCcw, {}), " Retry"] }) })] }));
26
+ return (_jsxs(Stack, { gap: "2", "data-testid": "references-error", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate('references.loadFailed') })] }), _jsx(Text, { size: "xs", tone: "muted", children: message ?? translate('references.indexUnreachable') }), _jsx(Row, { children: _jsxs(Button, { variant: "outline", size: "sm", onClick: onRetry, children: [_jsx(RotateCcw, {}), " ", translate('common.retry')] }) })] }));
26
27
  }
27
28
  function ReferenceSection({ title, icon, hint, direction, projectId, entries, error, emptyLabel, }) {
28
- return (_jsxs(Stack, { gap: "2", "data-testid": `references-${direction}`, children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "text-muted-foreground", children: icon }), _jsx(Text, { size: "xs", weight: "semibold", tone: "muted", className: "uppercase tracking-wide", children: title }), _jsx(Badge, { variant: "outline", children: entries.length })] }), error ? (_jsxs(Row, { gap: "2", "data-testid": `references-${direction}-error`, children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0 text-destructive" }), _jsxs(Text, { size: "xs", tone: "muted", children: ["Couldn\u2019t load this section: ", error] })] })) : entries.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", "data-testid": `references-${direction}-empty`, children: emptyLabel })) : (_jsx(Stack, { gap: "2", title: hint, children: entries.map((entry) => (_jsx(ReferenceEntryRow, { entry: entry, direction: direction, projectId: projectId }, entry.key))) }))] }));
29
+ return (_jsxs(Stack, { gap: "2", "data-testid": `references-${direction}`, children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "text-muted-foreground", children: icon }), _jsx(Text, { size: "xs", weight: "semibold", tone: "muted", className: "uppercase tracking-wide", children: title }), _jsx(Badge, { variant: "outline", children: entries.length })] }), error ? (_jsxs(Row, { gap: "2", "data-testid": `references-${direction}-error`, children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0 text-destructive" }), _jsx(Text, { size: "xs", tone: "muted", children: translate('references.sectionLoadError', { error }) })] })) : entries.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", "data-testid": `references-${direction}-empty`, children: emptyLabel })) : (_jsx(Stack, { gap: "2", title: hint, children: entries.map((entry) => (_jsx(ReferenceEntryRow, { entry: entry, direction: direction, projectId: projectId }, entry.key))) }))] }));
29
30
  }
30
31
  /**
31
32
  * One entry row. Inbound rows lead with the *source* (where it is used) and
@@ -36,11 +37,11 @@ function ReferenceEntryRow({ entry, direction, projectId, }) {
36
37
  const primary = direction === 'inbound'
37
38
  ? { label: entry.sourceLabel, link: entry.sourceLink }
38
39
  : { label: entry.targetLabel, link: entry.targetLink };
39
- return (_jsx(RowItem, { "data-testid": "reference-entry", actions: _jsxs(_Fragment, { children: [_jsx(Badge, { variant: "outline", children: entry.kindLabel }), _jsx(VariantBadges, { draft: entry.draft, published: entry.published })] }), children: _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Row, { gap: "2", className: "min-w-0", children: _jsx(DeepLinkText, { projectId: projectId, link: primary.link, label: primary.label, primary: true }) }), _jsxs(Row, { gap: "1", className: "min-w-0", children: [_jsx(Text, { size: "xs", tone: "muted", className: "shrink-0", children: "at" }), _jsx(DeepLinkText, { projectId: projectId, link: entry.sourceLink, label: entry.locationLabel })] })] }) }));
40
+ return (_jsx(RowItem, { "data-testid": "reference-entry", actions: _jsxs(_Fragment, { children: [_jsx(Badge, { variant: "outline", children: entry.kindLabel }), _jsx(VariantBadges, { draft: entry.draft, published: entry.published })] }), children: _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Row, { gap: "2", className: "min-w-0", children: _jsx(DeepLinkText, { projectId: projectId, link: primary.link, label: primary.label, primary: true }) }), _jsxs(Row, { gap: "1", className: "min-w-0", children: [_jsx(Text, { size: "xs", tone: "muted", className: "shrink-0", children: translate('references.locationPrefix') }), _jsx(DeepLinkText, { projectId: projectId, link: entry.sourceLink, label: entry.locationLabel })] })] }) }));
40
41
  }
41
42
  /** Draft/published visibility (the Sanity hidden-draft-reference case, AD-6). */
42
43
  function VariantBadges({ draft, published }) {
43
- return (_jsxs(Row, { gap: "1", className: "shrink-0", "data-testid": "reference-variants", children: [draft ? _jsx(Badge, { variant: "warning", children: "draft" }) : null, published ? _jsx(Badge, { variant: "success", children: "published" }) : null] }));
44
+ return (_jsxs(Row, { gap: "1", className: "shrink-0", "data-testid": "reference-variants", children: [draft ? _jsx(Badge, { variant: "warning", children: translate('references.badgeDraft') }) : null, published ? _jsx(Badge, { variant: "success", children: translate('references.badgePublished') }) : null] }));
44
45
  }
45
46
  /** Render a deep-link descriptor as a router link (plain text when unresolvable). */
46
47
  function DeepLinkText({ projectId, link, label, primary = false, }) {
@@ -1 +1 @@
1
- {"version":3,"file":"references-panel.js","sourceRoot":"","sources":["../../../src/components/references/references-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { AlertTriangle, ArrowDownLeft, ArrowUpRight, Link2, RotateCcw } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { ReferenceSource, ReferenceTarget } from '@elytracms/core/persistence'\nimport { useReferencePanel } from '@elytracms/studio/lib/references'\nimport type { ReferenceDeepLink, ReferenceEntryView } from '@elytracms/studio/lib/references'\nimport { Badge, Button, Panel, PanelHeader, Row, RowItem, Spacer, Stack, Text } from '@elytracms/studio/ui'\n\n/**\n * The shared \"References / Used by\" panel (EC-172) — one generic component,\n * three hosts (CMS document detail, asset library, component detail). Entries\n * are served entirely by `@elytracms/core/operations` reference queries over the\n * EC-155 index; every entry deep-links to the exact field control or graph\n * node its index path identifies, and draft-only entries are visibly\n * distinguished from published (live) ones.\n */\nexport function ReferencesPanel({\n projectId,\n source,\n target,\n className,\n}: {\n projectId: string\n /** The holding entity — enables the outbound \"References\" section. */\n source?: ReferenceSource\n /** The referenced entity — enables the inbound \"Used by\" section. */\n target?: ReferenceTarget\n className?: string\n}) {\n const panel = useReferencePanel({ projectId, source, target })\n const total =\n panel.data === null ? null : panel.data.outbound.length + panel.data.inbound.length\n\n return (\n <Panel className={className} data-testid=\"references-panel\">\n <PanelHeader>\n <Link2 className=\"size-4 text-primary\" />\n <Text size=\"sm\" weight=\"semibold\">\n References\n </Text>\n {total !== null ? <Badge variant=\"outline\">{total}</Badge> : null}\n <Spacer />\n </PanelHeader>\n <Stack gap=\"4\" className=\"p-4\">\n {panel.status === 'loading' ? (\n <ReferencesLoading />\n ) : panel.status === 'error' ? (\n <ReferencesError message={panel.errorMessage} onRetry={panel.refresh} />\n ) : (\n <>\n {source ? (\n <ReferenceSection\n title=\"References\"\n icon={<ArrowUpRight className=\"size-3.5\" />}\n hint=\"What this entry points at — click to jump to the target, or to the exact field.\"\n direction=\"outbound\"\n projectId={projectId}\n entries={panel.data?.outbound ?? []}\n error={panel.data?.outboundError ?? null}\n emptyLabel=\"No outbound references.\"\n />\n ) : null}\n {target ? (\n <ReferenceSection\n title=\"Used by\"\n icon={<ArrowDownLeft className=\"size-3.5\" />}\n hint=\"Everything referencing this entry — click to jump to the exact field or node.\"\n direction=\"inbound\"\n projectId={projectId}\n entries={panel.data?.inbound ?? []}\n error={panel.data?.inboundError ?? null}\n emptyLabel=\"Not referenced anywhere.\"\n />\n ) : null}\n </>\n )}\n </Stack>\n </Panel>\n )\n}\n\n/** Named loading state, consistent with existing studio panels. */\nfunction ReferencesLoading() {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"references-loading\">\n Loading references…\n </Text>\n )\n}\n\n/** Named error state with retry — never a silent failure. */\nfunction ReferencesError({\n message,\n onRetry,\n}: {\n message: string | null\n onRetry: () => void\n}) {\n return (\n <Stack gap=\"2\" data-testid=\"references-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" weight=\"semibold\">\n References failed to load\n </Text>\n </Row>\n <Text size=\"xs\" tone=\"muted\">\n {message ?? 'The reference index is unreachable.'}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n <RotateCcw /> Retry\n </Button>\n </Row>\n </Stack>\n )\n}\n\nfunction ReferenceSection({\n title,\n icon,\n hint,\n direction,\n projectId,\n entries,\n error,\n emptyLabel,\n}: {\n title: string\n icon: ReactNode\n hint: string\n direction: 'outbound' | 'inbound'\n projectId: string\n entries: readonly ReferenceEntryView[]\n /** EC-205: this direction's read failed — shown inline, the panel stays usable. */\n error?: string | null\n emptyLabel: string\n}) {\n return (\n <Stack gap=\"2\" data-testid={`references-${direction}`}>\n <Row gap=\"2\">\n <span className=\"text-muted-foreground\">{icon}</span>\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {title}\n </Text>\n <Badge variant=\"outline\">{entries.length}</Badge>\n </Row>\n {error ? (\n <Row gap=\"2\" data-testid={`references-${direction}-error`}>\n <AlertTriangle className=\"size-3.5 shrink-0 text-destructive\" />\n <Text size=\"xs\" tone=\"muted\">\n Couldn’t load this section: {error}\n </Text>\n </Row>\n ) : entries.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" data-testid={`references-${direction}-empty`}>\n {emptyLabel}\n </Text>\n ) : (\n <Stack gap=\"2\" title={hint}>\n {entries.map((entry) => (\n <ReferenceEntryRow\n key={entry.key}\n entry={entry}\n direction={direction}\n projectId={projectId}\n />\n ))}\n </Stack>\n )}\n </Stack>\n )\n}\n\n/**\n * One entry row. Inbound rows lead with the *source* (where it is used) and\n * deep-link to the exact location; outbound rows lead with the *target* and\n * keep the exact-field link on the location line.\n */\nfunction ReferenceEntryRow({\n entry,\n direction,\n projectId,\n}: {\n entry: ReferenceEntryView\n direction: 'outbound' | 'inbound'\n projectId: string\n}) {\n const primary =\n direction === 'inbound'\n ? { label: entry.sourceLabel, link: entry.sourceLink }\n : { label: entry.targetLabel, link: entry.targetLink }\n return (\n <RowItem\n data-testid=\"reference-entry\"\n actions={\n <>\n <Badge variant=\"outline\">{entry.kindLabel}</Badge>\n <VariantBadges draft={entry.draft} published={entry.published} />\n </>\n }\n >\n <Stack gap=\"1\" className=\"min-w-0\">\n <Row gap=\"2\" className=\"min-w-0\">\n <DeepLinkText projectId={projectId} link={primary.link} label={primary.label} primary />\n </Row>\n <Row gap=\"1\" className=\"min-w-0\">\n <Text size=\"xs\" tone=\"muted\" className=\"shrink-0\">\n at\n </Text>\n <DeepLinkText\n projectId={projectId}\n link={entry.sourceLink}\n label={entry.locationLabel}\n />\n </Row>\n </Stack>\n </RowItem>\n )\n}\n\n/** Draft/published visibility (the Sanity hidden-draft-reference case, AD-6). */\nfunction VariantBadges({ draft, published }: { draft: boolean; published: boolean }) {\n return (\n <Row gap=\"1\" className=\"shrink-0\" data-testid=\"reference-variants\">\n {draft ? <Badge variant=\"warning\">draft</Badge> : null}\n {published ? <Badge variant=\"success\">published</Badge> : null}\n </Row>\n )\n}\n\n/** Render a deep-link descriptor as a router link (plain text when unresolvable). */\nfunction DeepLinkText({\n projectId,\n link,\n label,\n primary = false,\n}: {\n projectId: string\n link: ReferenceDeepLink | null\n label: string\n primary?: boolean\n}) {\n const className = primary\n ? 'min-w-0 truncate text-sm font-medium text-foreground hover:text-primary hover:underline'\n : 'min-w-0 truncate text-xs text-muted-foreground hover:text-primary hover:underline'\n if (link === null) {\n return primary ? (\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n )\n }\n switch (link.kind) {\n case 'cms-field':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsDocument(link.collectionId, link.documentId) }}\n search={link.fieldPath !== undefined ? { field: link.fieldPath } : {}}\n className={className}\n data-testid=\"reference-link-cms\"\n >\n {label}\n </Link>\n )\n case 'builder-node':\n // The builder redirect (EC-187) only reads `?page=`; the page-graph-only\n // `node` has no home in the document editor and is dropped.\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.builder() }}\n search={link.pageId !== undefined ? { page: link.pageId } : {}}\n className={className}\n data-testid=\"reference-link-builder\"\n >\n {label}\n </Link>\n )\n case 'component':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.component(link.componentId) }}\n className={className}\n data-testid=\"reference-link-component\"\n >\n {label}\n </Link>\n )\n case 'asset':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.assets() }}\n search={{ asset: link.assetId }}\n className={className}\n data-testid=\"reference-link-asset\"\n >\n {label}\n </Link>\n )\n }\n}\n"]}
1
+ {"version":3,"file":"references-panel.js","sourceRoot":"","sources":["../../../src/components/references/references-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { AlertTriangle, ArrowDownLeft, ArrowUpRight, Link2, RotateCcw } from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport type { ReferenceSource, ReferenceTarget } from '@elytracms/core/persistence'\nimport { useReferencePanel } from '@elytracms/studio/lib/references'\nimport type { ReferenceDeepLink, ReferenceEntryView } from '@elytracms/studio/lib/references'\nimport { Badge, Button, Panel, PanelHeader, Row, RowItem, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * The shared \"References / Used by\" panel (EC-172) — one generic component,\n * three hosts (CMS document detail, asset library, component detail). Entries\n * are served entirely by `@elytracms/core/operations` reference queries over the\n * EC-155 index; every entry deep-links to the exact field control or graph\n * node its index path identifies, and draft-only entries are visibly\n * distinguished from published (live) ones.\n */\nexport function ReferencesPanel({\n projectId,\n source,\n target,\n className,\n}: {\n projectId: string\n /** The holding entity — enables the outbound \"References\" section. */\n source?: ReferenceSource\n /** The referenced entity — enables the inbound \"Used by\" section. */\n target?: ReferenceTarget\n className?: string\n}) {\n const panel = useReferencePanel({ projectId, source, target })\n const total =\n panel.data === null ? null : panel.data.outbound.length + panel.data.inbound.length\n\n return (\n <Panel className={className} data-testid=\"references-panel\">\n <PanelHeader>\n <Link2 className=\"size-4 text-primary\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate('references.title')}\n </Text>\n {total !== null ? <Badge variant=\"outline\">{total}</Badge> : null}\n <Spacer />\n </PanelHeader>\n <Stack gap=\"4\" className=\"p-4\">\n {panel.status === 'loading' ? (\n <ReferencesLoading />\n ) : panel.status === 'error' ? (\n <ReferencesError message={panel.errorMessage} onRetry={panel.refresh} />\n ) : (\n <>\n {source ? (\n <ReferenceSection\n title={translate('references.title')}\n icon={<ArrowUpRight className=\"size-3.5\" />}\n hint={translate('references.hintOutbound')}\n direction=\"outbound\"\n projectId={projectId}\n entries={panel.data?.outbound ?? []}\n error={panel.data?.outboundError ?? null}\n emptyLabel={translate('references.emptyOutbound')}\n />\n ) : null}\n {target ? (\n <ReferenceSection\n title={translate('references.usedBy')}\n icon={<ArrowDownLeft className=\"size-3.5\" />}\n hint={translate('references.hintInbound')}\n direction=\"inbound\"\n projectId={projectId}\n entries={panel.data?.inbound ?? []}\n error={panel.data?.inboundError ?? null}\n emptyLabel={translate('references.emptyInbound')}\n />\n ) : null}\n </>\n )}\n </Stack>\n </Panel>\n )\n}\n\n/** Named loading state, consistent with existing studio panels. */\nfunction ReferencesLoading() {\n return (\n <Text size=\"sm\" tone=\"muted\" data-testid=\"references-loading\">\n {translate('references.loading')}\n </Text>\n )\n}\n\n/** Named error state with retry — never a silent failure. */\nfunction ReferencesError({\n message,\n onRetry,\n}: {\n message: string | null\n onRetry: () => void\n}) {\n return (\n <Stack gap=\"2\" data-testid=\"references-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate('references.loadFailed')}\n </Text>\n </Row>\n <Text size=\"xs\" tone=\"muted\">\n {message ?? translate('references.indexUnreachable')}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n <RotateCcw /> {translate('common.retry')}\n </Button>\n </Row>\n </Stack>\n )\n}\n\nfunction ReferenceSection({\n title,\n icon,\n hint,\n direction,\n projectId,\n entries,\n error,\n emptyLabel,\n}: {\n title: string\n icon: ReactNode\n hint: string\n direction: 'outbound' | 'inbound'\n projectId: string\n entries: readonly ReferenceEntryView[]\n /** EC-205: this direction's read failed — shown inline, the panel stays usable. */\n error?: string | null\n emptyLabel: string\n}) {\n return (\n <Stack gap=\"2\" data-testid={`references-${direction}`}>\n <Row gap=\"2\">\n <span className=\"text-muted-foreground\">{icon}</span>\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {title}\n </Text>\n <Badge variant=\"outline\">{entries.length}</Badge>\n </Row>\n {error ? (\n <Row gap=\"2\" data-testid={`references-${direction}-error`}>\n <AlertTriangle className=\"size-3.5 shrink-0 text-destructive\" />\n <Text size=\"xs\" tone=\"muted\">\n {translate('references.sectionLoadError', { error })}\n </Text>\n </Row>\n ) : entries.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" data-testid={`references-${direction}-empty`}>\n {emptyLabel}\n </Text>\n ) : (\n <Stack gap=\"2\" title={hint}>\n {entries.map((entry) => (\n <ReferenceEntryRow\n key={entry.key}\n entry={entry}\n direction={direction}\n projectId={projectId}\n />\n ))}\n </Stack>\n )}\n </Stack>\n )\n}\n\n/**\n * One entry row. Inbound rows lead with the *source* (where it is used) and\n * deep-link to the exact location; outbound rows lead with the *target* and\n * keep the exact-field link on the location line.\n */\nfunction ReferenceEntryRow({\n entry,\n direction,\n projectId,\n}: {\n entry: ReferenceEntryView\n direction: 'outbound' | 'inbound'\n projectId: string\n}) {\n const primary =\n direction === 'inbound'\n ? { label: entry.sourceLabel, link: entry.sourceLink }\n : { label: entry.targetLabel, link: entry.targetLink }\n return (\n <RowItem\n data-testid=\"reference-entry\"\n actions={\n <>\n <Badge variant=\"outline\">{entry.kindLabel}</Badge>\n <VariantBadges draft={entry.draft} published={entry.published} />\n </>\n }\n >\n <Stack gap=\"1\" className=\"min-w-0\">\n <Row gap=\"2\" className=\"min-w-0\">\n <DeepLinkText projectId={projectId} link={primary.link} label={primary.label} primary />\n </Row>\n <Row gap=\"1\" className=\"min-w-0\">\n <Text size=\"xs\" tone=\"muted\" className=\"shrink-0\">\n {translate('references.locationPrefix')}\n </Text>\n <DeepLinkText\n projectId={projectId}\n link={entry.sourceLink}\n label={entry.locationLabel}\n />\n </Row>\n </Stack>\n </RowItem>\n )\n}\n\n/** Draft/published visibility (the Sanity hidden-draft-reference case, AD-6). */\nfunction VariantBadges({ draft, published }: { draft: boolean; published: boolean }) {\n return (\n <Row gap=\"1\" className=\"shrink-0\" data-testid=\"reference-variants\">\n {draft ? <Badge variant=\"warning\">{translate('references.badgeDraft')}</Badge> : null}\n {published ? <Badge variant=\"success\">{translate('references.badgePublished')}</Badge> : null}\n </Row>\n )\n}\n\n/** Render a deep-link descriptor as a router link (plain text when unresolvable). */\nfunction DeepLinkText({\n projectId,\n link,\n label,\n primary = false,\n}: {\n projectId: string\n link: ReferenceDeepLink | null\n label: string\n primary?: boolean\n}) {\n const className = primary\n ? 'min-w-0 truncate text-sm font-medium text-foreground hover:text-primary hover:underline'\n : 'min-w-0 truncate text-xs text-muted-foreground hover:text-primary hover:underline'\n if (link === null) {\n return primary ? (\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n )\n }\n switch (link.kind) {\n case 'cms-field':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsDocument(link.collectionId, link.documentId) }}\n search={link.fieldPath !== undefined ? { field: link.fieldPath } : {}}\n className={className}\n data-testid=\"reference-link-cms\"\n >\n {label}\n </Link>\n )\n case 'builder-node':\n // The builder redirect (EC-187) only reads `?page=`; the page-graph-only\n // `node` has no home in the document editor and is dropped.\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.builder() }}\n search={link.pageId !== undefined ? { page: link.pageId } : {}}\n className={className}\n data-testid=\"reference-link-builder\"\n >\n {label}\n </Link>\n )\n case 'component':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.component(link.componentId) }}\n className={className}\n data-testid=\"reference-link-component\"\n >\n {label}\n </Link>\n )\n case 'asset':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.assets() }}\n search={{ asset: link.assetId }}\n className={className}\n data-testid=\"reference-link-asset\"\n >\n {label}\n </Link>\n )\n }\n}\n"]}