@elytracms/studio 0.0.15 → 0.0.17

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 (69) hide show
  1. package/dist/components/app-views/default-modules.d.ts +41 -0
  2. package/dist/components/app-views/default-modules.js +201 -0
  3. package/dist/components/app-views/default-modules.js.map +1 -0
  4. package/dist/components/app-views/module.d.ts +93 -0
  5. package/dist/components/app-views/module.js +75 -0
  6. package/dist/components/app-views/module.js.map +1 -0
  7. package/dist/components/app-views/sidebar.d.ts +95 -0
  8. package/dist/components/app-views/sidebar.js +153 -0
  9. package/dist/components/app-views/sidebar.js.map +1 -0
  10. package/dist/components/builder/builder-store.d.ts +7 -1
  11. package/dist/components/builder/builder-store.js +1 -1
  12. package/dist/components/builder/builder-store.js.map +1 -1
  13. package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
  14. package/dist/components/builder/canvas-breakpoints.js +55 -0
  15. package/dist/components/builder/canvas-breakpoints.js.map +1 -0
  16. package/dist/components/builder/canvas.js +14 -8
  17. package/dist/components/builder/canvas.js.map +1 -1
  18. package/dist/components/builder/composition-field-editor.js +18 -17
  19. package/dist/components/builder/composition-field-editor.js.map +1 -1
  20. package/dist/components/cms/bulk-actions.d.ts +23 -5
  21. package/dist/components/cms/bulk-actions.js +7 -3
  22. package/dist/components/cms/bulk-actions.js.map +1 -1
  23. package/dist/components/cms/bulk-bar.d.ts +22 -6
  24. package/dist/components/cms/bulk-bar.js +46 -9
  25. package/dist/components/cms/bulk-bar.js.map +1 -1
  26. package/dist/components/cms/cms-index.d.ts +4 -2
  27. package/dist/components/cms/cms-index.js +6 -4
  28. package/dist/components/cms/cms-index.js.map +1 -1
  29. package/dist/components/cms/cms-store.d.ts +6 -5
  30. package/dist/components/cms/cms-store.js +51 -9
  31. package/dist/components/cms/cms-store.js.map +1 -1
  32. package/dist/components/cms/collection-table.js +7 -3
  33. package/dist/components/cms/collection-table.js.map +1 -1
  34. package/dist/components/cms/collections-sidebar.d.ts +10 -1
  35. package/dist/components/cms/collections-sidebar.js +13 -2
  36. package/dist/components/cms/collections-sidebar.js.map +1 -1
  37. package/dist/components/cms/field-control.d.ts +11 -2
  38. package/dist/components/cms/field-control.js +22 -1
  39. package/dist/components/cms/field-control.js.map +1 -1
  40. package/dist/components/cms/field-renderer.d.ts +71 -0
  41. package/dist/components/cms/field-renderer.js +67 -0
  42. package/dist/components/cms/field-renderer.js.map +1 -0
  43. package/dist/components/cms/filter-builder.js +6 -28
  44. package/dist/components/cms/filter-builder.js.map +1 -1
  45. package/dist/components/cms/scalar-value-editor.d.ts +35 -0
  46. package/dist/components/cms/scalar-value-editor.js +43 -0
  47. package/dist/components/cms/scalar-value-editor.js.map +1 -0
  48. package/dist/components/project-session.js +8 -3
  49. package/dist/components/project-session.js.map +1 -1
  50. package/dist/components/section-nav.d.ts +10 -10
  51. package/dist/components/section-nav.js +48 -25
  52. package/dist/components/section-nav.js.map +1 -1
  53. package/dist/components/section-surface.js +2 -3
  54. package/dist/components/section-surface.js.map +1 -1
  55. package/dist/components/studio-project-screen.d.ts +3 -4
  56. package/dist/components/studio-project-screen.js +29 -147
  57. package/dist/components/studio-project-screen.js.map +1 -1
  58. package/dist/components/studio-shell.js +13 -9
  59. package/dist/components/studio-shell.js.map +1 -1
  60. package/dist/lib/data/collection-content.d.ts +19 -1
  61. package/dist/lib/data/collection-content.js +16 -0
  62. package/dist/lib/data/collection-content.js.map +1 -1
  63. package/dist/lib/workspace/index.d.ts +27 -1
  64. package/dist/lib/workspace/index.js +34 -0
  65. package/dist/lib/workspace/index.js.map +1 -1
  66. package/dist/router/match.d.ts +22 -34
  67. package/dist/router/match.js +22 -40
  68. package/dist/router/match.js.map +1 -1
  69. package/package.json +2 -2
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router';
3
- import { availabilityOf, sectionStateSchema } from '@elytracms/core/studio-core';
3
+ import { availabilityOf, SECTIONS, sectionStateSchema } from '@elytracms/core/studio-core';
4
4
  import { Code, Row, Stack, Text } from '@elytracms/studio/ui';
5
5
  import { cn } from '@elytracms/studio/lib/utils';
6
6
  import { resolveSectionState, SectionStateView } from './section-state';
7
- import { sectionNavItem } from './sections';
8
7
  /** Parse a `?state=` search param into a SectionState (undefined when absent/invalid). */
9
8
  export function parseSectionState(raw) {
10
9
  const parsed = sectionStateSchema.safeParse(raw);
@@ -18,7 +17,7 @@ export function parseSectionState(raw) {
18
17
  export function SectionSurface({ project, section, forcedState, children }) {
19
18
  const availability = availabilityOf(project.settings.sectionAvailability, section);
20
19
  const state = resolveSectionState(availability, forcedState);
21
- return (_jsxs(Stack, { gap: "none", className: "min-h-full", children: [_jsx(StateSwitcher, { projectId: project.id, section: section, current: forcedState }), state === 'working' ? (children) : (_jsx(SectionStateView, { state: state, title: sectionNavItem(section).label }))] }));
20
+ return (_jsxs(Stack, { gap: "none", className: "min-h-full", children: [_jsx(StateSwitcher, { projectId: project.id, section: section, current: forcedState }), state === 'working' ? (children) : (_jsx(SectionStateView, { state: state, title: SECTIONS[section].label }))] }));
22
21
  }
23
22
  /**
24
23
  * Deterministic state switcher (brief §11). Links to the same section with each
@@ -1 +1 @@
1
- {"version":3,"file":"section-surface.js","sourceRoot":"","sources":["../../src/components/section-surface.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router'\nimport type { ReactNode } from 'react'\nimport { availabilityOf, sectionStateSchema } from '@elytracms/core/studio-core'\nimport type { Project, ProjectId, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { Code, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { resolveSectionState, SectionStateView } from './section-state'\nimport { sectionNavItem } from './sections'\n\n/** Parse a `?state=` search param into a SectionState (undefined when absent/invalid). */\nexport function parseSectionState(raw: unknown): SectionState | undefined {\n const parsed = sectionStateSchema.safeParse(raw)\n return parsed.success ? parsed.data : undefined\n}\n\nexport interface SectionSurfaceProps {\n project: Project\n section: SectionId\n /** Forced state from `?state=`; overrides availability. */\n forcedState?: SectionState\n /** Working-state content. */\n children?: ReactNode\n}\n\n/**\n * Resolves a section's deterministic state (brief §8.1, §11) and renders it: the\n * working content when `working`, else a consistent state view. A small state\n * switcher makes every state reachable for inspection.\n */\nexport function SectionSurface({ project, section, forcedState, children }: SectionSurfaceProps) {\n const availability = availabilityOf(project.settings.sectionAvailability, section)\n const state = resolveSectionState(availability, forcedState)\n\n return (\n <Stack gap=\"none\" className=\"min-h-full\">\n <StateSwitcher projectId={project.id} section={section} current={forcedState} />\n {state === 'working' ? (\n children\n ) : (\n <SectionStateView state={state} title={sectionNavItem(section).label} />\n )}\n </Stack>\n )\n}\n\n/**\n * Deterministic state switcher (brief §11). Links to the same section with each\n * `?state=` value so agents/humans can reproduce every state by URL or click.\n */\nexport function StateSwitcher({\n projectId,\n section,\n current,\n}: {\n projectId: ProjectId\n section: SectionId\n current?: SectionState\n}) {\n const states: SectionState[] = ['working', 'empty', 'loading', 'error', 'unavailable', 'planned']\n const activeKey = current ?? 'default'\n return (\n <Row\n gap=\"1\"\n wrap\n className=\"shrink-0 border-b border-dashed border-border/70 px-4 py-2 sm:px-6\"\n data-testid=\"state-switcher\"\n >\n <Text size=\"xs\" tone=\"subtle\" mono className=\"mr-1\">\n state\n </Text>\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{}}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n activeKey === 'default'\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n default\n </Link>\n {states.map((s) => {\n const active = s === activeKey\n return (\n <Link\n key={s}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{ state: s }}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n active\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {s}\n </Link>\n )\n })}\n <Code tone=\"muted\" className=\"ml-2 hidden sm:inline-flex\">\n {activeKey === 'default' ? 'auto' : `?state=${activeKey}`}\n </Code>\n </Row>\n )\n}\n"]}
1
+ {"version":3,"file":"section-surface.js","sourceRoot":"","sources":["../../src/components/section-surface.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router'\nimport type { ReactNode } from 'react'\nimport { availabilityOf, SECTIONS, sectionStateSchema } from '@elytracms/core/studio-core'\nimport type { Project, ProjectId, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { Code, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { resolveSectionState, SectionStateView } from './section-state'\n\n/** Parse a `?state=` search param into a SectionState (undefined when absent/invalid). */\nexport function parseSectionState(raw: unknown): SectionState | undefined {\n const parsed = sectionStateSchema.safeParse(raw)\n return parsed.success ? parsed.data : undefined\n}\n\nexport interface SectionSurfaceProps {\n project: Project\n section: SectionId\n /** Forced state from `?state=`; overrides availability. */\n forcedState?: SectionState\n /** Working-state content. */\n children?: ReactNode\n}\n\n/**\n * Resolves a section's deterministic state (brief §8.1, §11) and renders it: the\n * working content when `working`, else a consistent state view. A small state\n * switcher makes every state reachable for inspection.\n */\nexport function SectionSurface({ project, section, forcedState, children }: SectionSurfaceProps) {\n const availability = availabilityOf(project.settings.sectionAvailability, section)\n const state = resolveSectionState(availability, forcedState)\n\n return (\n <Stack gap=\"none\" className=\"min-h-full\">\n <StateSwitcher projectId={project.id} section={section} current={forcedState} />\n {state === 'working' ? (\n children\n ) : (\n <SectionStateView state={state} title={SECTIONS[section].label} />\n )}\n </Stack>\n )\n}\n\n/**\n * Deterministic state switcher (brief §11). Links to the same section with each\n * `?state=` value so agents/humans can reproduce every state by URL or click.\n */\nexport function StateSwitcher({\n projectId,\n section,\n current,\n}: {\n projectId: ProjectId\n section: SectionId\n current?: SectionState\n}) {\n const states: SectionState[] = ['working', 'empty', 'loading', 'error', 'unavailable', 'planned']\n const activeKey = current ?? 'default'\n return (\n <Row\n gap=\"1\"\n wrap\n className=\"shrink-0 border-b border-dashed border-border/70 px-4 py-2 sm:px-6\"\n data-testid=\"state-switcher\"\n >\n <Text size=\"xs\" tone=\"subtle\" mono className=\"mr-1\">\n state\n </Text>\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{}}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n activeKey === 'default'\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n default\n </Link>\n {states.map((s) => {\n const active = s === activeKey\n return (\n <Link\n key={s}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{ state: s }}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n active\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {s}\n </Link>\n )\n })}\n <Code tone=\"muted\" className=\"ml-2 hidden sm:inline-flex\">\n {activeKey === 'default' ? 'auto' : `?state=${activeKey}`}\n </Code>\n </Row>\n )\n}\n"]}
@@ -11,9 +11,8 @@
11
11
  *
12
12
  * `projectId` stays a real route param (the `$projectId` segment); the trailing
13
13
  * pathname (`_splat`) is matched against the engine route table
14
- * (`matchStudioRoute`) and the deeper ids (collection / document / component)
15
- * flow down as props. Every screen already takes its ids as props, so this
16
- * collapse is mechanical — the package never read deep params through the router;
17
- * the disappearing route-file wrappers did.
14
+ * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)
15
+ * resolves the screen. Every surface already takes its ids as props, so the
16
+ * resolver just hands the module its parsed deep-link.
18
17
  */
19
18
  export declare function StudioProjectScreen(): import("react").JSX.Element;
@@ -1,33 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
- import { isSingleton, resolveSingletonDocument } from '@elytracms/core/cms-core';
4
- import { Link, Navigate, STUDIO_PROJECT_ROUTE, studioPath, useParams, useSearch, } from '@elytracms/studio/router';
3
+ import { Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch } from '@elytracms/studio/router';
5
4
  import { matchStudioRoute } from '@elytracms/studio/router/match';
6
5
  import { studioSearch } from '@elytracms/studio/router/search';
7
6
  import { Button, Heading, Stack, Text } from '@elytracms/studio/ui';
8
7
  import { DataProvider } from '@elytracms/studio/lib/data';
9
- import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
10
8
  import { useStudio } from './studio-context';
11
9
  import { StudioShell } from './studio-shell';
12
- import { firstAvailableSection } from './sections';
13
- import { SectionSurface } from './section-surface';
14
- import { SectionStateView } from './section-state';
15
- import { SettingsForm } from './settings-form';
16
- import { AssetLibrary } from './assets/asset-library';
17
10
  import { AssetsProvider } from './assets/assets-store';
18
11
  import { CollabProvider } from './collab/collab-context';
19
- import { RoutesShell } from './routes/routes-shell';
20
- import { ExtensionsView } from './extensions/extensions-view';
21
- import { RegistryBrowser } from './components/registry-browser';
22
- import { ComponentsIndex } from './components/components-index';
23
- import { ComponentDetail } from './components/component-detail';
24
- import { CmsSessionGate, ProjectSessionProvider } from './project-session';
25
- import { CmsProvider, useCms } from './cms/cms-store';
26
- import { CollectionsSidebar } from './cms/collections-sidebar';
27
- import { CmsIndex } from './cms/cms-index';
28
- import { CollectionTable } from './cms/collection-table';
29
- import { DetailEditor } from './cms/detail-editor';
30
- import { OperationErrorBanner } from './operation-error';
12
+ import { ProjectSessionProvider } from './project-session';
13
+ import { firstAvailableModule, flatPath } from './app-views/module';
14
+ import { useAppViews } from './app-views/default-modules';
31
15
  /**
32
16
  * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.
33
17
  *
@@ -41,10 +25,9 @@ import { OperationErrorBanner } from './operation-error';
41
25
  *
42
26
  * `projectId` stays a real route param (the `$projectId` segment); the trailing
43
27
  * pathname (`_splat`) is matched against the engine route table
44
- * (`matchStudioRoute`) and the deeper ids (collection / document / component)
45
- * flow down as props. Every screen already takes its ids as props, so this
46
- * collapse is mechanical — the package never read deep params through the router;
47
- * the disappearing route-file wrappers did.
28
+ * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)
29
+ * resolves the screen. Every surface already takes its ids as props, so the
30
+ * resolver just hands the module its parsed deep-link.
48
31
  */
49
32
  export function StudioProjectScreen() {
50
33
  const params = useParams({ strict: false });
@@ -85,132 +68,31 @@ function ProjectProviders({ project, children }) {
85
68
  _jsx(DataProvider, { projectId: project.id, children: _jsx(AssetsProvider, { projectId: project.id, children: _jsx(CollabProvider, { projectId: project.id, children: children }, project.id) }, project.id) }, project.id));
86
69
  }
87
70
  /**
88
- * The URL→screen switch (EC-223 Stage 6) — what the per-section `<Outlet />`s
89
- * used to resolve. Renders inside the shell + provider stack, so every screen
90
- * sees the same live graph, CMS store, and breadcrumb context the file routes
91
- * gave it. `index` and `builder` are redirect-only screens, kept here (rather
92
- * than short-circuited before the shell) because the retired routes nested under
93
- * the project layout too — they mounted the stack and then navigated, exact
94
- * parity.
71
+ * The URL→screen switch (EC-223 Stage 6), now driven by the app-view module
72
+ * registry (EC-239). The bare project URL redirects to the first available
73
+ * module; otherwise the matched module id is resolved from the registry (defaults
74
+ * + `config.appViews`) and its `parsePath` interprets the trailing segments into a
75
+ * deep-link the module renders. An unknown module id or an unparseable path is a
76
+ * not-found; module CONFIG problems (duplicate/invalid `appViews`) surface as
77
+ * visible issues in the shell, never silently. Renders inside the shell + provider
78
+ * stack, so every surface sees the same live CMS store and breadcrumb context.
95
79
  */
96
80
  function StudioSectionResolver({ project, match, search, }) {
97
81
  const projectId = project.id;
98
- switch (match.section) {
99
- case 'index':
100
- // Bare `/$projectId` → the project's first available section.
101
- return (_jsx(Navigate, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: firstAvailableSection(project) }, replace: true }));
102
- case 'settings':
103
- return (_jsx(SectionSurface, { project: project, section: "settings", forcedState: search.state, children: _jsx(SettingsForm, { project: project }) }));
104
- case 'assets':
105
- return (_jsx(SectionSurface, { project: project, section: "assets", forcedState: search.state, children: _jsx(AssetLibrary, { projectId: projectId, focusAssetId: search.asset }) }));
106
- case 'routes':
107
- // Deterministic forced states (?state=) for the surface (EC-036).
108
- return search.state && search.state !== 'working' ? (_jsx(SectionStateView, { state: search.state, title: "Routes" })) : (_jsx(RoutesShell, { project: project }));
109
- case 'extensions':
110
- return search.state && search.state !== 'working' ? (_jsx(SectionStateView, { state: search.state, title: "Extensions" })) : (_jsx(ExtensionsView, {}));
111
- case 'builder':
112
- // Retired surface (EC-187 slice 6): a page is a document in the `page`
113
- // collection. A `?page=<id>` deep link lands on that document's editor;
114
- // otherwise the page-collection list. The page-graph-only params have no
115
- // home here and are dropped.
116
- return (_jsx(Navigate, { to: STUDIO_PROJECT_ROUTE, params: {
117
- projectId,
118
- _splat: search.page
119
- ? studioPath.cmsDocument('page', search.page)
120
- : studioPath.cmsCollection('page'),
121
- }, replace: true }));
122
- case 'components':
123
- return (_jsx(ComponentsScreen, { projectId: projectId, componentId: match.componentId, state: search.state }));
124
- case 'cms':
125
- return (_jsx(CmsScreen, { project: project, collectionId: match.collectionId, documentId: match.documentId, state: search.state, field: search.field }));
126
- case 'not-found':
127
- default:
128
- return _jsx(StudioRouteNotFound, {});
82
+ const appViews = useAppViews();
83
+ if (match.section === 'index') {
84
+ // Bare `/$projectId` → the project's first available module.
85
+ return (_jsx(Navigate, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: firstAvailableModule(appViews.modules, project) }, replace: true }));
129
86
  }
130
- }
131
- /**
132
- * Components section (EC-050): a registry-browser sidebar beside the active
133
- * screen — the index (no selection) or a component's detail. Forced states
134
- * (?state=) replace the whole surface.
135
- */
136
- function ComponentsScreen({ projectId, componentId, state, }) {
137
- if (state && state !== 'working') {
138
- return _jsx(SectionStateView, { state: state, title: "Components" });
139
- }
140
- return (_jsxs("div", { className: "flex h-full min-h-0", children: [_jsx("aside", { className: "hidden h-full min-h-0 w-[300px] shrink-0 border-r border-border md:block", children: _jsx(RegistryBrowser, { projectId: projectId, activeComponentId: componentId ?? undefined }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: componentId === null ? (_jsx(ComponentsIndex, { projectId: projectId })) : (_jsx(ComponentDetail, { projectId: projectId, componentId: componentId })) })] }));
141
- }
142
- /**
143
- * CMS section: hydrates the persisted (or kit-seeded) collections + documents,
144
- * then renders the collections sidebar beside the active screen (index, a
145
- * collection table, or a document editor). Forced states (?state=) replace the
146
- * whole surface before the provider mounts.
147
- */
148
- function CmsScreen({ project, collectionId, documentId, state, field, }) {
149
- const projectId = project.id;
150
- if (state && state !== 'working') {
151
- return _jsx(SectionStateView, { state: state, title: "CMS" });
152
- }
153
- // A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.
154
- const kit = getInstantiatedKit(projectId);
155
- return (
156
- // EC-177 gap 1: in Convex mode every CMS mount loads the CURRENT persisted
157
- // schema + documents (fixtures only when nothing is persisted — first run).
158
- // Playground renders synchronously with `null` → fixture seeds, as before.
159
- _jsx(CmsSessionGate, { children: (cms) => (_jsx(CmsProvider, { projectId: projectId, initialCollections: cms?.collections ?? kit?.collections, initialDocuments: cms?.documents ?? kit?.documents, initialPublishedKeys: cms?.publishedKeys, initialLocaleConfig: cms
160
- ? // Hydrated sessions follow the project's persisted locale settings.
161
- { default: project.settings.defaultLocale, locales: [...project.settings.locales] }
162
- : kit?.localeConfig, children: _jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx(CmsPersistErrorBanner, {}), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx("aside", { className: "hidden h-full min-h-0 w-[240px] shrink-0 border-r border-border md:block", children: _jsx(CollectionsSidebar, { projectId: projectId, activeCollectionId: collectionId ?? undefined }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CmsContent, { projectId: projectId, collectionId: collectionId, documentId: documentId, field: field }) })] })] }) }, projectId)) }));
163
- }
164
- /** Resolves the active CMS screen inside the provider (needs the live store). */
165
- function CmsContent({ projectId, collectionId, documentId, field, }) {
166
- const cms = useCms();
167
- if (collectionId === null)
168
- return _jsx(CmsIndex, { projectId: projectId });
169
- if (documentId === null) {
170
- const collection = cms.getCollection(collectionId);
171
- // EC-274: a singleton is a UI lens — at the collection URL it opens its one
172
- // document directly *while in its expected shape*, but degrades to the list
173
- // (with a "for review" lane) on drift. SingletonGate decides per the data.
174
- if (collection && isSingleton(collection)) {
175
- return _jsx(SingletonGate, { projectId: projectId, collectionId: collectionId });
176
- }
177
- return _jsx(CollectionTable, { projectId: projectId, collectionId: collectionId });
178
- }
179
- return (_jsx(DetailEditor, { projectId: projectId, collectionId: collectionId, documentId: documentId, focusPath: field }));
180
- }
181
- /**
182
- * EC-274 — the singleton lens gate. At a singleton collection's URL, resolve its
183
- * lone document and open it directly; on drift (> 1 document) fall back to the
184
- * list with the "for review" lane so an editor can resolve the conflict. The one
185
- * document keeps a normal id — nothing here names a literal document id.
186
- */
187
- function SingletonGate({ projectId, collectionId }) {
188
- const cms = useCms();
189
- const collection = cms.getCollection(collectionId);
190
- const docs = cms.documentsOf(collectionId);
191
- const resolution = collection
192
- ? resolveSingletonDocument(collection, docs)
193
- : { kind: 'empty' };
194
- // Materialize the lone document on first visit (an empty draft, persisted like
195
- // any create). Idempotent in the store; only fires while genuinely empty.
196
- useEffect(() => {
197
- if (resolution.kind === 'empty')
198
- cms.ensureSingletonDocument(collectionId);
199
- }, [resolution.kind, cms, collectionId]);
200
- if (resolution.kind === 'ready') {
201
- return (_jsx(DetailEditor, { projectId: projectId, collectionId: collectionId, documentId: resolution.doc.id }));
202
- }
203
- if (resolution.kind === 'drift') {
204
- return _jsx(CollectionTable, { projectId: projectId, collectionId: collectionId });
205
- }
206
- return (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsxs(Text, { size: "sm", tone: "muted", children: ["Opening ", collection?.form?.label ?? collectionId, "\u2026"] }) }));
207
- }
208
- /** Failed/denied persisted CMS mutations are always visible (EC-141). */
209
- function CmsPersistErrorBanner() {
210
- const cms = useCms();
211
- if (!cms.persistError)
212
- return null;
213
- return (_jsx("div", { className: "shrink-0 border-b border-border p-2", children: _jsx(OperationErrorBanner, { error: cms.persistError, onDismiss: cms.clearPersistError }) }));
87
+ const module = appViews.get(match.moduleId);
88
+ if (!module)
89
+ return _jsx(StudioRouteNotFound, {});
90
+ // A flat module (no `parsePath`) accepts only the bare path; a nested one
91
+ // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.
92
+ const deepLink = (module.parsePath ?? flatPath)(match.segments);
93
+ if (deepLink === null)
94
+ return _jsx(StudioRouteNotFound, {});
95
+ return _jsx(_Fragment, { children: module.render({ project, projectId, deepLink, search }) });
214
96
  }
215
97
  function ProjectNotFound() {
216
98
  return (_jsx("div", { className: "grid min-h-dvh place-items-center p-8", children: _jsxs(Stack, { gap: "3", align: "center", className: "max-w-sm text-center", children: [_jsx(Heading, { size: "display", children: "Project not found" }), _jsx(Text, { tone: "muted", children: "This project doesn\u2019t exist or was deleted. It may only have existed in this session." }), _jsx(Button, { asChild: true, children: _jsx(Link, { to: "/", children: "Back to projects" }) })] }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"studio-project-screen.js","sourceRoot":"","sources":["../../src/components/studio-project-screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { isSingleton, resolveSingletonDocument } from '@elytracms/core/cms-core'\nimport type { Project, SectionState } from '@elytracms/core/studio-core'\nimport {\n Link,\n Navigate,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useParams,\n useSearch,\n} from '@elytracms/studio/router'\nimport { matchStudioRoute } from '@elytracms/studio/router/match'\nimport type { StudioRouteMatch } from '@elytracms/studio/router/match'\nimport { studioSearch } from '@elytracms/studio/router/search'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\nimport { Button, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { DataProvider } from '@elytracms/studio/lib/data'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { useStudio } from './studio-context'\nimport { StudioShell } from './studio-shell'\nimport { firstAvailableSection } from './sections'\nimport { SectionSurface } from './section-surface'\nimport { SectionStateView } from './section-state'\nimport { SettingsForm } from './settings-form'\nimport { AssetLibrary } from './assets/asset-library'\nimport { AssetsProvider } from './assets/assets-store'\nimport { CollabProvider } from './collab/collab-context'\nimport { RoutesShell } from './routes/routes-shell'\nimport { ExtensionsView } from './extensions/extensions-view'\nimport { RegistryBrowser } from './components/registry-browser'\nimport { ComponentsIndex } from './components/components-index'\nimport { ComponentDetail } from './components/component-detail'\nimport { CmsSessionGate, ProjectSessionProvider } from './project-session'\nimport { CmsProvider, useCms } from './cms/cms-store'\nimport { CollectionsSidebar } from './cms/collections-sidebar'\nimport { CmsIndex } from './cms/cms-index'\nimport { CollectionTable } from './cms/collection-table'\nimport { DetailEditor } from './cms/detail-editor'\nimport { OperationErrorBanner } from './operation-error'\n\n/**\n * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.\n *\n * The consumer mounts ONE splat route (`/$projectId/$`) whose component is this.\n * Everything that was a per-project file route — the provider stack, the section\n * rail shell, and every section screen — now lives inside the package and is\n * resolved here from the URL. The consumer's route tree shrinks to a single\n * ~10-line mount, which is the whole point of Stage 6: the engine owns its\n * navigation, so an external repo consumes the studio without rebuilding a route\n * file per screen.\n *\n * `projectId` stays a real route param (the `$projectId` segment); the trailing\n * pathname (`_splat`) is matched against the engine route table\n * (`matchStudioRoute`) and the deeper ids (collection / document / component)\n * flow down as props. Every screen already takes its ids as props, so this\n * collapse is mechanical — the package never read deep params through the router;\n * the disappearing route-file wrappers did.\n */\nexport function StudioProjectScreen() {\n const params = useParams({ strict: false }) as { projectId?: string; _splat?: string }\n // The route's VALIDATED search (the splat's `validateSearch: studioSearch`).\n // Read it through `useSearch`, not `useLocation().search`: the validated search\n // is resolved at route-match time and stable from the component's first render,\n // whereas `location.search` can lag a tick during hydration — long enough for a\n // child `<Navigate>` (the builder redirect) to fire on a momentarily-empty\n // search and drop its `?page=` deep-link target.\n const rawSearch = useSearch({ strict: false }) as Record<string, unknown>\n const studio = useStudio()\n const projectId = params.projectId ?? ''\n const project = studio.getProject(projectId)\n\n // Record the active project once it resolves (preserves recents across sections).\n useEffect(() => {\n if (project) studio.activate(project.id)\n }, [project, studio])\n\n if (!project) return <ProjectNotFound />\n\n const match = matchStudioRoute(params._splat ?? '')\n const search = studioSearch(rawSearch)\n\n return (\n <StudioShell project={project}>\n {/* EC-177 gap 1: in Convex mode the session HYDRATES from persistence\n before any provider mounts — the builder boots from the latest\n persisted revision, publishing reflects the persisted state, and the\n preview content engine serves live documents. Playground renders\n synchronously from fixtures (session: null), exactly as before. */}\n <ProjectSessionProvider key={project.id} projectId={project.id}>\n <ProjectProviders project={project}>\n <StudioSectionResolver project={project} match={match} search={search} />\n </ProjectProviders>\n </ProjectSessionProvider>\n </StudioShell>\n )\n}\n\n/**\n * The per-project provider stack, mounted once the session source is known.\n *\n * EC-287: there is no project-level builder graph anymore — layouts are code and\n * the DB holds content only. The block-field canvas mounts its OWN scoped\n * `BuilderProvider` per document (`composition-field-editor`), so no graph is\n * threaded here. `CollabProvider` stays as the canvas's session-scoped\n * presence/perspective store, seeded graph-lessly.\n */\nfunction ProjectProviders({ project, children }: { project: Project; children: ReactNode }) {\n return (\n // Key by project id so the session data sources reset when switching projects.\n // The data store hydrates/persists this project's `cms-data-sources` records.\n <DataProvider key={project.id} projectId={project.id}>\n {/* EC-195: project-level so the canvas resolves `asset`-typed Image props\n and the inspector picks from the live library — not just the Assets\n section. Asset METADATA only (small); blobs stay lazy. */}\n <AssetsProvider key={project.id} projectId={project.id}>\n <CollabProvider key={project.id} projectId={project.id}>\n {children}\n </CollabProvider>\n </AssetsProvider>\n </DataProvider>\n )\n}\n\n/**\n * The URL→screen switch (EC-223 Stage 6) — what the per-section `<Outlet />`s\n * used to resolve. Renders inside the shell + provider stack, so every screen\n * sees the same live graph, CMS store, and breadcrumb context the file routes\n * gave it. `index` and `builder` are redirect-only screens, kept here (rather\n * than short-circuited before the shell) because the retired routes nested under\n * the project layout too — they mounted the stack and then navigated, exact\n * parity.\n */\nfunction StudioSectionResolver({\n project,\n match,\n search,\n}: {\n project: Project\n match: StudioRouteMatch\n search: StudioSearch\n}) {\n const projectId = project.id\n\n switch (match.section) {\n case 'index':\n // Bare `/$projectId` → the project's first available section.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: firstAvailableSection(project) }}\n replace\n />\n )\n\n case 'settings':\n return (\n <SectionSurface project={project} section=\"settings\" forcedState={search.state}>\n <SettingsForm project={project} />\n </SectionSurface>\n )\n\n case 'assets':\n return (\n <SectionSurface project={project} section=\"assets\" forcedState={search.state}>\n {/* The AssetsProvider is mounted at the project level (EC-195). */}\n <AssetLibrary projectId={projectId} focusAssetId={search.asset} />\n </SectionSurface>\n )\n\n case 'routes':\n // Deterministic forced states (?state=) for the surface (EC-036).\n return search.state && search.state !== 'working' ? (\n <SectionStateView state={search.state} title=\"Routes\" />\n ) : (\n <RoutesShell project={project} />\n )\n\n case 'extensions':\n return search.state && search.state !== 'working' ? (\n <SectionStateView state={search.state} title=\"Extensions\" />\n ) : (\n <ExtensionsView />\n )\n\n case 'builder':\n // Retired surface (EC-187 slice 6): a page is a document in the `page`\n // collection. A `?page=<id>` deep link lands on that document's editor;\n // otherwise the page-collection list. The page-graph-only params have no\n // home here and are dropped.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{\n projectId,\n _splat: search.page\n ? studioPath.cmsDocument('page', search.page)\n : studioPath.cmsCollection('page'),\n }}\n replace\n />\n )\n\n case 'components':\n return (\n <ComponentsScreen\n projectId={projectId}\n componentId={match.componentId}\n state={search.state}\n />\n )\n\n case 'cms':\n return (\n <CmsScreen\n project={project}\n collectionId={match.collectionId}\n documentId={match.documentId}\n state={search.state}\n field={search.field}\n />\n )\n\n case 'not-found':\n default:\n return <StudioRouteNotFound />\n }\n}\n\n/**\n * Components section (EC-050): a registry-browser sidebar beside the active\n * screen — the index (no selection) or a component's detail. Forced states\n * (?state=) replace the whole surface.\n */\nfunction ComponentsScreen({\n projectId,\n componentId,\n state,\n}: {\n projectId: string\n componentId: string | null\n state?: SectionState\n}) {\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"Components\" />\n }\n return (\n <div className=\"flex h-full min-h-0\">\n <aside className=\"hidden h-full min-h-0 w-[300px] shrink-0 border-r border-border md:block\">\n <RegistryBrowser projectId={projectId} activeComponentId={componentId ?? undefined} />\n </aside>\n <div className=\"min-h-0 flex-1 overflow-auto\">\n {componentId === null ? (\n <ComponentsIndex projectId={projectId} />\n ) : (\n <ComponentDetail projectId={projectId} componentId={componentId} />\n )}\n </div>\n </div>\n )\n}\n\n/**\n * CMS section: hydrates the persisted (or kit-seeded) collections + documents,\n * then renders the collections sidebar beside the active screen (index, a\n * collection table, or a document editor). Forced states (?state=) replace the\n * whole surface before the provider mounts.\n */\nfunction CmsScreen({\n project,\n collectionId,\n documentId,\n state,\n field,\n}: {\n project: Project\n collectionId: string | null\n documentId: string | null\n state?: SectionState\n field?: string\n}) {\n const projectId = project.id\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"CMS\" />\n }\n\n // A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.\n const kit = getInstantiatedKit(projectId)\n\n return (\n // EC-177 gap 1: in Convex mode every CMS mount loads the CURRENT persisted\n // schema + documents (fixtures only when nothing is persisted — first run).\n // Playground renders synchronously with `null` → fixture seeds, as before.\n <CmsSessionGate>\n {(cms) => (\n <CmsProvider\n key={projectId}\n projectId={projectId}\n initialCollections={cms?.collections ?? kit?.collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n initialLocaleConfig={\n cms\n ? // Hydrated sessions follow the project's persisted locale settings.\n { default: project.settings.defaultLocale, locales: [...project.settings.locales] }\n : kit?.localeConfig\n }\n >\n <div className=\"flex h-full min-h-0 flex-col\">\n <CmsPersistErrorBanner />\n <div className=\"flex min-h-0 flex-1\">\n <aside className=\"hidden h-full min-h-0 w-[240px] shrink-0 border-r border-border md:block\">\n <CollectionsSidebar\n projectId={projectId}\n activeCollectionId={collectionId ?? undefined}\n />\n </aside>\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <CmsContent\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n field={field}\n />\n </div>\n </div>\n </div>\n </CmsProvider>\n )}\n </CmsSessionGate>\n )\n}\n\n/** Resolves the active CMS screen inside the provider (needs the live store). */\nfunction CmsContent({\n projectId,\n collectionId,\n documentId,\n field,\n}: {\n projectId: string\n collectionId: string | null\n documentId: string | null\n field?: string\n}) {\n const cms = useCms()\n\n if (collectionId === null) return <CmsIndex projectId={projectId} />\n\n if (documentId === null) {\n const collection = cms.getCollection(collectionId)\n // EC-274: a singleton is a UI lens — at the collection URL it opens its one\n // document directly *while in its expected shape*, but degrades to the list\n // (with a \"for review\" lane) on drift. SingletonGate decides per the data.\n if (collection && isSingleton(collection)) {\n return <SingletonGate projectId={projectId} collectionId={collectionId} />\n }\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n\n return (\n <DetailEditor\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n focusPath={field}\n />\n )\n}\n\n/**\n * EC-274 — the singleton lens gate. At a singleton collection's URL, resolve its\n * lone document and open it directly; on drift (> 1 document) fall back to the\n * list with the \"for review\" lane so an editor can resolve the conflict. The one\n * document keeps a normal id — nothing here names a literal document id.\n */\nfunction SingletonGate({ projectId, collectionId }: { projectId: string; collectionId: string }) {\n const cms = useCms()\n const collection = cms.getCollection(collectionId)\n const docs = cms.documentsOf(collectionId)\n const resolution = collection\n ? resolveSingletonDocument(collection, docs)\n : ({ kind: 'empty' } as const)\n\n // Materialize the lone document on first visit (an empty draft, persisted like\n // any create). Idempotent in the store; only fires while genuinely empty.\n useEffect(() => {\n if (resolution.kind === 'empty') cms.ensureSingletonDocument(collectionId)\n }, [resolution.kind, cms, collectionId])\n\n if (resolution.kind === 'ready') {\n return (\n <DetailEditor projectId={projectId} collectionId={collectionId} documentId={resolution.doc.id} />\n )\n }\n if (resolution.kind === 'drift') {\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n Opening {collection?.form?.label ?? collectionId}…\n </Text>\n </div>\n )\n}\n\n/** Failed/denied persisted CMS mutations are always visible (EC-141). */\nfunction CmsPersistErrorBanner() {\n const cms = useCms()\n if (!cms.persistError) return null\n return (\n <div className=\"shrink-0 border-b border-border p-2\">\n <OperationErrorBanner error={cms.persistError} onDismiss={cms.clearPersistError} />\n </div>\n )\n}\n\nfunction ProjectNotFound() {\n return (\n <div className=\"grid min-h-dvh place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading size=\"display\">Project not found</Heading>\n <Text tone=\"muted\">\n This project doesn’t exist or was deleted. It may only have existed in this session.\n </Text>\n <Button asChild>\n <Link to=\"/\">Back to projects</Link>\n </Button>\n </Stack>\n </div>\n )\n}\n\nfunction StudioRouteNotFound() {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"2\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n Page not found\n </Heading>\n <Text tone=\"muted\">This studio URL doesn’t resolve to a section.</Text>\n </Stack>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"studio-project-screen.js","sourceRoot":"","sources":["../../src/components/studio-project-screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport { Link, Navigate, STUDIO_PROJECT_ROUTE, useParams, useSearch } from '@elytracms/studio/router'\nimport { matchStudioRoute } from '@elytracms/studio/router/match'\nimport type { StudioRouteMatch } from '@elytracms/studio/router/match'\nimport { studioSearch } from '@elytracms/studio/router/search'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\nimport { Button, Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { DataProvider } from '@elytracms/studio/lib/data'\nimport { useStudio } from './studio-context'\nimport { StudioShell } from './studio-shell'\nimport { AssetsProvider } from './assets/assets-store'\nimport { CollabProvider } from './collab/collab-context'\nimport { ProjectSessionProvider } from './project-session'\nimport { firstAvailableModule, flatPath } from './app-views/module'\nimport { useAppViews } from './app-views/default-modules'\n\n/**\n * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.\n *\n * The consumer mounts ONE splat route (`/$projectId/$`) whose component is this.\n * Everything that was a per-project file route — the provider stack, the section\n * rail shell, and every section screen — now lives inside the package and is\n * resolved here from the URL. The consumer's route tree shrinks to a single\n * ~10-line mount, which is the whole point of Stage 6: the engine owns its\n * navigation, so an external repo consumes the studio without rebuilding a route\n * file per screen.\n *\n * `projectId` stays a real route param (the `$projectId` segment); the trailing\n * pathname (`_splat`) is matched against the engine route table\n * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)\n * resolves the screen. Every surface already takes its ids as props, so the\n * resolver just hands the module its parsed deep-link.\n */\nexport function StudioProjectScreen() {\n const params = useParams({ strict: false }) as { projectId?: string; _splat?: string }\n // The route's VALIDATED search (the splat's `validateSearch: studioSearch`).\n // Read it through `useSearch`, not `useLocation().search`: the validated search\n // is resolved at route-match time and stable from the component's first render,\n // whereas `location.search` can lag a tick during hydration — long enough for a\n // child `<Navigate>` (the builder redirect) to fire on a momentarily-empty\n // search and drop its `?page=` deep-link target.\n const rawSearch = useSearch({ strict: false }) as Record<string, unknown>\n const studio = useStudio()\n const projectId = params.projectId ?? ''\n const project = studio.getProject(projectId)\n\n // Record the active project once it resolves (preserves recents across sections).\n useEffect(() => {\n if (project) studio.activate(project.id)\n }, [project, studio])\n\n if (!project) return <ProjectNotFound />\n\n const match = matchStudioRoute(params._splat ?? '')\n const search = studioSearch(rawSearch)\n\n return (\n <StudioShell project={project}>\n {/* EC-177 gap 1: in Convex mode the session HYDRATES from persistence\n before any provider mounts — the builder boots from the latest\n persisted revision, publishing reflects the persisted state, and the\n preview content engine serves live documents. Playground renders\n synchronously from fixtures (session: null), exactly as before. */}\n <ProjectSessionProvider key={project.id} projectId={project.id}>\n <ProjectProviders project={project}>\n <StudioSectionResolver project={project} match={match} search={search} />\n </ProjectProviders>\n </ProjectSessionProvider>\n </StudioShell>\n )\n}\n\n/**\n * The per-project provider stack, mounted once the session source is known.\n *\n * EC-287: there is no project-level builder graph anymore — layouts are code and\n * the DB holds content only. The block-field canvas mounts its OWN scoped\n * `BuilderProvider` per document (`composition-field-editor`), so no graph is\n * threaded here. `CollabProvider` stays as the canvas's session-scoped\n * presence/perspective store, seeded graph-lessly.\n */\nfunction ProjectProviders({ project, children }: { project: Project; children: ReactNode }) {\n return (\n // Key by project id so the session data sources reset when switching projects.\n // The data store hydrates/persists this project's `cms-data-sources` records.\n <DataProvider key={project.id} projectId={project.id}>\n {/* EC-195: project-level so the canvas resolves `asset`-typed Image props\n and the inspector picks from the live library — not just the Assets\n section. Asset METADATA only (small); blobs stay lazy. */}\n <AssetsProvider key={project.id} projectId={project.id}>\n <CollabProvider key={project.id} projectId={project.id}>\n {children}\n </CollabProvider>\n </AssetsProvider>\n </DataProvider>\n )\n}\n\n/**\n * The URL→screen switch (EC-223 Stage 6), now driven by the app-view module\n * registry (EC-239). The bare project URL redirects to the first available\n * module; otherwise the matched module id is resolved from the registry (defaults\n * + `config.appViews`) and its `parsePath` interprets the trailing segments into a\n * deep-link the module renders. An unknown module id or an unparseable path is a\n * not-found; module CONFIG problems (duplicate/invalid `appViews`) surface as\n * visible issues in the shell, never silently. Renders inside the shell + provider\n * stack, so every surface sees the same live CMS store and breadcrumb context.\n */\nfunction StudioSectionResolver({\n project,\n match,\n search,\n}: {\n project: Project\n match: StudioRouteMatch\n search: StudioSearch\n}) {\n const projectId = project.id\n const appViews = useAppViews()\n\n if (match.section === 'index') {\n // Bare `/$projectId` → the project's first available module.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: firstAvailableModule(appViews.modules, project) }}\n replace\n />\n )\n }\n\n const module = appViews.get(match.moduleId)\n if (!module) return <StudioRouteNotFound />\n\n // A flat module (no `parsePath`) accepts only the bare path; a nested one\n // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.\n const deepLink = (module.parsePath ?? flatPath)(match.segments)\n if (deepLink === null) return <StudioRouteNotFound />\n\n return <>{module.render({ project, projectId, deepLink, search })}</>\n}\n\nfunction ProjectNotFound() {\n return (\n <div className=\"grid min-h-dvh place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading size=\"display\">Project not found</Heading>\n <Text tone=\"muted\">\n This project doesn’t exist or was deleted. It may only have existed in this session.\n </Text>\n <Button asChild>\n <Link to=\"/\">Back to projects</Link>\n </Button>\n </Stack>\n </div>\n )\n}\n\nfunction StudioRouteNotFound() {\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Stack gap=\"2\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading level={2} size=\"h2\">\n Page not found\n </Heading>\n <Text tone=\"muted\">This studio URL doesn’t resolve to a section.</Text>\n </Stack>\n </div>\n )\n}\n"]}
@@ -10,7 +10,8 @@ import { PlaygroundBadge } from './playground-badge';
10
10
  import { DevLoginBadge } from './dev-login-badge';
11
11
  import { ProjectSwitcher } from './project-switcher';
12
12
  import { SectionNav } from './section-nav';
13
- import { activeSectionFromPath, sectionNavItem } from './sections';
13
+ import { useAppViews } from './app-views/default-modules';
14
+ import { activeModuleFromPath } from './app-views/sidebar';
14
15
  import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context';
15
16
  /**
16
17
  * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that
@@ -21,7 +22,7 @@ import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context';
21
22
  */
22
23
  export function StudioShell({ project, children }) {
23
24
  const { pathname } = useLocation();
24
- const activeSection = activeSectionFromPath(pathname);
25
+ const activeModule = activeModuleFromPath(pathname);
25
26
  // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the
26
27
  // stored preference is read after mount (no flash script needed for a rail).
27
28
  const [railCollapsed, setRailCollapsed] = useState(false);
@@ -35,22 +36,25 @@ export function StudioShell({ project, children }) {
35
36
  return next;
36
37
  });
37
38
  }, []);
38
- return (_jsxs(BreadcrumbProvider, { children: [_jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", className: "grid size-6 shrink-0 place-items-center", "aria-label": "All projects", children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), _jsx(ProjectSwitcher, { activeProject: project }), _jsx(HeaderBreadcrumb, { projectId: project.id, activeSection: activeSection }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, {}), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { projectId: project.id, activeSection: activeSection, availability: project.settings.sectionAvailability, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
39
+ return (_jsxs(BreadcrumbProvider, { children: [_jsxs("div", { className: "flex h-dvh flex-col bg-background", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(Link, { to: "/", className: "grid size-6 shrink-0 place-items-center", "aria-label": "All projects", children: _jsx("span", { className: "size-5 rounded-[5px] bg-primary shadow-sm" }) }), _jsx(ProjectSwitcher, { activeProject: project }), _jsx(HeaderBreadcrumb, { projectId: project.id, activeModule: activeModule }), _jsx(Spacer, {}), _jsx(PlaygroundBadge, {}), _jsx(DevLoginBadge, {}), _jsx(CurrentUserMenu, { projectId: project.id })] }), _jsx(DeliveryHealthBanner, {}), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(SectionNav, { project: project, collapsed: railCollapsed, onToggleCollapsed: toggleRail }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children }) })] })] }), _jsx(Toaster, {})] }));
39
40
  }
40
41
  const CRUMB_LINK = 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground';
41
42
  const CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground';
42
43
  /**
43
44
  * The single global breadcrumb (EC-214): project (the switcher) › section › …the
44
- * active record. The section crumb is shell-owned; deeper crumbs are contributed
45
- * by the active surface via {@link useBreadcrumbTrail}. The last crumb is the
46
- * current page (plain text); earlier crumbs link back.
45
+ * active record. The section crumb is the active app-view module's label (EC-239,
46
+ * resolved from the registry); deeper crumbs are contributed by the active surface
47
+ * via {@link useBreadcrumbTrail}. The last crumb is the current page (plain text);
48
+ * earlier crumbs link back.
47
49
  */
48
- function HeaderBreadcrumb({ projectId, activeSection, }) {
50
+ function HeaderBreadcrumb({ projectId, activeModule, }) {
51
+ const views = useAppViews();
49
52
  const extra = useBreadcrumbTrail();
50
- if (!activeSection)
53
+ const module = activeModule ? views.get(activeModule) : undefined;
54
+ if (!module)
51
55
  return null;
52
56
  const sectionIsLast = extra.length === 0;
53
- return (_jsxs(Row, { gap: "1", className: "min-w-0 text-sm", "aria-label": "Breadcrumb", children: [_jsx(ChevronRight, { className: "size-3.5 shrink-0 text-muted-foreground/50", "aria-hidden": true }), sectionIsLast ? (_jsx("span", { className: CRUMB_CURRENT, children: sectionNavItem(activeSection).label })) : (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: activeSection }, className: CRUMB_LINK, children: sectionNavItem(activeSection).label })), extra.map((item, index) => {
57
+ return (_jsxs(Row, { gap: "1", className: "min-w-0 text-sm", "aria-label": "Breadcrumb", children: [_jsx(ChevronRight, { className: "size-3.5 shrink-0 text-muted-foreground/50", "aria-hidden": true }), sectionIsLast ? (_jsx("span", { className: CRUMB_CURRENT, children: module.label })) : (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: module.id }, className: CRUMB_LINK, children: module.label })), extra.map((item, index) => {
54
58
  const isLast = index === extra.length - 1;
55
59
  return (_jsxs(Row, { gap: "1", className: "min-w-0", children: [_jsx(ChevronRight, { className: "size-3.5 shrink-0 text-muted-foreground/50", "aria-hidden": true }), !isLast && item.to ? (
56
60
  // `to` and `params` are always contributed as a unit (the type
@@ -1 +1 @@
1
- {"version":3,"file":"studio-shell.js","sourceRoot":"","sources":["../../src/components/studio-shell.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGxD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAElE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAO7E;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAoB;IACjE,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAE,CAAA;IAClC,MAAM,aAAa,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAA;IAErD,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACzD,SAAS,CAAC,GAAG,EAAE;QACb,gBAAgB,CAAC,iBAAiB,EAAE,CAAC,CAAA;IACvC,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;QAClC,gBAAgB,CAAC,CAAC,IAAI,EAAE,EAAE;YACxB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAA;YAClB,kBAAkB,CAAC,IAAI,CAAC,CAAA;YACxB,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CACL,MAAC,kBAAkB,eACjB,eAAK,SAAS,EAAC,mCAAmC,aAEhD,kBAAQ,SAAS,EAAC,mEAAmE,aACnF,KAAC,IAAI,IAAC,EAAE,EAAC,GAAG,EAAC,SAAS,EAAC,yCAAyC,gBAAY,cAAc,YACxF,eAAM,SAAS,EAAC,2CAA2C,GAAG,GACzD,EACP,KAAC,eAAe,IAAC,aAAa,EAAE,OAAO,GAAI,EAC3C,KAAC,gBAAgB,IAAC,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,aAAa,EAAE,aAAa,GAAI,EACzE,KAAC,MAAM,KAAG,EAEV,KAAC,eAAe,KAAG,EAEnB,KAAC,aAAa,KAAG,EAEjB,KAAC,eAAe,IAAC,SAAS,EAAE,OAAO,CAAC,EAAE,GAAI,IACnC,EAGT,KAAC,oBAAoB,KAAG,EAExB,eAAK,SAAS,EAAC,qBAAqB,aAClC,KAAC,UAAU,IACT,SAAS,EAAE,OAAO,CAAC,EAAE,EACrB,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,EAClD,SAAS,EAAE,aAAa,EACxB,iBAAiB,EAAE,UAAU,GAC7B,EAEF,eAAM,SAAS,EAAC,8CAA8C,YAC5D,cAAK,SAAS,EAAC,8BAA8B,YAAE,QAAQ,GAAO,GACzD,IACH,IACF,EAEN,KAAC,OAAO,KAAG,IACQ,CACtB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,GACd,sFAAsF,CAAA;AACxF,MAAM,aAAa,GAAG,oDAAoD,CAAA;AAE1E;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,aAAa,GAId;IACC,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAA;IAClC,IAAI,CAAC,aAAa;QAAE,OAAO,IAAI,CAAA;IAC/B,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,CAAA;IAExC,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,iBAAiB,gBAAY,YAAY,aAC9D,KAAC,YAAY,IAAC,SAAS,EAAC,4CAA4C,wBAAe,EAClF,aAAa,CAAC,CAAC,CAAC,CACf,eAAM,SAAS,EAAE,aAAa,YAAG,cAAc,CAAC,aAAa,CAAC,CAAC,KAAK,GAAQ,CAC7E,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IACH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,EAC5C,SAAS,EAAE,UAAU,YAEpB,cAAc,CAAC,aAAa,CAAC,CAAC,KAAK,GAC/B,CACR,EACA,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBACzB,MAAM,MAAM,GAAG,KAAK,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;gBACzC,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAgC,SAAS,EAAC,SAAS,aAC7D,KAAC,YAAY,IAAC,SAAS,EAAC,4CAA4C,wBAAe,EAClF,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;wBACpB,+DAA+D;wBAC/D,oEAAoE;wBACpE,KAAC,IAAI,IAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAO,EAAE,SAAS,EAAE,UAAU,YAC3D,IAAI,CAAC,KAAK,GACN,CACR,CAAC,CAAC,CAAC,CACF,eAAM,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,YAAG,IAAI,CAAC,KAAK,GAAQ,CAC1E,KAVe,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,EAAE,CAWpC,CACP,CAAA;YACH,CAAC,CAAC,IACE,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { ChevronRight } from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project, ProjectId } from '@elytracms/core/studio-core'\nimport { Row, Spacer, Toaster } from '@elytracms/studio/ui'\nimport { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail'\nimport { CurrentUserMenu } from './current-user-menu'\nimport { DeliveryHealthBanner } from './delivery-health-banner'\nimport { PlaygroundBadge } from './playground-badge'\nimport { DevLoginBadge } from './dev-login-badge'\nimport { ProjectSwitcher } from './project-switcher'\nimport { SectionNav } from './section-nav'\nimport { activeSectionFromPath, sectionNavItem } from './sections'\nimport type { StudioNavId } from './sections'\nimport { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context'\n\nexport interface StudioShellProps {\n project: Project\n children: ReactNode\n}\n\n/**\n * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that\n * preserve the active project while the user moves between surfaces. The top bar\n * carries the single global breadcrumb reaching the active record (EC-214) — no\n * secondary band, no per-view title repeating the section. Responsive and\n * overflow-safe.\n */\nexport function StudioShell({ project, children }: StudioShellProps) {\n const { pathname } = useLocation()\n const activeSection = activeSectionFromPath(pathname)\n\n // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the\n // stored preference is read after mount (no flash script needed for a rail).\n const [railCollapsed, setRailCollapsed] = useState(false)\n useEffect(() => {\n setRailCollapsed(readRailCollapsed())\n }, [])\n const toggleRail = useCallback(() => {\n setRailCollapsed((prev) => {\n const next = !prev\n storeRailCollapsed(next)\n return next\n })\n }, [])\n\n return (\n <BreadcrumbProvider>\n <div className=\"flex h-dvh flex-col bg-background\">\n {/* Top bar — global controls + the one breadcrumb to the active record. */}\n <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border px-3\">\n <Link to=\"/\" className=\"grid size-6 shrink-0 place-items-center\" aria-label=\"All projects\">\n <span className=\"size-5 rounded-[5px] bg-primary shadow-sm\" />\n </Link>\n <ProjectSwitcher activeProject={project} />\n <HeaderBreadcrumb projectId={project.id} activeSection={activeSection} />\n <Spacer />\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {/* Dev-only unattended-auth marker (renders nothing in prod / when unset). */}\n <DevLoginBadge />\n {/* EC-150: signed-in user + their role on this project + sign-out. */}\n <CurrentUserMenu projectId={project.id} />\n </header>\n\n {/* EC-268: warns when instant-publish env is incomplete (connected mode only). */}\n <DeliveryHealthBanner />\n\n <div className=\"flex min-h-0 flex-1\">\n <SectionNav\n projectId={project.id}\n activeSection={activeSection}\n availability={project.settings.sectionAvailability}\n collapsed={railCollapsed}\n onToggleCollapsed={toggleRail}\n />\n\n <main className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-auto\">{children}</div>\n </main>\n </div>\n </div>\n {/* EC-268: transient action feedback (e.g. a publish that fails at transport). */}\n <Toaster />\n </BreadcrumbProvider>\n )\n}\n\nconst CRUMB_LINK =\n 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground'\nconst CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground'\n\n/**\n * The single global breadcrumb (EC-214): project (the switcher) › section › …the\n * active record. The section crumb is shell-owned; deeper crumbs are contributed\n * by the active surface via {@link useBreadcrumbTrail}. The last crumb is the\n * current page (plain text); earlier crumbs link back.\n */\nfunction HeaderBreadcrumb({\n projectId,\n activeSection,\n}: {\n projectId: ProjectId\n activeSection: StudioNavId | null\n}) {\n const extra = useBreadcrumbTrail()\n if (!activeSection) return null\n const sectionIsLast = extra.length === 0\n\n return (\n <Row gap=\"1\" className=\"min-w-0 text-sm\" aria-label=\"Breadcrumb\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {sectionIsLast ? (\n <span className={CRUMB_CURRENT}>{sectionNavItem(activeSection).label}</span>\n ) : (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: activeSection }}\n className={CRUMB_LINK}\n >\n {sectionNavItem(activeSection).label}\n </Link>\n )}\n {extra.map((item, index) => {\n const isLast = index === extra.length - 1\n return (\n <Row gap=\"1\" key={`${item.label}-${index}`} className=\"min-w-0\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {!isLast && item.to ? (\n // `to` and `params` are always contributed as a unit (the type\n // pairs them); the assertion satisfies the route's required params.\n <Link to={item.to} params={item.params!} className={CRUMB_LINK}>\n {item.label}\n </Link>\n ) : (\n <span className={isLast ? CRUMB_CURRENT : CRUMB_LINK}>{item.label}</span>\n )}\n </Row>\n )\n })}\n </Row>\n )\n}\n"]}
1
+ {"version":3,"file":"studio-shell.js","sourceRoot":"","sources":["../../src/components/studio-shell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { ChevronRight } from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { Project, ProjectId } from '@elytracms/core/studio-core'\nimport { Row, Spacer, Toaster } from '@elytracms/studio/ui'\nimport { readRailCollapsed, storeRailCollapsed } from '@elytracms/studio/lib/rail'\nimport { CurrentUserMenu } from './current-user-menu'\nimport { DeliveryHealthBanner } from './delivery-health-banner'\nimport { PlaygroundBadge } from './playground-badge'\nimport { DevLoginBadge } from './dev-login-badge'\nimport { ProjectSwitcher } from './project-switcher'\nimport { SectionNav } from './section-nav'\nimport { useAppViews } from './app-views/default-modules'\nimport { activeModuleFromPath } from './app-views/sidebar'\nimport { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context'\n\nexport interface StudioShellProps {\n project: Project\n children: ReactNode\n}\n\n/**\n * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that\n * preserve the active project while the user moves between surfaces. The top bar\n * carries the single global breadcrumb reaching the active record (EC-214) — no\n * secondary band, no per-view title repeating the section. Responsive and\n * overflow-safe.\n */\nexport function StudioShell({ project, children }: StudioShellProps) {\n const { pathname } = useLocation()\n const activeModule = activeModuleFromPath(pathname)\n\n // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the\n // stored preference is read after mount (no flash script needed for a rail).\n const [railCollapsed, setRailCollapsed] = useState(false)\n useEffect(() => {\n setRailCollapsed(readRailCollapsed())\n }, [])\n const toggleRail = useCallback(() => {\n setRailCollapsed((prev) => {\n const next = !prev\n storeRailCollapsed(next)\n return next\n })\n }, [])\n\n return (\n <BreadcrumbProvider>\n <div className=\"flex h-dvh flex-col bg-background\">\n {/* Top bar — global controls + the one breadcrumb to the active record. */}\n <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border px-3\">\n <Link to=\"/\" className=\"grid size-6 shrink-0 place-items-center\" aria-label=\"All projects\">\n <span className=\"size-5 rounded-[5px] bg-primary shadow-sm\" />\n </Link>\n <ProjectSwitcher activeProject={project} />\n <HeaderBreadcrumb projectId={project.id} activeModule={activeModule} />\n <Spacer />\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {/* Dev-only unattended-auth marker (renders nothing in prod / when unset). */}\n <DevLoginBadge />\n {/* EC-150: signed-in user + their role on this project + sign-out. */}\n <CurrentUserMenu projectId={project.id} />\n </header>\n\n {/* EC-268: warns when instant-publish env is incomplete (connected mode only). */}\n <DeliveryHealthBanner />\n\n <div className=\"flex min-h-0 flex-1\">\n <SectionNav\n project={project}\n collapsed={railCollapsed}\n onToggleCollapsed={toggleRail}\n />\n\n <main className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <div className=\"min-h-0 flex-1 overflow-auto\">{children}</div>\n </main>\n </div>\n </div>\n {/* EC-268: transient action feedback (e.g. a publish that fails at transport). */}\n <Toaster />\n </BreadcrumbProvider>\n )\n}\n\nconst CRUMB_LINK =\n 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground'\nconst CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground'\n\n/**\n * The single global breadcrumb (EC-214): project (the switcher) › section › …the\n * active record. The section crumb is the active app-view module's label (EC-239,\n * resolved from the registry); deeper crumbs are contributed by the active surface\n * via {@link useBreadcrumbTrail}. The last crumb is the current page (plain text);\n * earlier crumbs link back.\n */\nfunction HeaderBreadcrumb({\n projectId,\n activeModule,\n}: {\n projectId: ProjectId\n activeModule: string | null\n}) {\n const views = useAppViews()\n const extra = useBreadcrumbTrail()\n const module = activeModule ? views.get(activeModule) : undefined\n if (!module) return null\n const sectionIsLast = extra.length === 0\n\n return (\n <Row gap=\"1\" className=\"min-w-0 text-sm\" aria-label=\"Breadcrumb\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {sectionIsLast ? (\n <span className={CRUMB_CURRENT}>{module.label}</span>\n ) : (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: module.id }}\n className={CRUMB_LINK}\n >\n {module.label}\n </Link>\n )}\n {extra.map((item, index) => {\n const isLast = index === extra.length - 1\n return (\n <Row gap=\"1\" key={`${item.label}-${index}`} className=\"min-w-0\">\n <ChevronRight className=\"size-3.5 shrink-0 text-muted-foreground/50\" aria-hidden />\n {!isLast && item.to ? (\n // `to` and `params` are always contributed as a unit (the type\n // pairs them); the assertion satisfies the route's required params.\n <Link to={item.to} params={item.params!} className={CRUMB_LINK}>\n {item.label}\n </Link>\n ) : (\n <span className={isLast ? CRUMB_CURRENT : CRUMB_LINK}>{item.label}</span>\n )}\n </Row>\n )\n })}\n </Row>\n )\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { GetDocumentResult, ListDocumentsQuery, ListDocumentsResult, ResolvedDocument } from '@elytracms/core/content';
2
- import type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core';
2
+ import type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig, RouteRecord } from '@elytracms/core/cms-core';
3
3
  import type { AssetRecord } from '@elytracms/core/persistence';
4
4
  import type { ListingQuery } from '@elytracms/core/runtime-renderer';
5
5
  /**
@@ -34,6 +34,14 @@ export interface ContentSessionState {
34
34
  publishedKeys?: readonly string[];
35
35
  /** Locale config of the session content; defaults to the sample config. */
36
36
  localeConfig?: LocaleConfig;
37
+ /**
38
+ * The project's config-sourced routes (EC-207). Wired into the content client so
39
+ * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from
40
+ * the SAME routes delivery serves — the studio-live half of the host link bake.
41
+ * Absent (playground/tests) → no routes, so `resolveLink` returns null and a
42
+ * reference link degrades to the visible `unresolved-ref` issue.
43
+ */
44
+ routes?: readonly RouteRecord[];
37
45
  /**
38
46
  * The project's asset records (assets table). Wired into the lookup so the
39
47
  * content client resolves `asset`-typed FIELDS on listing/relation results
@@ -96,6 +104,16 @@ export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale)
96
104
  * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.
97
105
  */
98
106
  export declare function resolveCanvasListing(query: ListingQuery, locale?: Locale): ResolvedDocument[];
107
+ /**
108
+ * Resolve a rich-text internal link's target document to its URL path for the canvas
109
+ * (EC-291) — the studio analog of the host's baked links map. Derives the URL through
110
+ * the active session's client (published perspective, active locale, config-sourced
111
+ * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as
112
+ * delivery serves it. `null` when the target is unpublished, missing, or addressed by
113
+ * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses
114
+ * the same `resolveLink` path as the host bake, so both agree on the derived URL.
115
+ */
116
+ export declare function resolveCanvasLink(ref: DocumentRef, locale?: Locale): string | null;
99
117
  /** A pickable entry of a collection (the template-page preview switcher). */
100
118
  export interface CollectionEntrySummary {
101
119
  id: string;
@@ -137,6 +137,10 @@ function clientFor(locale) {
137
137
  perspective: 'published',
138
138
  locales: activeLocaleConfig(),
139
139
  }),
140
+ // EC-291: the routes a rich-text link resolves against (config-sourced). The
141
+ // canvas previews PUBLISHED content, so a link derives the live URL — matching
142
+ // the host's baked link map.
143
+ routes: contentSession?.routes ?? [],
140
144
  });
141
145
  clients.set(locale, client);
142
146
  return client;
@@ -182,6 +186,18 @@ export function resolveCanvasListing(query, locale = getStudioLocale()) {
182
186
  });
183
187
  return result.ok ? result.documents : [];
184
188
  }
189
+ /**
190
+ * Resolve a rich-text internal link's target document to its URL path for the canvas
191
+ * (EC-291) — the studio analog of the host's baked links map. Derives the URL through
192
+ * the active session's client (published perspective, active locale, config-sourced
193
+ * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as
194
+ * delivery serves it. `null` when the target is unpublished, missing, or addressed by
195
+ * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses
196
+ * the same `resolveLink` path as the host bake, so both agree on the derived URL.
197
+ */
198
+ export function resolveCanvasLink(ref, locale = getStudioLocale()) {
199
+ return clientFor(locale).resolveLink(ref);
200
+ }
185
201
  /** Published entries of a collection, labeled by the schema's title field. */
186
202
  export function listCollectionEntries(collectionId) {
187
203
  const collection = playgroundCollection(collectionId);