@elytracms/studio 0.0.17 → 0.0.19

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 (43) hide show
  1. package/dist/components/app-views/default-modules.d.ts +102 -26
  2. package/dist/components/app-views/default-modules.js +166 -53
  3. package/dist/components/app-views/default-modules.js.map +1 -1
  4. package/dist/components/app-views/module.d.ts +81 -50
  5. package/dist/components/app-views/module.js +25 -43
  6. package/dist/components/app-views/module.js.map +1 -1
  7. package/dist/components/app-views/sidebar.d.ts +64 -73
  8. package/dist/components/app-views/sidebar.js +120 -125
  9. package/dist/components/app-views/sidebar.js.map +1 -1
  10. package/dist/components/app-views/studio-config.d.ts +77 -0
  11. package/dist/components/app-views/studio-config.js +31 -0
  12. package/dist/components/app-views/studio-config.js.map +1 -0
  13. package/dist/components/cms/cms-index.js +8 -8
  14. package/dist/components/cms/cms-index.js.map +1 -1
  15. package/dist/components/cms/collection-focus.d.ts +10 -0
  16. package/dist/components/cms/collection-focus.js +16 -0
  17. package/dist/components/cms/collection-focus.js.map +1 -0
  18. package/dist/components/cms/field-control.d.ts +8 -5
  19. package/dist/components/cms/field-control.js +21 -10
  20. package/dist/components/cms/field-control.js.map +1 -1
  21. package/dist/components/cms/field-renderer.d.ts +28 -30
  22. package/dist/components/cms/field-renderer.js +15 -20
  23. package/dist/components/cms/field-renderer.js.map +1 -1
  24. package/dist/components/section-nav.d.ts +8 -7
  25. package/dist/components/section-nav.js +36 -26
  26. package/dist/components/section-nav.js.map +1 -1
  27. package/dist/components/studio-project-screen.js +16 -16
  28. package/dist/components/studio-project-screen.js.map +1 -1
  29. package/dist/components/studio-shell.js +13 -13
  30. package/dist/components/studio-shell.js.map +1 -1
  31. package/dist/customization.d.ts +48 -0
  32. package/dist/customization.js +46 -0
  33. package/dist/customization.js.map +1 -0
  34. package/dist/lib/cms-views/view-model.d.ts +2 -2
  35. package/dist/lib/cms-views/views-store.d.ts +1 -1
  36. package/dist/lib/workspace/index.d.ts +32 -24
  37. package/dist/lib/workspace/index.js +35 -27
  38. package/dist/lib/workspace/index.js.map +1 -1
  39. package/dist/router/match.d.ts +16 -16
  40. package/dist/router/match.js +17 -17
  41. package/dist/router/match.js.map +1 -1
  42. package/dist/ui/badge.d.ts +1 -1
  43. package/package.json +6 -2
@@ -10,8 +10,8 @@ import { StudioShell } from './studio-shell';
10
10
  import { AssetsProvider } from './assets/assets-store';
11
11
  import { CollabProvider } from './collab/collab-context';
12
12
  import { ProjectSessionProvider } from './project-session';
13
- import { firstAvailableModule, flatPath } from './app-views/module';
14
- import { useAppViews } from './app-views/default-modules';
13
+ import { flatPath } from './app-views/module';
14
+ import { landingViewId, useStudioNav } from './app-views/sidebar';
15
15
  /**
16
16
  * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.
17
17
  *
@@ -68,31 +68,31 @@ function ProjectProviders({ project, children }) {
68
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));
69
69
  }
70
70
  /**
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
71
+ * The URL→screen switch (EC-223 Stage 6), driven by the view registry DERIVED from
72
+ * the section rail (EC-237/239). The bare project URL redirects to the first
73
+ * navigable rail view; otherwise the matched view id is resolved from the registry
74
+ * (rail views + always-on built-ins) and its `parsePath` interprets the trailing
75
+ * segments into a deep-link the view renders. An unknown view id or an unparseable
76
+ * path is a not-found; rail CONFIG problems (duplicate/invalid views) surface as
77
77
  * visible issues in the shell, never silently. Renders inside the shell + provider
78
78
  * stack, so every surface sees the same live CMS store and breadcrumb context.
79
79
  */
80
80
  function StudioSectionResolver({ project, match, search, }) {
81
81
  const projectId = project.id;
82
- const appViews = useAppViews();
82
+ const nav = useStudioNav(project);
83
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 }));
84
+ // Bare `/$projectId` → the project's first navigable rail view.
85
+ return (_jsx(Navigate, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: landingViewId(nav, project) }, replace: true }));
86
86
  }
87
- const module = appViews.get(match.moduleId);
88
- if (!module)
87
+ const view = nav.registry.get(match.viewId);
88
+ if (!view)
89
89
  return _jsx(StudioRouteNotFound, {});
90
- // A flat module (no `parsePath`) accepts only the bare path; a nested one
90
+ // A flat view (no `parsePath`) accepts only the bare path; a nested one
91
91
  // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.
92
- const deepLink = (module.parsePath ?? flatPath)(match.segments);
92
+ const deepLink = (view.parsePath ?? flatPath)(match.segments);
93
93
  if (deepLink === null)
94
94
  return _jsx(StudioRouteNotFound, {});
95
- return _jsx(_Fragment, { children: module.render({ project, projectId, deepLink, search }) });
95
+ return _jsx(_Fragment, { children: view.render({ project, projectId, deepLink, search }) });
96
96
  }
97
97
  function ProjectNotFound() {
98
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 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"]}
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 { flatPath } from './app-views/module'\nimport { landingViewId, useStudioNav } from './app-views/sidebar'\n\n/**\n * EC-223 Stage 6 (catch-all routing): the studio's per-project route component.\n *\n * The consumer mounts ONE splat route (`/$projectId/$`) whose component is this.\n * Everything that was a per-project file route — the provider stack, the section\n * rail shell, and every section screen — now lives inside the package and is\n * resolved here from the URL. The consumer's route tree shrinks to a single\n * ~10-line mount, which is the whole point of Stage 6: the engine owns its\n * navigation, so an external repo consumes the studio without rebuilding a route\n * file per screen.\n *\n * `projectId` stays a real route param (the `$projectId` segment); the trailing\n * pathname (`_splat`) is matched against the engine route table\n * (`matchStudioRoute`) into a module id + segments, and the active module (EC-239)\n * resolves the screen. Every surface already takes its ids as props, so the\n * resolver just hands the module its parsed deep-link.\n */\nexport function StudioProjectScreen() {\n const params = useParams({ strict: false }) as { projectId?: string; _splat?: string }\n // The route's VALIDATED search (the splat's `validateSearch: studioSearch`).\n // Read it through `useSearch`, not `useLocation().search`: the validated search\n // is resolved at route-match time and stable from the component's first render,\n // whereas `location.search` can lag a tick during hydration — long enough for a\n // child `<Navigate>` (the builder redirect) to fire on a momentarily-empty\n // search and drop its `?page=` deep-link target.\n const rawSearch = useSearch({ strict: false }) as Record<string, unknown>\n const studio = useStudio()\n const projectId = params.projectId ?? ''\n const project = studio.getProject(projectId)\n\n // Record the active project once it resolves (preserves recents across sections).\n useEffect(() => {\n if (project) studio.activate(project.id)\n }, [project, studio])\n\n if (!project) return <ProjectNotFound />\n\n const match = matchStudioRoute(params._splat ?? '')\n const search = studioSearch(rawSearch)\n\n return (\n <StudioShell project={project}>\n {/* EC-177 gap 1: in Convex mode the session HYDRATES from persistence\n before any provider mounts — the builder boots from the latest\n persisted revision, publishing reflects the persisted state, and the\n preview content engine serves live documents. Playground renders\n synchronously from fixtures (session: null), exactly as before. */}\n <ProjectSessionProvider key={project.id} projectId={project.id}>\n <ProjectProviders project={project}>\n <StudioSectionResolver project={project} match={match} search={search} />\n </ProjectProviders>\n </ProjectSessionProvider>\n </StudioShell>\n )\n}\n\n/**\n * The per-project provider stack, mounted once the session source is known.\n *\n * EC-287: there is no project-level builder graph anymore — layouts are code and\n * the DB holds content only. The block-field canvas mounts its OWN scoped\n * `BuilderProvider` per document (`composition-field-editor`), so no graph is\n * threaded here. `CollabProvider` stays as the canvas's session-scoped\n * presence/perspective store, seeded graph-lessly.\n */\nfunction ProjectProviders({ project, children }: { project: Project; children: ReactNode }) {\n return (\n // Key by project id so the session data sources reset when switching projects.\n // The data store hydrates/persists this project's `cms-data-sources` records.\n <DataProvider key={project.id} projectId={project.id}>\n {/* EC-195: project-level so the canvas resolves `asset`-typed Image props\n and the inspector picks from the live library — not just the Assets\n section. Asset METADATA only (small); blobs stay lazy. */}\n <AssetsProvider key={project.id} projectId={project.id}>\n <CollabProvider key={project.id} projectId={project.id}>\n {children}\n </CollabProvider>\n </AssetsProvider>\n </DataProvider>\n )\n}\n\n/**\n * The URL→screen switch (EC-223 Stage 6), driven by the view registry DERIVED from\n * the section rail (EC-237/239). The bare project URL redirects to the first\n * navigable rail view; otherwise the matched view id is resolved from the registry\n * (rail views + always-on built-ins) and its `parsePath` interprets the trailing\n * segments into a deep-link the view renders. An unknown view id or an unparseable\n * path is a not-found; rail CONFIG problems (duplicate/invalid views) surface as\n * visible issues in the shell, never silently. Renders inside the shell + provider\n * stack, so every surface sees the same live CMS store and breadcrumb context.\n */\nfunction StudioSectionResolver({\n project,\n match,\n search,\n}: {\n project: Project\n match: StudioRouteMatch\n search: StudioSearch\n}) {\n const projectId = project.id\n const nav = useStudioNav(project)\n\n if (match.section === 'index') {\n // Bare `/$projectId` → the project's first navigable rail view.\n return (\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: landingViewId(nav, project) }}\n replace\n />\n )\n }\n\n const view = nav.registry.get(match.viewId)\n if (!view) return <StudioRouteNotFound />\n\n // A flat view (no `parsePath`) accepts only the bare path; a nested one\n // (CMS/components) interprets its segments. `null` → an over-deep path → not-found.\n const deepLink = (view.parsePath ?? flatPath)(match.segments)\n if (deepLink === null) return <StudioRouteNotFound />\n\n return <>{view.render({ project, projectId, deepLink, search })}</>\n}\n\nfunction ProjectNotFound() {\n return (\n <div className=\"grid min-h-dvh place-items-center p-8\">\n <Stack gap=\"3\" align=\"center\" className=\"max-w-sm text-center\">\n <Heading size=\"display\">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,8 +10,7 @@ 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 { useAppViews } from './app-views/default-modules';
14
- import { activeModuleFromPath } from './app-views/sidebar';
13
+ import { activeViewIdFromPath, useStudioNav } from './app-views/sidebar';
15
14
  import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context';
16
15
  /**
17
16
  * The Studio shell (brief §4.3, §8.1): a persistent top bar + section rail that
@@ -22,7 +21,7 @@ import { BreadcrumbProvider, useBreadcrumbTrail } from './breadcrumb-context';
22
21
  */
23
22
  export function StudioShell({ project, children }) {
24
23
  const { pathname } = useLocation();
25
- const activeModule = activeModuleFromPath(pathname);
24
+ const activeViewId = activeViewIdFromPath(pathname);
26
25
  // User-toggled, persisted rail collapse (EC-214). Defaults to expanded; the
27
26
  // stored preference is read after mount (no flash script needed for a rail).
28
27
  const [railCollapsed, setRailCollapsed] = useState(false);
@@ -36,25 +35,26 @@ export function StudioShell({ project, children }) {
36
35
  return next;
37
36
  });
38
37
  }, []);
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, {})] }));
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, { project: project, activeViewId: activeViewId }), _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, {})] }));
40
39
  }
41
40
  const CRUMB_LINK = 'max-w-[12rem] truncate text-muted-foreground transition-colors hover:text-foreground';
42
41
  const CRUMB_CURRENT = 'max-w-[16rem] truncate font-medium text-foreground';
43
42
  /**
44
43
  * The single global breadcrumb (EC-214): project (the switcher) › section › …the
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.
44
+ * active record. The section crumb is the active VIEW's label (EC-237/239, resolved
45
+ * from the rail-derived registry); deeper crumbs are contributed by the active
46
+ * surface via {@link useBreadcrumbTrail}. The last crumb is the current page (plain
47
+ * text); earlier crumbs link back.
49
48
  */
50
- function HeaderBreadcrumb({ projectId, activeModule, }) {
51
- const views = useAppViews();
49
+ function HeaderBreadcrumb({ project, activeViewId, }) {
50
+ const projectId = project.id;
51
+ const nav = useStudioNav(project);
52
52
  const extra = useBreadcrumbTrail();
53
- const module = activeModule ? views.get(activeModule) : undefined;
54
- if (!module)
53
+ const view = activeViewId ? nav.registry.get(activeViewId) : undefined;
54
+ if (!view)
55
55
  return null;
56
56
  const sectionIsLast = extra.length === 0;
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) => {
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: view.label })) : (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: view.id }, className: CRUMB_LINK, children: view.label })), extra.map((item, index) => {
58
58
  const isLast = index === extra.length - 1;
59
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 ? (
60
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":"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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
+ {"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 { activeViewIdFromPath, useStudioNav } 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 activeViewId = activeViewIdFromPath(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 project={project} activeViewId={activeViewId} />\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 VIEW's label (EC-237/239, resolved\n * from the rail-derived registry); deeper crumbs are contributed by the active\n * surface via {@link useBreadcrumbTrail}. The last crumb is the current page (plain\n * text); earlier crumbs link back.\n */\nfunction HeaderBreadcrumb({\n project,\n activeViewId,\n}: {\n project: Project\n activeViewId: string | null\n}) {\n const projectId: ProjectId = project.id\n const nav = useStudioNav(project)\n const extra = useBreadcrumbTrail()\n const view = activeViewId ? nav.registry.get(activeViewId) : undefined\n if (!view) 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}>{view.label}</span>\n ) : (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: view.id }}\n className={CRUMB_LINK}\n >\n {view.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"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `@elytracms/studio/customization` — the public customization API for a consuming
3
+ * repo's `studio.config.tsx` (EC-236/237/238/239).
4
+ *
5
+ * Everything a repo customizes about the builder UI is **live studio code passed by
6
+ * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom
7
+ * field renderers, and (in time) branding. This config is studio-only: it is handed
8
+ * to `createStudio(config, studioConfig)` at the mount and never enters the shared
9
+ * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle
10
+ * in one import surface: direct values, no registries, no string ids — the lone
11
+ * string seam is `fieldRenderers`' control key, which the delivery-shared schema
12
+ * references via `form.control`.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * // studio.config.tsx
17
+ * import { defineStudio, nav } from '@elytracms/studio/customization'
18
+ * import { ColorField } from './fields/color'
19
+ *
20
+ * export default defineStudio({
21
+ * sidebar: [
22
+ * nav.cms({ label: 'Content' }),
23
+ * nav.group('Catalog', [
24
+ * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),
25
+ * nav.cms({ label: 'Categories', collection: 'productCategory' }),
26
+ * ]),
27
+ * nav.separator(),
28
+ * nav.assets(),
29
+ * nav.settings(),
30
+ * ],
31
+ * fieldRenderers: { color: ColorField },
32
+ * })
33
+ *
34
+ * // router.tsx (the mount)
35
+ * import { createStudio } from '@elytracms/studio/customization'
36
+ * import config from '../../elytra.config'
37
+ * import studioConfig from './studio.config'
38
+ * createStudio(config, studioConfig)
39
+ * ```
40
+ */
41
+ export { defineStudio } from './components/app-views/studio-config';
42
+ export type { StudioConfig, CustomFieldRenderer, CustomFieldRendererProps, } from './components/app-views/studio-config';
43
+ export { nav } from './components/app-views/default-modules';
44
+ export type { CmsViewConfig, BuiltinViewConfig, CustomViewConfig, } from './components/app-views/default-modules';
45
+ export type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module';
46
+ export { createStudio } from './lib/workspace';
47
+ /** A ready-made example renderer; a consuming repo may use it or author its own. */
48
+ export { colorFieldRenderer } from './components/cms/field-renderer';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `@elytracms/studio/customization` — the public customization API for a consuming
3
+ * repo's `studio.config.tsx` (EC-236/237/238/239).
4
+ *
5
+ * Everything a repo customizes about the builder UI is **live studio code passed by
6
+ * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom
7
+ * field renderers, and (in time) branding. This config is studio-only: it is handed
8
+ * to `createStudio(config, studioConfig)` at the mount and never enters the shared
9
+ * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle
10
+ * in one import surface: direct values, no registries, no string ids — the lone
11
+ * string seam is `fieldRenderers`' control key, which the delivery-shared schema
12
+ * references via `form.control`.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * // studio.config.tsx
17
+ * import { defineStudio, nav } from '@elytracms/studio/customization'
18
+ * import { ColorField } from './fields/color'
19
+ *
20
+ * export default defineStudio({
21
+ * sidebar: [
22
+ * nav.cms({ label: 'Content' }),
23
+ * nav.group('Catalog', [
24
+ * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),
25
+ * nav.cms({ label: 'Categories', collection: 'productCategory' }),
26
+ * ]),
27
+ * nav.separator(),
28
+ * nav.assets(),
29
+ * nav.settings(),
30
+ * ],
31
+ * fieldRenderers: { color: ColorField },
32
+ * })
33
+ *
34
+ * // router.tsx (the mount)
35
+ * import { createStudio } from '@elytracms/studio/customization'
36
+ * import config from '../../elytra.config'
37
+ * import studioConfig from './studio.config'
38
+ * createStudio(config, studioConfig)
39
+ * ```
40
+ */
41
+ export { defineStudio } from './components/app-views/studio-config';
42
+ export { nav } from './components/app-views/default-modules';
43
+ export { createStudio } from './lib/workspace';
44
+ /** A ready-made example renderer; a consuming repo may use it or author its own. */
45
+ export { colorFieldRenderer } from './components/cms/field-renderer';
46
+ //# sourceMappingURL=customization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customization.js","sourceRoot":"","sources":["../src/customization.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAMnE,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAO5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,oFAAoF;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA","sourcesContent":["/**\n * `@elytracms/studio/customization` — the public customization API for a consuming\n * repo's `studio.config.tsx` (EC-236/237/238/239).\n *\n * Everything a repo customizes about the builder UI is **live studio code passed by\n * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom\n * field renderers, and (in time) branding. This config is studio-only: it is handed\n * to `createStudio(config, studioConfig)` at the mount and never enters the shared\n * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle\n * in one import surface: direct values, no registries, no string ids — the lone\n * string seam is `fieldRenderers`' control key, which the delivery-shared schema\n * references via `form.control`.\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n *\n * // router.tsx (the mount)\n * import { createStudio } from '@elytracms/studio/customization'\n * import config from '../../elytra.config'\n * import studioConfig from './studio.config'\n * createStudio(config, studioConfig)\n * ```\n */\nexport { defineStudio } from './components/app-views/studio-config'\nexport type {\n StudioConfig,\n CustomFieldRenderer,\n CustomFieldRendererProps,\n} from './components/app-views/studio-config'\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CmsViewConfig,\n BuiltinViewConfig,\n CustomViewConfig,\n} from './components/app-views/default-modules'\nexport type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module'\nexport { createStudio } from './lib/workspace'\n/** A ready-made example renderer; a consuming repo may use it or author its own. */\nexport { colorFieldRenderer } from './components/cms/field-renderer'\n"]}
@@ -34,8 +34,8 @@ export type FilterClause = z.infer<typeof filterClauseSchema>;
34
34
  export declare const viewSortSchema: z.ZodObject<{
35
35
  field: z.ZodString;
36
36
  dir: z.ZodEnum<{
37
- asc: "asc";
38
37
  desc: "desc";
38
+ asc: "asc";
39
39
  }>;
40
40
  }, z.core.$strip>;
41
41
  export type ViewSort = z.infer<typeof viewSortSchema>;
@@ -55,8 +55,8 @@ export declare const tableViewStateSchema: z.ZodObject<{
55
55
  sort: z.ZodNullable<z.ZodObject<{
56
56
  field: z.ZodString;
57
57
  dir: z.ZodEnum<{
58
- asc: "asc";
59
58
  desc: "desc";
59
+ asc: "asc";
60
60
  }>;
61
61
  }, z.core.$strip>>;
62
62
  columns: z.ZodArray<z.ZodString>;
@@ -41,8 +41,8 @@ export declare const savedViewRecordSchema: z.ZodObject<{
41
41
  sort: z.ZodNullable<z.ZodObject<{
42
42
  field: z.ZodString;
43
43
  dir: z.ZodEnum<{
44
- asc: "asc";
45
44
  desc: "desc";
45
+ asc: "asc";
46
46
  }>;
47
47
  }, z.core.$strip>>;
48
48
  columns: z.ZodArray<z.ZodString>;
@@ -1,20 +1,31 @@
1
- import type { AppViewEntry, BreakpointConfig, ElytraConfigInput, FieldRendererEntry, SidebarEntry } from '@elytracms/core/studio-core';
1
+ import type { BreakpointConfig, ElytraConfigInput } from '@elytracms/core/studio-core';
2
2
  import type { CollectionDef, RedirectRecord, RouteRecord } from '@elytracms/core/cms-core';
3
+ import type { SidebarNode } from '../../components/app-views/module';
4
+ import type { CustomFieldRenderer, StudioConfig } from '../../components/app-views/studio-config';
3
5
  import type { WorkspaceState } from './workspace-source';
4
6
  export { resolveWorkspaceState, WORKSPACE_ENV_VAR } from './workspace-source';
5
7
  export type { WorkspaceSources, WorkspaceState } from './workspace-source';
6
8
  export { persistedProjects, selectSwitcherProjects, unprovisionedProjects } from './switcher-source';
7
9
  export type { SwitcherEntry, SwitcherSourceInput } from './switcher-source';
8
10
  /**
9
- * Mount the studio against a project config (EC-223 mount contract). The
10
- * consumer's entry point does `import config from './elytra.config'` and calls
11
- * this — an explicit init, NOT an `import.meta.glob` autoload that reaches into
12
- * the consumer's filesystem (Oli, 2026-06-19: "there's always an entry point that
13
- * can trigger a function call"). This is the config-injection responsibility of
14
- * the eventual `createStudio(config)` package export; it seeds the workspace state
15
- * every later query (`getActiveProjectSlug`, `resolveDeployment*`) reads.
11
+ * Mount the studio against a project config (EC-223 mount contract) and an
12
+ * optional studio-only customization config (EC-236). The consumer's entry point
13
+ * does `import config from './elytra.config'` + `import studio from
14
+ * './studio.config'` and calls this — an explicit init, NOT an `import.meta.glob`
15
+ * autoload that reaches into the consumer's filesystem (Oli, 2026-06-19: "there's
16
+ * always an entry point that can trigger a function call").
17
+ *
18
+ * Two configs, deliberately: `config` (`elytra.config.ts`) is the project
19
+ * definition, shared with the `frontend/` delivery bundle and projected to data;
20
+ * `studio` (`studio.config.tsx` via `defineStudio`) is live builder UI code — the
21
+ * section rail, custom field renderers, branding — that must NEVER reach delivery.
22
+ * Keeping them separate is the customization boundary principle in the mount
23
+ * signature. Seeds the workspace state + the studio-config holder every later
24
+ * query (`getActiveProjectSlug`, `resolveDeployment*`) reads.
16
25
  */
17
- export declare function createStudio(config: ElytraConfigInput): void;
26
+ export declare function createStudio(config: ElytraConfigInput, studio?: StudioConfig): void;
27
+ /** Test seam: set/clear the studio customization config. Never used by app code. */
28
+ export declare function setStudioConfigForTests(next: StudioConfig | null): void;
18
29
  /** Set the active project slug; returns whether the resolved deployment changed. */
19
30
  export declare function setActiveProjectSlug(slug: string | null): boolean;
20
31
  export declare function getActiveProjectSlug(): string | null;
@@ -73,24 +84,21 @@ export declare function resolveDeploymentCollections(): CollectionDef[];
73
84
  */
74
85
  export declare function resolveDeploymentBreakpoints(): BreakpointConfig[] | undefined;
75
86
  /**
76
- * The custom field renderers of the active config project (EC-238). Config-sourced
77
- * live React code (omitted from the serializable projection); `[]` when none — the
78
- * document editor then uses its built-in control for every field type.
87
+ * The custom field renderers from the studio config (EC-238): control name →
88
+ * component. Studio-only live code (from `defineStudio`, never the project schema
89
+ * or the delivery bundle); `undefined` when none — the document editor then uses
90
+ * its built-in control for every field. Studio-wide (not per-project): the rail and
91
+ * renderers customize the studio, not one backend.
79
92
  */
80
- export declare function resolveDeploymentFieldRenderers(): FieldRendererEntry[];
93
+ export declare function resolveDeploymentFieldRenderers(): Record<string, CustomFieldRenderer> | undefined;
81
94
  /**
82
- * The custom app-view modules of the active config project (EC-239). Config-sourced
83
- * live React modules (omitted from the serializable projection); `[]` when none —
84
- * the studio then mounts only its default module registry (CMS/Routes/Assets/…).
95
+ * The configurable section rail from the studio config (EC-237): an ordered list of
96
+ * `nav.*` nodes (views / groups / separators). Studio-only live code (carries view
97
+ * objects + icons); `undefined` when none — the studio then derives the default
98
+ * rail from its built-in views. Invalid entries surface as visible issues at
99
+ * resolution, never silent.
85
100
  */
86
- export declare function resolveDeploymentAppViews(): AppViewEntry[];
87
- /**
88
- * The configurable sidebar of the active config project (EC-237). Config-sourced
89
- * live entries (carry icons; omitted from the projection); `undefined` when the
90
- * project declares none — the studio then derives the default rail from its module
91
- * registry (declared-but-invalid entries surface as visible issues, never silent).
92
- */
93
- export declare function resolveDeploymentSidebar(): SidebarEntry[] | undefined;
101
+ export declare function resolveDeploymentSidebar(): SidebarNode[] | undefined;
94
102
  /**
95
103
  * The resolved URL routes of the active config project (EC-207, "routes as
96
104
  * code"). Routes are config-sourced: the studio reads the materialized
@@ -3,20 +3,33 @@ import { resolveWorkspaceState } from './workspace-source';
3
3
  export { resolveWorkspaceState, WORKSPACE_ENV_VAR } from './workspace-source';
4
4
  export { persistedProjects, selectSwitcherProjects, unprovisionedProjects } from './switcher-source';
5
5
  let cached = null;
6
- /**
7
- * Mount the studio against a project config (EC-223 mount contract). The
8
- * consumer's entry point does `import config from './elytra.config'` and calls
9
- * this — an explicit init, NOT an `import.meta.glob` autoload that reaches into
10
- * the consumer's filesystem (Oli, 2026-06-19: "there's always an entry point that
11
- * can trigger a function call"). This is the config-injection responsibility of
12
- * the eventual `createStudio(config)` package export; it seeds the workspace state
13
- * every later query (`getActiveProjectSlug`, `resolveDeployment*`) reads.
14
- */
15
- export function createStudio(config) {
6
+ let studioConfig = null;
7
+ /**
8
+ * Mount the studio against a project config (EC-223 mount contract) and an
9
+ * optional studio-only customization config (EC-236). The consumer's entry point
10
+ * does `import config from './elytra.config'` + `import studio from
11
+ * './studio.config'` and calls this — an explicit init, NOT an `import.meta.glob`
12
+ * autoload that reaches into the consumer's filesystem (Oli, 2026-06-19: "there's
13
+ * always an entry point that can trigger a function call").
14
+ *
15
+ * Two configs, deliberately: `config` (`elytra.config.ts`) is the project
16
+ * definition, shared with the `frontend/` delivery bundle and projected to data;
17
+ * `studio` (`studio.config.tsx` via `defineStudio`) is live builder UI code — the
18
+ * section rail, custom field renderers, branding — that must NEVER reach delivery.
19
+ * Keeping them separate is the customization boundary principle in the mount
20
+ * signature. Seeds the workspace state + the studio-config holder every later
21
+ * query (`getActiveProjectSlug`, `resolveDeployment*`) reads.
22
+ */
23
+ export function createStudio(config, studio) {
16
24
  cached = resolveWorkspaceState({
17
25
  fileModule: { default: config },
18
26
  fileName: 'elytra.config.ts',
19
27
  });
28
+ studioConfig = studio ?? null;
29
+ }
30
+ /** Test seam: set/clear the studio customization config. Never used by app code. */
31
+ export function setStudioConfigForTests(next) {
32
+ studioConfig = next;
20
33
  }
21
34
  /**
22
35
  * The active project slug (EC-179, vision AD-8). One project per backend, so the
@@ -157,29 +170,24 @@ export function resolveDeploymentBreakpoints() {
157
170
  return activeConfigProject()?.breakpoints;
158
171
  }
159
172
  /**
160
- * The custom field renderers of the active config project (EC-238). Config-sourced
161
- * live React code (omitted from the serializable projection); `[]` when none — the
162
- * document editor then uses its built-in control for every field type.
173
+ * The custom field renderers from the studio config (EC-238): control name →
174
+ * component. Studio-only live code (from `defineStudio`, never the project schema
175
+ * or the delivery bundle); `undefined` when none — the document editor then uses
176
+ * its built-in control for every field. Studio-wide (not per-project): the rail and
177
+ * renderers customize the studio, not one backend.
163
178
  */
164
179
  export function resolveDeploymentFieldRenderers() {
165
- return activeConfigProject()?.fieldRenderers ?? [];
166
- }
167
- /**
168
- * The custom app-view modules of the active config project (EC-239). Config-sourced
169
- * live React modules (omitted from the serializable projection); `[]` when none —
170
- * the studio then mounts only its default module registry (CMS/Routes/Assets/…).
171
- */
172
- export function resolveDeploymentAppViews() {
173
- return activeConfigProject()?.appViews ?? [];
180
+ return studioConfig?.fieldRenderers;
174
181
  }
175
182
  /**
176
- * The configurable sidebar of the active config project (EC-237). Config-sourced
177
- * live entries (carry icons; omitted from the projection); `undefined` when the
178
- * project declares none — the studio then derives the default rail from its module
179
- * registry (declared-but-invalid entries surface as visible issues, never silent).
183
+ * The configurable section rail from the studio config (EC-237): an ordered list of
184
+ * `nav.*` nodes (views / groups / separators). Studio-only live code (carries view
185
+ * objects + icons); `undefined` when none — the studio then derives the default
186
+ * rail from its built-in views. Invalid entries surface as visible issues at
187
+ * resolution, never silent.
180
188
  */
181
189
  export function resolveDeploymentSidebar() {
182
- return activeConfigProject()?.sidebar;
190
+ return studioConfig?.sidebar;
183
191
  }
184
192
  /**
185
193
  * The resolved URL routes of the active config project (EC-207, "routes as