@elytracms/studio 0.0.80 → 0.0.81

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 (97) hide show
  1. package/convex/contract-parity.test.ts +1 -1
  2. package/convex/functions.ts +13 -3
  3. package/convex/http-routes.test.ts +143 -0
  4. package/convex/http.ts +19 -9
  5. package/convex/schema.ts +0 -19
  6. package/convex/users.ts +1 -1
  7. package/dist/components/app-views/default-modules.d.ts +141 -44
  8. package/dist/components/app-views/default-modules.js +229 -65
  9. package/dist/components/app-views/default-modules.js.map +1 -1
  10. package/dist/components/app-views/module.d.ts +38 -0
  11. package/dist/components/app-views/module.js.map +1 -1
  12. package/dist/components/app-views/sidebar.d.ts +5 -2
  13. package/dist/components/app-views/sidebar.js +16 -5
  14. package/dist/components/app-views/sidebar.js.map +1 -1
  15. package/dist/components/app-views/studio-config.d.ts +27 -0
  16. package/dist/components/app-views/studio-config.js.map +1 -1
  17. package/dist/components/builder/canvas-render-context.js +7 -1
  18. package/dist/components/builder/canvas-render-context.js.map +1 -1
  19. package/dist/components/builder/reuse-context.js +7 -1
  20. package/dist/components/builder/reuse-context.js.map +1 -1
  21. package/dist/components/builder/reuse.d.ts +94 -16
  22. package/dist/components/builder/reuse.js +201 -19
  23. package/dist/components/builder/reuse.js.map +1 -1
  24. package/dist/components/cms/bulk-actions.js +3 -1
  25. package/dist/components/cms/bulk-actions.js.map +1 -1
  26. package/dist/components/cms/cell-edit.js +6 -2
  27. package/dist/components/cms/cell-edit.js.map +1 -1
  28. package/dist/components/cms/cms-store.d.ts +40 -13
  29. package/dist/components/cms/cms-store.js +50 -22
  30. package/dist/components/cms/cms-store.js.map +1 -1
  31. package/dist/components/cms/collection-table.js +2 -2
  32. package/dist/components/cms/collection-table.js.map +1 -1
  33. package/dist/components/cms/detail-editor.js +11 -5
  34. package/dist/components/cms/detail-editor.js.map +1 -1
  35. package/dist/components/cms/field-control.js +2 -1
  36. package/dist/components/cms/field-control.js.map +1 -1
  37. package/dist/components/cms/field-renderer.js +8 -1
  38. package/dist/components/cms/field-renderer.js.map +1 -1
  39. package/dist/components/cms/import-export.js +2 -1
  40. package/dist/components/cms/import-export.js.map +1 -1
  41. package/dist/components/cms/issues-pane.d.ts +4 -2
  42. package/dist/components/cms/issues-pane.js +6 -4
  43. package/dist/components/cms/issues-pane.js.map +1 -1
  44. package/dist/components/components/transfer-dialog.js +10 -12
  45. package/dist/components/components/transfer-dialog.js.map +1 -1
  46. package/dist/components/project-list.d.ts +9 -3
  47. package/dist/components/project-list.js +11 -5
  48. package/dist/components/project-list.js.map +1 -1
  49. package/dist/components/project-switcher.d.ts +3 -1
  50. package/dist/components/project-switcher.js +4 -2
  51. package/dist/components/project-switcher.js.map +1 -1
  52. package/dist/components/redirects/redirects-store.d.ts +21 -0
  53. package/dist/components/redirects/redirects-store.js +17 -4
  54. package/dist/components/redirects/redirects-store.js.map +1 -1
  55. package/dist/components/section-nav.js +102 -4
  56. package/dist/components/section-nav.js.map +1 -1
  57. package/dist/customization.d.ts +7 -0
  58. package/dist/customization.js.map +1 -1
  59. package/dist/lib/data/collection-content.d.ts +6 -1
  60. package/dist/lib/data/collection-content.js +50 -12
  61. package/dist/lib/data/collection-content.js.map +1 -1
  62. package/dist/lib/i18n/messages.de.js +132 -0
  63. package/dist/lib/i18n/messages.de.js.map +1 -1
  64. package/dist/lib/i18n/messages.en.d.ts +103 -0
  65. package/dist/lib/i18n/messages.en.js +146 -0
  66. package/dist/lib/i18n/messages.en.js.map +1 -1
  67. package/dist/lib/locale/binding-fallback.d.ts +8 -1
  68. package/dist/lib/locale/binding-fallback.js +8 -1
  69. package/dist/lib/locale/binding-fallback.js.map +1 -1
  70. package/dist/lib/locale/locale-store.d.ts +41 -9
  71. package/dist/lib/locale/locale-store.js +34 -15
  72. package/dist/lib/locale/locale-store.js.map +1 -1
  73. package/dist/lib/locale/locale-switcher.d.ts +3 -1
  74. package/dist/lib/locale/locale-switcher.js +4 -2
  75. package/dist/lib/locale/locale-switcher.js.map +1 -1
  76. package/dist/lib/locale/use-studio-locale.d.ts +9 -4
  77. package/dist/lib/locale/use-studio-locale.js.map +1 -1
  78. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  79. package/dist/lib/persistence/convex-adapter.js +0 -55
  80. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  81. package/dist/lib/persona/labels.d.ts +42 -1
  82. package/dist/lib/persona/labels.js +57 -0
  83. package/dist/lib/persona/labels.js.map +1 -1
  84. package/dist/lib/routes/route-issues.d.ts +9 -0
  85. package/dist/lib/routes/route-issues.js +117 -0
  86. package/dist/lib/routes/route-issues.js.map +1 -0
  87. package/dist/lib/workspace/config-issues.d.ts +54 -0
  88. package/dist/lib/workspace/config-issues.js +70 -0
  89. package/dist/lib/workspace/config-issues.js.map +1 -0
  90. package/dist/lib/workspace/index.d.ts +9 -0
  91. package/dist/lib/workspace/index.js +11 -0
  92. package/dist/lib/workspace/index.js.map +1 -1
  93. package/dist/lib/workspace/switcher-source.d.ts +20 -3
  94. package/dist/lib/workspace/switcher-source.js.map +1 -1
  95. package/package.json +2 -2
  96. package/convex/cliTokens.ts +0 -63
  97. package/convex/sync.ts +0 -297
@@ -1 +1 @@
1
- {"version":3,"file":"default-modules.js","sourceRoot":"","sources":["../../../src/components/app-views/default-modules.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Blocks,\n Database,\n FileCog,\n Image,\n LayoutGrid,\n Puzzle,\n Settings as SettingsIcon,\n Signpost,\n Users,\n} from 'lucide-react'\nimport {\n assetCollection,\n extendCollectionDef,\n isSingleton,\n resolveSingletonDocument,\n sampleDocuments,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n CollectionFieldOverride,\n CollectionFormMeta,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { reconcileSeedDocumentLocalization } from '@elytracms/core/persistence'\nimport type { Project, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { NavigateOnce, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Text } from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport type { InstantiatedKit } from '@elytracms/core/starter-kits'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { resolveDeploymentCollections, resolveDeploymentLocaleConfig } from '@elytracms/studio/lib/workspace'\nimport type { HydratedCms } from '@elytracms/studio/lib/operations'\nimport {\n cmsDeepLink,\n componentsDeepLink,\n usersDeepLink,\n redirectsDeepLink,\n assetsDeepLink,\n groupNode,\n separatorNode,\n viewNode,\n type AppViewContext,\n type AppViewDeepLink,\n type AppViewModule,\n type NavPermission,\n type SidebarViewNode,\n} from './module'\nimport { SectionSurface } from '../section-surface'\nimport { SectionStateView } from '../section-state'\nimport { SettingsForm } from '../settings-form'\nimport { AssetsModule } from '../assets/assets-module'\nimport { UsersModule } from '../users/users-module'\nimport { RedirectsModule } from '../redirects/redirects-module'\nimport { redirectsCollectionDef } from '../redirects/redirects-schema'\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 } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { AiCmsPanel } from '../ai/ai-cms-panel'\nimport { focusCollectionList } from '../cms/collection-focus'\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-237/239 — the studio's BUILT-IN views + the consumer-facing `nav` namespace,\n * an instance of the EC-236 customization seam.\n *\n * Each built-in surface (the former `StudioSectionResolver` case body) is promoted\n * to a self-contained {@link AppViewModule} factory. The `nav` namespace is the\n * single, typed builder a consuming repo's `studio.config.tsx` uses to declare its\n * section rail — `nav.cms(...)`, `nav.group(...)`, `nav.view(...)` — passing the\n * view objects (and a focus config) DIRECTLY, never a string id. The CMS surface\n * is a factory ({@link cmsView}) so a focused view (\"Catalog\", \"Categories\") is the\n * SAME surface instantiated with config, not a copy — the EC-239 reuse proof.\n */\n\n// --- The CMS surface, reusable via a config factory (the EC-239 reuse seam) ---\n\n/** Config for a CMS-backed view — `collections`/`collection` focus the lens (EC-239). */\nexport interface CmsViewConfig {\n /** Explicit stable id (the route segment). Omitted → derived from the focus. */\n id?: string\n label?: string\n description?: string\n icon?: LucideIcon\n /**\n * Scope the rail auto-expansion (EC-293) to these collection ids, in order. The\n * parent expands into one focused-lens child per id; the parent index shows the\n * same subset. Omitted → every collection. Ignored for a single-collection lens.\n */\n collections?: readonly string[]\n /** Single-collection lens: a LEAF rail item that opens straight into this collection. */\n collection?: string\n /**\n * EC-293 escape hatch: explicit rail children for this CMS parent, overriding the\n * default per-collection auto-expansion. Ignored for a single-collection lens.\n */\n children?: SidebarViewNode[]\n /** The availability section this view rides on (EC-221); defaults to `cms`. */\n availabilitySection?: SectionId\n /** OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18). See {@link BuiltinViewConfig.visible}. */\n visible?: NavPermission\n}\n\n/** Common label/icon overrides for a built-in view (`nav.redirects({ label: 'URLs' })`). */\nexport interface BuiltinViewConfig {\n label?: string\n icon?: LucideIcon\n /**\n * OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18): hide this\n * rail item for principals it returns `false` for; absent → always visible. The\n * policy (which roles) lives in the consuming project, never the engine. E.g.\n * `nav.users({ visible: ({ role }) => role === 'admin' })`.\n */\n visible?: NavPermission\n}\n\n/**\n * Config for the **redirects** rail (EC-311) — a built-in view whose engine-owned\n * schema is customizable. Beyond the shared label/icon/visible, a mount may rename\n * the rail (`form`) and **override built-in fields by equal name** (`fields`):\n * naming, permissions (`form.readOnly`), rendering (`form.control` → `fieldRenderers`)\n * and validation. The canonical `from`/`to`/`permanent`/`enabled` names+types are\n * contract-locked (a type change on one is a loud config error), but a *net-new*\n * field is allowed (EC-311 Tier 2): it's appended and persists in the redirect\n * record's `meta` bag — editorial only, so it never reaches delivery/routing.\n */\nexport interface RedirectsViewConfig extends BuiltinViewConfig {\n /** Rename the rail (e.g. `{ label: 'URLs' }`). */\n form?: Partial<CollectionFormMeta>\n /** Override built-in redirect fields, matched by `name`. */\n fields?: readonly CollectionFieldOverride[]\n}\n\n/**\n * Config for the **assets** rail (EC-319), customizable by the SAME EC-311\n * mechanism as redirects (`form`/`fields`) — but the BASE schema it overrides is\n * different in kind: redirects' base is an engine-owned, fixed `CollectionDef`\n * known at config-build time; the assets base is the DEPLOYMENT's own\n * `kind:'asset'` collection (a project's `cms/collections/asset.ts`), only\n * knowable once the deployment resolves (hydrated session → kit → playground\n * config — see {@link resolveAssetsSchema}). So the merge runs at RENDER time\n * (inside `assetsView`'s `CmsSessionGate`), not at `nav.assets(config)` call\n * time: a locked-violation override (e.g. changing a declared field's type)\n * surfaces the first time the Assets section renders, not at `defineStudio`\n * mount — the one place this rail's customization contract differs from\n * redirects', a direct consequence of its base schema being project-declared.\n */\nexport interface AssetsViewConfig extends BuiltinViewConfig {\n /** Rename the rail (e.g. `{ label: 'Media' }`). */\n form?: Partial<CollectionFormMeta>\n /** Override the project's declared asset fields, matched by `name`. */\n fields?: readonly CollectionFieldOverride[]\n}\n\n/** Config for a fully custom view (`nav.view({...})`) — the net-new surface escape hatch. */\nexport interface CustomViewConfig {\n /** Explicit stable id (the route segment). Omitted → a slug of the label. */\n id?: string\n label: string\n description?: string\n icon?: LucideIcon\n render: (ctx: AppViewContext) => ReactNode\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n availabilitySection?: SectionId\n /** OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18). See {@link BuiltinViewConfig.visible}. */\n visible?: NavPermission\n}\n\nfunction slugify(label: string): string {\n return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'view'\n}\n\n/** Derive a stable, deterministic id from the CMS focus (overridable via `config.id`). */\nfunction cmsViewId(config: CmsViewConfig): string {\n if (config.id) return config.id\n if (config.collection) return `cms:${config.collection}`\n if (config.collections && config.collections.length > 0) {\n return `cms:${[...config.collections].join('-')}`\n }\n return 'cms'\n}\n\n/**\n * Build a CMS view — the default `cms` view, or a focused instance. Every instance\n * shares one surface ({@link CmsScreen}); `collections` narrows the sidebar + index\n * to a subset, `collection` is a single-collection lens that opens straight into\n * that collection's list. The id is derived from the focus (deterministic, stable\n * across rail reorders) unless `id` is given — so two focused views never collide.\n */\nexport function cmsView(config: CmsViewConfig = {}): AppViewModule {\n const single = config.collection\n const focus = single ? [single] : config.collections\n return {\n id: cmsViewId(config),\n // Lazy label/description (EC-241): the sidebar is built when the consumer's\n // `studio.config` module loads — BEFORE `createStudio` seeds the i18n config — so\n // resolving here would bake English. A getter defers resolution to render, when\n // the config is present and the browser locale is known.\n get label() {\n return config.label ?? translate('nav.cms.label')\n },\n get description() {\n return config.description ?? translate('nav.cms.description')\n },\n icon: config.icon ?? Database,\n availabilitySection: config.availabilitySection ?? 'cms',\n // A single-collection lens routes `[]`→that collection, `[d]`→its document;\n // an unfocused / multi-collection view uses the full index/collection/document arity.\n parsePath: single\n ? (segments) =>\n segments.length === 0\n ? { collectionId: single, documentId: null }\n : segments.length === 1\n ? { collectionId: single, documentId: segments[0]! }\n : null\n : cmsDeepLink,\n render: (ctx) => (\n <CmsScreen\n project={ctx.project}\n collectionId={ctx.deepLink.collectionId ?? null}\n documentId={ctx.deepLink.documentId ?? null}\n state={ctx.search.state}\n field={ctx.search.field}\n focusCollections={focus}\n />\n ),\n }\n}\n\n/**\n * A focused-lens rail child for one collection (EC-293) — the SAME `cms:<id>` view a\n * single-collection `nav.cms({ collection })` produces, carrying the collection's own\n * label + a singleton-aware icon (the former `CollectionsSidebar` row, lifted into\n * the rail). CMS auto-expansion maps the project's collections through this.\n */\nfunction cmsCollectionLeaf(collection: CollectionDef): SidebarViewNode {\n const singleton = isSingleton(collection)\n return viewNode(\n cmsView({\n collection: collection.id,\n // Non-hook `localizedLabel` resolves the (possibly `{ en, de }`) collection label\n // against the active UI locale, so a German editor sees German collection names.\n label: localizedLabel(collection.form?.label, collection.id),\n icon: singleton ? FileCog : collection.kind === 'asset' ? Image : Database,\n }),\n )\n}\n\n/** The components registry browser view. */\nexport function componentsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'components',\n get label() {\n return config.label ?? translate('nav.components.label')\n },\n get description() {\n return translate('nav.components.description')\n },\n icon: config.icon ?? Blocks,\n availabilitySection: 'components',\n parsePath: componentsDeepLink,\n render: (ctx) => (\n <ComponentsScreen\n projectId={ctx.projectId}\n componentId={ctx.deepLink.componentId ?? null}\n state={ctx.search.state}\n />\n ),\n }\n}\n\n/**\n * The Redirects view (EC-303) — editorial redirects as DATA, an editor-owned CRUD\n * surface (create/edit/delete) over the portable list/edit surfaces. Routes are no\n * longer shown in the admin (they're code; native `next.config`/`vercel` own static\n * forwards, evaluated before these). Rides on the CMS availability (a content/\n * routing concern). `[]` is the list; `[redirectId]` opens that redirect's detail.\n */\nexport function redirectsView(config: RedirectsViewConfig = {}): AppViewModule {\n // EC-311: merge any field/label overrides over the engine-owned def ONCE, at config\n // build (config-as-code — a locked violation throws here, at `defineStudio` mount).\n // Tier 2: `additiveFields: 'allow'` — a net-new field is appended and persists in the\n // redirect record's `meta` bag (the canonical from/to/permanent/enabled stay locked).\n const schema = extendCollectionDef(redirectsCollectionDef, config, { additiveFields: 'allow' })\n return {\n id: 'redirects',\n get label() {\n // Prefer an explicit rail label, then a consumer-*customized* collection label,\n // else the localized catalog nav label. Never let the built-in English schema\n // label ('Redirects') preempt translation — compare against the built-in default.\n const customized = localizedLabel(schema.form?.label)\n const builtin = localizedLabel(redirectsCollectionDef.form?.label)\n return (\n config.label ??\n (customized && customized !== builtin ? customized : translate('nav.redirects.label'))\n )\n },\n get description() {\n return translate('nav.redirects.description')\n },\n icon: config.icon ?? Signpost,\n availabilitySection: 'cms',\n parsePath: redirectsDeepLink,\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title={translate('nav.redirects.label')} />\n ) : (\n <RedirectsModule\n projectId={ctx.projectId}\n redirectId={ctx.deepLink.redirectId ?? null}\n schema={schema}\n />\n ),\n }\n}\n\n/**\n * Resolve the assets rail's EFFECTIVE schema (EC-319/EC-311): the deployment's\n * OWN `kind:'asset'` collection — hydrated session → kit → playground config,\n * the exact same precedence {@link resolveCmsInitialCollections} gives every\n * other CMS collection — falling back to the engine's `assetCollection` fixture\n * only when nothing resolves (mirroring `CmsProvider`'s own fallback). A mount's\n * `nav.assets({ form, fields })` then merges over THAT base (EC-311, additive\n * fields allowed — a project's asset record already carries an open `meta` bag,\n * EC-301). Pure — no React — so it's unit-testable without `CmsSessionGate`.\n */\nexport function resolveAssetsSchema(\n collections: readonly CollectionDef[] | undefined,\n config: AssetsViewConfig,\n): CollectionDef {\n const base = collections?.find((c) => c.kind === 'asset') ?? assetCollection\n return extendCollectionDef(base, config, { additiveFields: 'allow' })\n}\n\n/**\n * The Assets view (EC-319) — the universal list (+ grid/list toggle) and a\n * bespoke per-asset detail surface, over the asset library store. `[]` is the\n * list; `[assetId]` opens that asset's detail (routed via `studioPath.asset`).\n * A `?asset=` deep link (EC-172 \"used by\" links) redirects straight to the\n * asset's detail — the surface those links used to scroll a card into and\n * auto-open, now a first-class route.\n */\nexport function assetsView(config: AssetsViewConfig = {}): AppViewModule {\n return {\n id: 'assets',\n get label() {\n return config.label ?? translate('nav.assets.label')\n },\n get description() {\n return translate('nav.assets.description')\n },\n icon: config.icon ?? Image,\n availabilitySection: 'assets',\n parsePath: assetsDeepLink,\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"assets\" forcedState={ctx.search.state}>\n {ctx.deepLink.assetId === null && ctx.search.asset ? (\n // EC-441: `NavigateOnce` — render-time redirects fire once per intent.\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: ctx.projectId, _splat: studioPath.asset(ctx.search.asset) }}\n replace\n />\n ) : (\n // The AssetsProvider is mounted at the project level (EC-195).\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(ctx.projectId)\n const schema = resolveAssetsSchema(resolveCmsInitialCollections(cms, kit), config)\n return (\n <AssetsModule\n projectId={ctx.projectId}\n assetId={ctx.deepLink.assetId ?? null}\n schema={schema}\n />\n )\n }}\n </CmsSessionGate>\n )}\n </SectionSurface>\n ),\n }\n}\n\n/**\n * The studio Users view (EC-240) — an admin-only roster of who has access to this\n * deployment and their role. A first-class section (its own rail entry), modeled\n * like the Assets library; the surface itself gates to admins and renders a\n * \"restricted\" state otherwise. Rides on the `settings` availability (an\n * admin/config concern, so it appears/hides with a project's settings surface).\n */\nexport function usersView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'users',\n get label() {\n return config.label ?? translate('nav.users.label')\n },\n get description() {\n return translate('nav.users.description')\n },\n icon: config.icon ?? Users,\n availabilitySection: 'users',\n // EC-240: `[]` is the roster; `[userId]` opens that member in the portable edit\n // shell (AD-19 reuse — the same `DetailEditor` body over the users source).\n parsePath: usersDeepLink,\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title={translate('nav.users.label')} />\n ) : (\n <UsersModule projectId={ctx.projectId} userId={ctx.deepLink.userId ?? null} />\n ),\n }\n}\n\n/** The extensions marketplace view. */\nexport function extensionsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'extensions',\n get label() {\n return config.label ?? translate('nav.extensions.label')\n },\n get description() {\n return translate('nav.extensions.description')\n },\n icon: config.icon ?? Puzzle,\n availabilitySection: 'extensions',\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title={translate('nav.extensions.label')} />\n ) : (\n <ExtensionsView />\n ),\n }\n}\n\n/** The project settings view. */\nexport function settingsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'settings',\n get label() {\n return config.label ?? translate('nav.settings.label')\n },\n get description() {\n return translate('nav.settings.description')\n },\n icon: config.icon ?? SettingsIcon,\n availabilitySection: 'settings',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"settings\" forcedState={ctx.search.state}>\n <SettingsForm project={ctx.project} />\n </SectionSurface>\n ),\n }\n}\n\n/** The legacy `builder` redirect — a hidden, routable view (so `/builder?page=` still lands). */\nexport function builderView(): AppViewModule {\n return {\n id: 'builder',\n label: 'Builder',\n description: 'Legacy builder route — redirects to the page document editor.',\n icon: Blocks,\n hidden: true,\n render: (ctx) => (\n // Retired surface (EC-187 slice 6): a page is a document in the `page`\n // collection. `?page=<id>` lands on that document; else the page list.\n // EC-441: `NavigateOnce` — render-time redirects fire once per intent.\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{\n projectId: ctx.projectId,\n _splat: ctx.search.page\n ? studioPath.cmsDocument('page', ctx.search.page)\n : studioPath.cmsCollection('page'),\n }}\n replace\n />\n ),\n }\n}\n\n/** Build a fully custom view (the `nav.view(...)` escape hatch). */\nexport function customView(config: CustomViewConfig): AppViewModule {\n return {\n id: config.id ?? slugify(config.label),\n label: config.label,\n description: config.description ?? config.label,\n icon: config.icon ?? LayoutGrid,\n render: config.render,\n ...(config.parsePath ? { parsePath: config.parsePath } : {}),\n ...(config.availabilitySection ? { availabilitySection: config.availabilitySection } : {}),\n }\n}\n\n/**\n * The studio's built-in views in canonical rail order (components · CMS · routes ·\n * assets · extensions · settings), plus the hidden `builder` redirect (routable but\n * unlisted). The default rail (no `defineStudio({ sidebar })`) is these as plain\n * entries; the router registry always includes them so deep-links resolve even\n * when a consumer's rail omits one.\n */\nexport const DEFAULT_VIEWS: readonly AppViewModule[] = [\n componentsView(),\n cmsView(),\n redirectsView(),\n assetsView(),\n extensionsView(),\n usersView(),\n settingsView(),\n builderView(),\n]\n\n/**\n * The `nav` namespace — the single typed builder for `studio.config.tsx`'s section\n * rail. Each call returns a {@link SidebarViewNode} (or a group / separator) that\n * carries its view DIRECTLY: `nav.cms({ collection })` is a focused CMS view,\n * `nav.view({ render })` a net-new surface, `nav.group(label, [...])` a header with\n * sub-views, `nav.separator()` a divider. No string ids, no `moduleId` — the rail\n * is the views.\n */\n/**\n * Forward the OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18)\n * from a `nav.*` config onto the built view, so gating is settable on EVERY nav\n * item — not just Users. Absent → the view is unchanged (always visible).\n */\nfunction withNavPermission(view: AppViewModule, config: { visible?: NavPermission }): AppViewModule {\n if (!config.visible) return view\n // Copy property DESCRIPTORS (not a spread) so a built-in view's lazy `label`/\n // `description` getters (EC-241) survive — a spread would invoke them at build time,\n // baking English before `createStudio` seeds the i18n config.\n return Object.defineProperties({} as AppViewModule, {\n ...Object.getOwnPropertyDescriptors(view),\n visible: { value: config.visible, enumerable: true, configurable: true, writable: true },\n })\n}\n\nexport const nav = {\n /**\n * The CMS view. `nav.cms()` / `nav.cms({ collections })` is a PARENT that nests its\n * collections in the rail (each a focused `cms:<id>` lens); `nav.cms({ collection })`\n * is a single-collection LEAF; `nav.cms({ children })` supplies explicit children.\n */\n cms: (config: CmsViewConfig = {}): SidebarViewNode => {\n const view = withNavPermission(cmsView(config), config)\n // A single-collection lens is a leaf — it opens that collection directly.\n if (config.collection) return viewNode(view)\n // Otherwise a parent that expands its collections as nested rail children (EC-293).\n const node: SidebarViewNode = { kind: 'view', view }\n if (config.children) node.children = config.children\n else\n node.expand = (collections) =>\n focusCollectionList(collections, config.collections).map(cmsCollectionLeaf)\n return node\n },\n /** The component registry browser. */\n components: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(componentsView(config), config)),\n /** The assets rail — the universal list/grid + detail (EC-319); built-in\n * fields customizable by name (EC-311), the same shape as `nav.redirects`. */\n assets: (config: AssetsViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(assetsView(config), config)),\n /** Editorial redirects (EC-303); built-in fields customizable by name (EC-311). */\n redirects: (config: RedirectsViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(redirectsView(config), config)),\n /** Project settings. */\n settings: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(settingsView(config), config)),\n /** The extensions marketplace. */\n extensions: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(extensionsView(config), config)),\n /** The admin users roster (EC-240). */\n users: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(usersView(config), config)),\n /** A fully custom view — pass your own `render` (and optional `parsePath`). */\n view: (config: CustomViewConfig): SidebarViewNode =>\n viewNode(withNavPermission(customView(config), config)),\n /** A labelled group of views (a header with sub-entries). */\n group: (label: string, children: SidebarViewNode[]) => groupNode(label, children),\n /** A divider between rail sections. */\n separator: () => separatorNode(),\n} as const\n\n// --- Surfaces (the view render bodies; CMS gains a focus lens) ---\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={translate('nav.components.label')} />\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 * The CMS schema source for a project mount (bug fix, playground-collections):\n * a hydrated session wins (Convex), then a kit's imported schema (EC-119), then —\n * in playground mode only — the active project's config-declared collections\n * (AD-11, `resolveDeploymentCollections`). Without this last fallback, a non-kit\n * playground project has no hydrated session (`null`, by design — see\n * `project-session.tsx`) and no kit, so `CmsProvider` fell through to the core's\n * hardcoded `sampleFilterableCollections` fixture instead of the project's own\n * on-disk schema. Convex mode's first-run `null` is untouched — it still seeds\n * from the fixture there, as before. `playground` defaults to the live\n * {@link isPlaygroundMode} but is overridable so this stays unit testable\n * without stubbing the env (same seam as `resolveBackendConfig`).\n */\nexport function resolveCmsInitialCollections(\n cms: HydratedCms | null,\n kit: InstantiatedKit | undefined,\n playground: boolean = isPlaygroundMode(),\n): CollectionDef[] | undefined {\n return cms?.collections ?? kit?.collections ?? (playground ? resolveDeploymentCollections() : undefined)\n}\n\n/**\n * The CMS documents source for a project mount (EC-424) — the document-side\n * twin of {@link resolveCmsInitialCollections}, same ladder: a hydrated session\n * wins (Convex), then a kit's imported documents, then — in playground mode\n * only, and only when a config project actually resolved collections — the\n * engine's own `sampleDocuments` fixture RECONCILED against them.\n *\n * Without this, a config project whose OWN schema diverges from the shared\n * fixture's localization (e.g. this repo's demo declares `post.slug` localized,\n * EC-315, while the engine's `postCollection` fixture deliberately does not)\n * left `CmsProvider` falling straight through to the un-reconciled\n * `sampleDocuments` default — the CMS surfaces' `initialCollections` already\n * reflected the config schema (via the fallback above), but `initialDocuments`\n * did not, so every seeded post showed EC-390's shape-aware validation's\n * `plain-value` warning + `missing-localized-value` error, standing noise a\n * project never actually wrote.\n *\n * This is the STATIC-baseline twin of `reconcileSeedDocumentLocalization`\n * (`@elytracms/core/persistence`, wired into the seeded persistence adapter for\n * Convex-mode/delivery reads): no adapter round-trip, so the EC-138/EC-320\n * synchronous-first-render contract (`project-session.tsx`: \"CMS surfaces keep\n * their fixture baseline\") is untouched. `collections` defaults to the SAME\n * resolution `resolveCmsInitialCollections` returns, so the two stay in lockstep\n * by construction; only reconciles when that resolved to a non-empty config\n * schema (env-only playground, with nothing config-declared, is unaffected).\n */\nexport function resolveCmsInitialDocuments(\n cms: HydratedCms | null,\n kit: InstantiatedKit | undefined,\n collections: CollectionDef[] | undefined = resolveCmsInitialCollections(cms, kit),\n): CmsDocument[] | undefined {\n if (cms) return cms.documents\n if (kit) return kit.documents\n if (!collections || collections.length === 0) return undefined\n const collectionsById = new Map(collections.map((collection) => [collection.id, collection]))\n return sampleDocuments.map((doc) => {\n const collection = collectionsById.get(doc.collection)\n return collection ? reconcileSeedDocumentLocalization(doc, collection) : doc\n })\n}\n\n/**\n * The CMS locale config for a project mount (EC-357 finding #3). Locales are\n * structure and structure is code: when the active `elytra.config.ts` project\n * declares locales, they drive the ambient locale switcher in EVERY mode. The old\n * wiring only applied the project's locale settings on a HYDRATED session, so a\n * playground boot — or a connected FIRST RUN with nothing persisted yet — fell\n * back to a kit's locales or the core's sample `en/de` fixture and the switcher\n * showed `EN·/DE` against a `locales: ['de']` config. Without a config project\n * (env-only wiring) the previous ladder is unchanged: hydrated project settings,\n * then the kit's locales, then the sample fixture (via `CmsProvider`'s default).\n * `configLocales` is a parameter purely so the ladder is unit-testable (same seam\n * pattern as {@link resolveCmsInitialCollections}).\n */\nexport function resolveCmsInitialLocaleConfig(\n cms: HydratedCms | null,\n kit: InstantiatedKit | undefined,\n project: Project,\n configLocales: LocaleConfig | undefined = resolveDeploymentLocaleConfig(),\n): LocaleConfig | undefined {\n if (configLocales) return configLocales\n if (cms) return { default: project.settings.defaultLocale, locales: [...project.settings.locales] }\n return kit?.localeConfig\n}\n\n/**\n * CMS section: hydrates the persisted (or kit-seeded) collections + documents, then\n * renders the active screen full-width (index, a collection table, or a document\n * editor). EC-293: the collection list lives in the section rail (nested under the\n * CMS view), so there is no in-surface collections sidebar. Forced states (?state=)\n * replace the whole surface. `focusCollections` (EC-239) narrows the index to a\n * subset, so a focused view (\"Catalog\") reuses this surface with config.\n */\nfunction CmsScreen({\n project,\n collectionId,\n documentId,\n state,\n field,\n focusCollections,\n}: {\n project: Project\n collectionId: string | null\n documentId: string | null\n state?: SectionState\n field?: string\n focusCollections?: readonly string[]\n}) {\n const projectId = project.id\n if (state && state !== 'working') {\n return <SectionStateView state={state} title={translate('nav.cms.label')} />\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 // EC-424: resolved once so `initialDocuments`' reconciliation (below)\n // stays in lockstep with the SAME collections `initialCollections` uses.\n const initialCollections = resolveCmsInitialCollections(cms, kit)\n return (\n <CmsProvider\n key={projectId}\n projectId={projectId}\n initialCollections={initialCollections}\n initialDocuments={resolveCmsInitialDocuments(cms, kit, initialCollections)}\n initialPublishedKeys={cms?.publishedKeys}\n initialTimestamps={cms?.timestamps}\n initialPublishTimestamps={cms?.publishTimestamps}\n initialChangedKeys={cms?.changedKeys}\n initialLocaleConfig={resolveCmsInitialLocaleConfig(cms, kit, project)}\n // EC-461: a hydrated mount (cms non-null — both modes now) seeds the\n // CURRENT persisted state, so staged docs are superseded by it; only\n // a static kit/fixture seed lets staged entries win.\n seedReflectsWrites={cms !== null}\n >\n <div className=\"flex h-full min-h-0 flex-col\">\n <CmsPersistErrorBanner />\n {/* EC-293: the collection list now lives in the section rail (nested\n under the CMS view), so the surface is full-width — the old 240px\n CollectionsSidebar aside is gone. EC-199: the AI assistant mounts as\n a right-hand sibling (both list + editor), so the content is a row. */}\n <div className=\"flex min-h-0 flex-1 overflow-hidden\">\n <div className=\"min-w-0 flex-1 overflow-hidden\">\n <CmsContent\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n field={field}\n focusCollections={focusCollections}\n />\n </div>\n <AiCmsPanel />\n </div>\n </div>\n </CmsProvider>\n )\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 focusCollections,\n}: {\n projectId: string\n collectionId: string | null\n documentId: string | null\n field?: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n\n if (collectionId === null) return <CmsIndex projectId={projectId} focusCollections={focusCollections} />\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.\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 {translate('appViews.singleton.opening', {\n label: localizedLabel(collection?.form?.label, collectionId),\n })}\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"]}
1
+ {"version":3,"file":"default-modules.js","sourceRoot":"","sources":["../../../src/components/app-views/default-modules.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Blocks,\n Database,\n FileCog,\n Image,\n LayoutGrid,\n Puzzle,\n Settings as SettingsIcon,\n Signpost,\n Users,\n} from 'lucide-react'\nimport {\n assetCollection,\n extendCollectionDef,\n isSingleton,\n resolveSingletonDocument,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n CollectionFieldOverride,\n CollectionFormMeta,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { CustomizationIssue, Project, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { NavigateOnce, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Text } from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport type { InstantiatedKit } from '@elytracms/core/starter-kits'\nimport { resolveDeploymentCollections, resolveDeploymentLocaleConfig } from '@elytracms/studio/lib/workspace'\nimport type { HydratedCms } from '@elytracms/studio/lib/operations'\nimport {\n cmsDeepLink,\n componentsDeepLink,\n usersDeepLink,\n redirectsDeepLink,\n assetsDeepLink,\n groupNode,\n separatorNode,\n viewNode,\n type AppViewContext,\n type AppViewDeepLink,\n type AppViewModule,\n type DerivedViewId,\n type NavPermission,\n type SidebarViewNode,\n} from './module'\nimport { SectionSurface } from '../section-surface'\nimport { SectionStateView } from '../section-state'\nimport { SettingsForm } from '../settings-form'\nimport { AssetsModule } from '../assets/assets-module'\nimport { UsersModule } from '../users/users-module'\nimport { RedirectsModule } from '../redirects/redirects-module'\nimport { redirectsCollectionDef } from '../redirects/redirects-schema'\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 } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { AiCmsPanel } from '../ai/ai-cms-panel'\nimport { focusCollectionList } from '../cms/collection-focus'\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-237/239 — the studio's BUILT-IN views + the consumer-facing `nav` namespace,\n * an instance of the EC-236 customization seam.\n *\n * Each built-in surface (the former `StudioSectionResolver` case body) is promoted\n * to a self-contained {@link AppViewModule} factory. The `nav` namespace is the\n * single, typed builder a consuming repo's `studio.config.tsx` uses to declare its\n * section rail — `nav.cms(...)`, `nav.group(...)`, `nav.view(...)` — passing the\n * view objects (and a focus config) DIRECTLY, never a string id. The CMS surface\n * is a factory ({@link cmsView}) so a focused view (\"Catalog\", \"Categories\") is the\n * SAME surface instantiated with config, not a copy — the EC-239 reuse proof.\n */\n\n// --- The CMS surface, reusable via a config factory (the EC-239 reuse seam) ---\n\n/** Config for a CMS-backed view — `collections`/`collection` focus the lens (EC-239). */\nexport interface CmsViewConfig {\n /** Explicit stable id (the route segment). Omitted → derived from the focus. */\n id?: string\n label?: string\n description?: string\n icon?: LucideIcon\n /**\n * Scope the rail auto-expansion (EC-293) to these collection ids, in order. The\n * parent expands into one focused-lens child per id; the parent index shows the\n * same subset. Omitted → every collection. Ignored for a single-collection lens.\n *\n * A ONE-ITEM array is legal and degrades to a working (if redundant) parent+child\n * pair — the parent takes the `cms-group:<id>` id so it cannot collide with its own\n * child (EC-468). For a single collection with no group heading, prefer the\n * {@link CmsViewConfig.collection} leaf form.\n */\n collections?: readonly string[]\n /** Single-collection lens: a LEAF rail item that opens straight into this collection. */\n collection?: string\n /**\n * EC-293 escape hatch: explicit rail children for this CMS parent, overriding the\n * default per-collection auto-expansion. Ignored for a single-collection lens.\n */\n children?: SidebarViewNode[]\n /** The availability section this view rides on (EC-221); defaults to `cms`. */\n availabilitySection?: SectionId\n /** OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18). See {@link BuiltinViewConfig.visible}. */\n visible?: NavPermission\n}\n\n/** Common label/icon overrides for a built-in view (`nav.redirects({ label: 'URLs' })`). */\nexport interface BuiltinViewConfig {\n label?: string\n icon?: LucideIcon\n /**\n * OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18): hide this\n * rail item for principals it returns `false` for; absent → always visible. The\n * policy (which roles) lives in the consuming project, never the engine. E.g.\n * `nav.users({ visible: ({ role }) => role === 'admin' })`.\n */\n visible?: NavPermission\n}\n\n/**\n * Config for the **redirects** rail (EC-311) — a built-in view whose engine-owned\n * schema is customizable. Beyond the shared label/icon/visible, a mount may rename\n * the rail (`form`) and **override built-in fields by equal name** (`fields`):\n * naming, permissions (`form.readOnly`), rendering (`form.control` → `fieldRenderers`)\n * and validation. The canonical `from`/`to`/`permanent`/`enabled` names+types are\n * contract-locked (a type change on one is a loud config error), but a *net-new*\n * field is allowed (EC-311 Tier 2): it's appended and persists in the redirect\n * record's `meta` bag — editorial only, so it never reaches delivery/routing.\n */\nexport interface RedirectsViewConfig extends BuiltinViewConfig {\n /** Rename the rail (e.g. `{ label: 'URLs' }`). */\n form?: Partial<CollectionFormMeta>\n /** Override built-in redirect fields, matched by `name`. */\n fields?: readonly CollectionFieldOverride[]\n}\n\n/**\n * Config for the **assets** rail (EC-319), customizable by the SAME EC-311\n * mechanism as redirects (`form`/`fields`) — but the BASE schema it overrides is\n * different in kind: redirects' base is an engine-owned, fixed `CollectionDef`\n * known at config-build time; the assets base is the DEPLOYMENT's own\n * `kind:'asset'` collection (a project's `cms/collections/asset.ts`), only\n * knowable once the deployment resolves (hydrated session → kit → playground\n * config — see {@link resolveAssetsSchema}). So the merge runs at RENDER time\n * (inside `assetsView`'s `CmsSessionGate`), not at `nav.assets(config)` call\n * time: a locked-violation override (e.g. changing a declared field's type)\n * surfaces the first time the Assets section renders, not at `defineStudio`\n * mount — the one place this rail's customization contract differs from\n * redirects', a direct consequence of its base schema being project-declared.\n */\nexport interface AssetsViewConfig extends BuiltinViewConfig {\n /** Rename the rail (e.g. `{ label: 'Media' }`). */\n form?: Partial<CollectionFormMeta>\n /** Override the project's declared asset fields, matched by `name`. */\n fields?: readonly CollectionFieldOverride[]\n}\n\n/** Config for a fully custom view (`nav.view({...})`) — the net-new surface escape hatch. */\nexport interface CustomViewConfig {\n /** Explicit stable id (the route segment). Omitted → a slug of the label. */\n id?: string\n label: string\n description?: string\n icon?: LucideIcon\n render: (ctx: AppViewContext) => ReactNode\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n availabilitySection?: SectionId\n /** OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18). See {@link BuiltinViewConfig.visible}. */\n visible?: NavPermission\n}\n\nfunction slugify(label: string): string {\n return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'view'\n}\n\n/**\n * The auto-parent namespace for a ONE-ITEM `collections` group (EC-468).\n *\n * A PREFIX, not a suffix, and deliberately so: every other derived CMS id starts with\n * `cms:`, so the 4th character (`-` here, `:` there) makes the two namespaces disjoint\n * BY CONSTRUCTION — no collection id, whatever it contains, can forge a `cms-group:`\n * id, whereas a suffix marker (`cms:page:group`) would be forged by a collection\n * literally named `page:group` (`collectionDefSchema.id` is `z.string().min(1)` —\n * nothing reserves `:` or `-`).\n */\nconst CMS_GROUP_ID_PREFIX = 'cms-group:'\n\n/**\n * Derive a stable, deterministic id from the CMS focus (overridable via `config.id`).\n *\n * The id is the route segment, the rail's active-state key AND the override key\n * (EC-311), so it must be injective: two focuses deriving one id means\n * `resolveOverrides` drops the later view. The derived shapes:\n *\n * | focus | id |\n * | ---------------------------- | ------------------------- |\n * | none | `cms` |\n * | `collection: 'page'` (leaf) | `cms:page` |\n * | `collections: ['a','b']` | `cms:a-b` |\n * | `collections: ['page']` | `cms-group:page` |\n *\n * EC-468 — why the one-item group is its own namespace: `cms:${join(L)}` of a\n * single-element list IS that element, so the auto-generated PARENT took the very id\n * its own auto-expanded child derives (`cmsCollectionLeaf` → `cmsView({ collection })`).\n * Both landed in the one flat override list `resolveSidebar` feeds `resolveOverrides`,\n * so the child was dropped with a `duplicate-override-id` error and the parent rendered\n * childless — a forced collision no config could avoid. Nothing else changes: the leaf\n * and the multi-collection join keep their exact ids (consumer rails route on them).\n *\n * EC-477 — `-` is a SEPARATOR here, and a collection id may legally contain one, so a\n * collection literally named `a-b` derives the same `cms:a-b` as a `['a','b']` group.\n * That ambiguity is NOT closed by constraining collection ids (a permanent, repo-wide\n * naming ban to fix a two-config coincidence, when hyphenated ids are ordinary — see\n * the task's Done); it is DETECTED where both facts are known, by\n * {@link checkSidebarIdKeyspace}, which raises a `derived-id-collision` the moment a\n * group's join equals a real collection id. The escape hatch is unchanged: an explicit\n * `nav.cms({ id })` always wins, and the derivation below is untouched — consumer rails\n * route on these ids.\n */\nexport function cmsViewId(config: CmsViewConfig): string {\n if (config.id) return config.id\n if (config.collection) return `cms:${config.collection}`\n const collections = config.collections\n if (collections && collections.length > 0) {\n if (collections.length === 1) return `${CMS_GROUP_ID_PREFIX}${collections[0]}`\n return `cms:${joinCollectionIds(collections)}`\n }\n return 'cms'\n}\n\n/** The multi-collection focus join — ONE definition, so the id and the EC-477 check cannot drift. */\nfunction joinCollectionIds(collections: readonly string[]): string {\n return [...collections].join('-')\n}\n\n/**\n * The id provenance a CMS focus carries for diagnostics (EC-477) — set only for the one\n * derived shape that is genuinely ambiguous: a MULTI-collection join. An explicit `id`,\n * a single-collection leaf and the one-item `cms-group:` parent are all unambiguous by\n * construction (EC-468), so they carry nothing and are never diagnosed.\n */\nfunction cmsDerivedId(config: CmsViewConfig): DerivedViewId | undefined {\n if (config.id || config.collection) return undefined\n const collections = config.collections\n if (!collections || collections.length < 2) return undefined\n return { from: 'collection-join', collections: [...collections] }\n}\n\n/**\n * Build a CMS view — the default `cms` view, or a focused instance. Every instance\n * shares one surface ({@link CmsScreen}); `collections` narrows the sidebar + index\n * to a subset, `collection` is a single-collection lens that opens straight into\n * that collection's list. The id is derived from the focus (deterministic, stable\n * across rail reorders) unless `id` is given — see {@link cmsViewId} for the derived\n * shapes and the one keyspace ambiguity that survives.\n */\nexport function cmsView(config: CmsViewConfig = {}): AppViewModule {\n const single = config.collection\n const focus = single ? [single] : config.collections\n const derivedId = cmsDerivedId(config)\n return {\n id: cmsViewId(config),\n ...(derivedId ? { derivedId } : {}),\n // Lazy label/description (EC-241): the sidebar is built when the consumer's\n // `studio.config` module loads — BEFORE `createStudio` seeds the i18n config — so\n // resolving here would bake English. A getter defers resolution to render, when\n // the config is present and the browser locale is known.\n get label() {\n return config.label ?? translate('nav.cms.label')\n },\n get description() {\n return config.description ?? translate('nav.cms.description')\n },\n icon: config.icon ?? Database,\n availabilitySection: config.availabilitySection ?? 'cms',\n // A single-collection lens routes `[]`→that collection, `[d]`→its document;\n // an unfocused / multi-collection view uses the full index/collection/document arity.\n parsePath: single\n ? (segments) =>\n segments.length === 0\n ? { collectionId: single, documentId: null }\n : segments.length === 1\n ? { collectionId: single, documentId: segments[0]! }\n : null\n : cmsDeepLink,\n render: (ctx) => (\n <CmsScreen\n project={ctx.project}\n collectionId={ctx.deepLink.collectionId ?? null}\n documentId={ctx.deepLink.documentId ?? null}\n state={ctx.search.state}\n field={ctx.search.field}\n focusCollections={focus}\n />\n ),\n }\n}\n\n/**\n * A focused-lens rail child for one collection (EC-293) — the SAME `cms:<id>` view a\n * single-collection `nav.cms({ collection })` produces, carrying the collection's own\n * label + a singleton-aware icon (the former `CollectionsSidebar` row, lifted into\n * the rail). CMS auto-expansion maps the project's collections through this.\n */\nfunction cmsCollectionLeaf(collection: CollectionDef): SidebarViewNode {\n const singleton = isSingleton(collection)\n return viewNode(\n cmsView({\n collection: collection.id,\n // Non-hook `localizedLabel` resolves the (possibly `{ en, de }`) collection label\n // against the active UI locale, so a German editor sees German collection names.\n label: localizedLabel(collection.form?.label, collection.id),\n icon: singleton ? FileCog : collection.kind === 'asset' ? Image : Database,\n }),\n )\n}\n\n/** The components registry browser view. */\nexport function componentsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'components',\n get label() {\n return config.label ?? translate('nav.components.label')\n },\n get description() {\n return translate('nav.components.description')\n },\n icon: config.icon ?? Blocks,\n availabilitySection: 'components',\n parsePath: componentsDeepLink,\n render: (ctx) => (\n <ComponentsScreen\n projectId={ctx.projectId}\n componentId={ctx.deepLink.componentId ?? null}\n state={ctx.search.state}\n />\n ),\n }\n}\n\n/**\n * The Redirects view (EC-303) — editorial redirects as DATA, an editor-owned CRUD\n * surface (create/edit/delete) over the portable list/edit surfaces. Routes are no\n * longer shown in the admin (they're code; native `next.config`/`vercel` own static\n * forwards, evaluated before these). Rides on the CMS availability (a content/\n * routing concern). `[]` is the list; `[redirectId]` opens that redirect's detail.\n */\nexport function redirectsView(config: RedirectsViewConfig = {}): AppViewModule {\n // EC-311: merge any field/label overrides over the engine-owned def ONCE, at config\n // build (config-as-code — a locked violation throws here, at `defineStudio` mount).\n // Tier 2: `additiveFields: 'allow'` — a net-new field is appended and persists in the\n // redirect record's `meta` bag (the canonical from/to/permanent/enabled stay locked).\n const schema = extendCollectionDef(redirectsCollectionDef, config, { additiveFields: 'allow' })\n return {\n id: 'redirects',\n get label() {\n // Prefer an explicit rail label, then a consumer-*customized* collection label,\n // else the localized catalog nav label. Never let the built-in English schema\n // label ('Redirects') preempt translation — compare against the built-in default.\n const customized = localizedLabel(schema.form?.label)\n const builtin = localizedLabel(redirectsCollectionDef.form?.label)\n return (\n config.label ??\n (customized && customized !== builtin ? customized : translate('nav.redirects.label'))\n )\n },\n get description() {\n return translate('nav.redirects.description')\n },\n icon: config.icon ?? Signpost,\n availabilitySection: 'cms',\n parsePath: redirectsDeepLink,\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title={translate('nav.redirects.label')} />\n ) : (\n <RedirectsModule\n projectId={ctx.projectId}\n redirectId={ctx.deepLink.redirectId ?? null}\n schema={schema}\n />\n ),\n }\n}\n\n/**\n * Resolve the assets rail's EFFECTIVE schema (EC-319/EC-311): the deployment's\n * OWN `kind:'asset'` collection — hydrated session → kit → playground config,\n * the exact same precedence {@link resolveCmsInitialCollections} gives every\n * other CMS collection — falling back to the engine's `assetCollection` fixture\n * only when nothing resolves (mirroring `CmsProvider`'s own fallback). A mount's\n * `nav.assets({ form, fields })` then merges over THAT base (EC-311, additive\n * fields allowed — a project's asset record already carries an open `meta` bag,\n * EC-301). Pure — no React — so it's unit-testable without `CmsSessionGate`.\n */\nexport function resolveAssetsSchema(\n collections: readonly CollectionDef[] | undefined,\n config: AssetsViewConfig,\n): CollectionDef {\n const base = collections?.find((c) => c.kind === 'asset') ?? assetCollection\n return extendCollectionDef(base, config, { additiveFields: 'allow' })\n}\n\n/**\n * The Assets view (EC-319) — the universal list (+ grid/list toggle) and a\n * bespoke per-asset detail surface, over the asset library store. `[]` is the\n * list; `[assetId]` opens that asset's detail (routed via `studioPath.asset`).\n * A `?asset=` deep link (EC-172 \"used by\" links) redirects straight to the\n * asset's detail — the surface those links used to scroll a card into and\n * auto-open, now a first-class route.\n */\nexport function assetsView(config: AssetsViewConfig = {}): AppViewModule {\n return {\n id: 'assets',\n get label() {\n return config.label ?? translate('nav.assets.label')\n },\n get description() {\n return translate('nav.assets.description')\n },\n icon: config.icon ?? Image,\n availabilitySection: 'assets',\n parsePath: assetsDeepLink,\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"assets\" forcedState={ctx.search.state}>\n {ctx.deepLink.assetId === null && ctx.search.asset ? (\n // EC-441: `NavigateOnce` — render-time redirects fire once per intent.\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: ctx.projectId, _splat: studioPath.asset(ctx.search.asset) }}\n replace\n />\n ) : (\n // The AssetsProvider is mounted at the project level (EC-195).\n <CmsSessionGate>\n {(cms) => {\n const kit = getInstantiatedKit(ctx.projectId)\n const schema = resolveAssetsSchema(resolveCmsInitialCollections(cms, kit), config)\n return (\n <AssetsModule\n projectId={ctx.projectId}\n assetId={ctx.deepLink.assetId ?? null}\n schema={schema}\n />\n )\n }}\n </CmsSessionGate>\n )}\n </SectionSurface>\n ),\n }\n}\n\n/**\n * The studio Users view (EC-240) — an admin-only roster of who has access to this\n * deployment and their role. A first-class section (its own rail entry), modeled\n * like the Assets library; the surface itself gates to admins and renders a\n * \"restricted\" state otherwise. Rides on the `settings` availability (an\n * admin/config concern, so it appears/hides with a project's settings surface).\n */\nexport function usersView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'users',\n get label() {\n return config.label ?? translate('nav.users.label')\n },\n get description() {\n return translate('nav.users.description')\n },\n icon: config.icon ?? Users,\n availabilitySection: 'users',\n // EC-240: `[]` is the roster; `[userId]` opens that member in the portable edit\n // shell (AD-19 reuse — the same `DetailEditor` body over the users source).\n parsePath: usersDeepLink,\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title={translate('nav.users.label')} />\n ) : (\n <UsersModule projectId={ctx.projectId} userId={ctx.deepLink.userId ?? null} />\n ),\n }\n}\n\n/** The extensions marketplace view. */\nexport function extensionsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'extensions',\n get label() {\n return config.label ?? translate('nav.extensions.label')\n },\n get description() {\n return translate('nav.extensions.description')\n },\n icon: config.icon ?? Puzzle,\n availabilitySection: 'extensions',\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title={translate('nav.extensions.label')} />\n ) : (\n <ExtensionsView />\n ),\n }\n}\n\n/** The project settings view. */\nexport function settingsView(config: BuiltinViewConfig = {}): AppViewModule {\n return {\n id: 'settings',\n get label() {\n return config.label ?? translate('nav.settings.label')\n },\n get description() {\n return translate('nav.settings.description')\n },\n icon: config.icon ?? SettingsIcon,\n availabilitySection: 'settings',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"settings\" forcedState={ctx.search.state}>\n <SettingsForm project={ctx.project} />\n </SectionSurface>\n ),\n }\n}\n\n/** The legacy `builder` redirect — a hidden, routable view (so `/builder?page=` still lands). */\nexport function builderView(): AppViewModule {\n return {\n id: 'builder',\n label: 'Builder',\n description: 'Legacy builder route — redirects to the page document editor.',\n icon: Blocks,\n hidden: true,\n render: (ctx) => (\n // Retired surface (EC-187 slice 6): a page is a document in the `page`\n // collection. `?page=<id>` lands on that document; else the page list.\n // EC-441: `NavigateOnce` — render-time redirects fire once per intent.\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{\n projectId: ctx.projectId,\n _splat: ctx.search.page\n ? studioPath.cmsDocument('page', ctx.search.page)\n : studioPath.cmsCollection('page'),\n }}\n replace\n />\n ),\n }\n}\n\n/**\n * Build a fully custom view (the `nav.view(...)` escape hatch).\n *\n * With no `id` the id is a SLUG of the label, and that slug carries a\n * {@link DerivedViewId} marker (EC-476): a label is a display string a developer picks\n * freely, so `label: 'CMS'` deriving `cms` is a coincidence, not a request to replace\n * the built-in CMS surface. {@link checkSidebarIdKeyspace} refuses the takeover and\n * says so; an explicit `id` carries no marker and keeps the documented replacement.\n */\nexport function customView(config: CustomViewConfig): AppViewModule {\n const derivedId: DerivedViewId | undefined = config.id\n ? undefined\n : { from: 'label', label: config.label }\n return {\n id: config.id ?? slugify(config.label),\n ...(derivedId ? { derivedId } : {}),\n label: config.label,\n description: config.description ?? config.label,\n icon: config.icon ?? LayoutGrid,\n render: config.render,\n ...(config.parsePath ? { parsePath: config.parsePath } : {}),\n ...(config.availabilitySection ? { availabilitySection: config.availabilitySection } : {}),\n }\n}\n\n/**\n * The studio's built-in views in canonical rail order (components · CMS · routes ·\n * assets · extensions · settings), plus the hidden `builder` redirect (routable but\n * unlisted). The default rail (no `defineStudio({ sidebar })`) is these as plain\n * entries; the router registry always includes them so deep-links resolve even\n * when a consumer's rail omits one.\n */\nexport const DEFAULT_VIEWS: readonly AppViewModule[] = [\n componentsView(),\n cmsView(),\n redirectsView(),\n assetsView(),\n extensionsView(),\n usersView(),\n settingsView(),\n builderView(),\n]\n\n/**\n * Every id the engine already owns — the replacement targets of the sidebar seam,\n * INCLUDING the hidden `builder` redirect (it is in the keyspace even though the stock\n * rail does not list it, so a derived id can swallow it just the same).\n */\nexport const BUILTIN_VIEW_IDS: ReadonlySet<string> = new Set(DEFAULT_VIEWS.map((view) => view.id))\n\n/** The extension point these issues are located at — the same literal `resolveSidebar` stamps. */\nconst SIDEBAR_POINT = 'sidebar'\n\n/**\n * The catalog keys for the two keyspace findings (EC-478). This module stays pure —\n * it names a key and its identifier values; `lib/persona/labels.ts` resolves them at\n * the render site, so the rail foot localizes as one list.\n */\nconst KEYSPACE_KEYS = {\n builtinCollision: 'customization.issue.derivedIdBuiltinCollision',\n collectionCollision: 'customization.issue.derivedIdCollectionCollision',\n} as const\n\n/** The outcome of the keyspace check: the entries that may still act as overrides, plus the findings. */\nexport interface SidebarIdKeyspace {\n /**\n * The views to resolve as overrides — an ACCIDENTAL built-in takeover removed, so the\n * engine keeps its own block (the EC-236 contract for a rejected entry). Everything\n * else passes through untouched, including the merely-ambiguous EC-477 case.\n */\n views: AppViewModule[]\n /** What was found — visible at the rail foot, never silent. */\n issues: CustomizationIssue[]\n}\n\n/**\n * EC-476/477 — guard the sidebar id keyspace against ids that were DERIVED rather than\n * declared, and landed somewhere that already means something.\n *\n * The seam's replacement rule is id equality (`resolveOverrides`: an entry whose id is a\n * default's id replaces it). That is exactly right for an id the developer TYPED, and\n * exactly wrong for one a builder computed — and the generic core cannot tell them\n * apart, because by the time an entry reaches it, an id is just a string. So the rule\n * lives HERE, next to the derivations that produce it ({@link slugify},\n * {@link cmsViewId}) and next to {@link DEFAULT_VIEWS}, the set it has to protect.\n *\n * Two derived shapes, two different remedies — because the cost of being wrong differs:\n *\n * - **A slugged LABEL landing on a built-in id (EC-476) is REJECTED** (`error`). Losing\n * a whole built-in surface — the CMS, Assets, the users roster — to a coincidence of\n * spelling is far worse than one custom view not appearing, and the developer's fix is\n * a one-line explicit `id`. The built-in is kept, so nothing disappears.\n * - **A collection JOIN equal to a real collection id (EC-477) is KEPT** (`warning`).\n * Nothing here is a takeover: the group's id is legitimate, it is merely no longer\n * unique to it, so dropping the view would destroy a rail item the developer\n * explicitly declared in order to report a naming clash. Reporting is the whole fix.\n *\n * Pure and deterministic — the house \"extract the core, test the core\" shape.\n * `builtinIds` and `collections` are parameters so the rule is testable against a\n * synthetic keyspace, not just the live one.\n */\nexport function checkSidebarIdKeyspace(\n views: readonly AppViewModule[],\n collections: readonly CollectionDef[] = [],\n builtinIds: ReadonlySet<string> = BUILTIN_VIEW_IDS,\n): SidebarIdKeyspace {\n const collectionIds = new Set(collections.map((collection) => collection.id))\n const kept: AppViewModule[] = []\n const issues: CustomizationIssue[] = []\n\n for (const view of views) {\n const derived = view.derivedId\n\n // EC-476 — an auto-derived id must never claim a built-in surface.\n if (derived?.from === 'label' && builtinIds.has(view.id)) {\n issues.push({\n code: 'derived-id-collision',\n severity: 'error',\n message: `View \"${derived.label}\" for \"${SIDEBAR_POINT}\" derives the id \"${view.id}\" from its label, which is the built-in \"${view.id}\" view — the built-in is kept and this view is ignored. Pass an explicit id to replace a built-in on purpose.`,\n messageRef: {\n key: KEYSPACE_KEYS.builtinCollision,\n vars: { label: derived.label, point: SIDEBAR_POINT, id: view.id },\n },\n point: SIDEBAR_POINT,\n id: view.id,\n })\n continue\n }\n\n // EC-477 — a multi-collection join that a single collection could equally have produced.\n if (derived?.from === 'collection-join' && collectionIds.has(joinCollectionIds(derived.collections))) {\n issues.push({\n code: 'derived-id-collision',\n severity: 'warning',\n message: `View id \"${view.id}\" for \"${SIDEBAR_POINT}\" is ambiguous: it joins the collections ${quotedIds(derived.collections)} with \"-\", and a collection is literally named \"${joinCollectionIds(derived.collections)}\" — its own lens derives the same id. Pass an explicit id to this group.`,\n messageRef: {\n key: KEYSPACE_KEYS.collectionCollision,\n vars: {\n id: view.id,\n point: SIDEBAR_POINT,\n collections: quotedIds(derived.collections),\n joined: joinCollectionIds(derived.collections),\n },\n },\n point: SIDEBAR_POINT,\n id: view.id,\n })\n }\n\n kept.push(view)\n }\n\n return { views: kept, issues }\n}\n\n/** `\"a\", \"b\"` — a readable id list for an issue message (the EC-467 issue-copy shape). */\nfunction quotedIds(ids: readonly string[]): string {\n return ids.map((id) => `\"${id}\"`).join(', ')\n}\n\n/**\n * The `nav` namespace — the single typed builder for `studio.config.tsx`'s section\n * rail. Each call returns a {@link SidebarViewNode} (or a group / separator) that\n * carries its view DIRECTLY: `nav.cms({ collection })` is a focused CMS view,\n * `nav.view({ render })` a net-new surface, `nav.group(label, [...])` a header with\n * sub-views, `nav.separator()` a divider. No string ids, no `moduleId` — the rail\n * is the views.\n */\n/**\n * Forward the OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18)\n * from a `nav.*` config onto the built view, so gating is settable on EVERY nav\n * item — not just Users. Absent → the view is unchanged (always visible).\n */\nfunction withNavPermission(view: AppViewModule, config: { visible?: NavPermission }): AppViewModule {\n if (!config.visible) return view\n // Copy property DESCRIPTORS (not a spread) so a built-in view's lazy `label`/\n // `description` getters (EC-241) survive — a spread would invoke them at build time,\n // baking English before `createStudio` seeds the i18n config.\n return Object.defineProperties({} as AppViewModule, {\n ...Object.getOwnPropertyDescriptors(view),\n visible: { value: config.visible, enumerable: true, configurable: true, writable: true },\n })\n}\n\nexport const nav = {\n /**\n * The CMS view. `nav.cms()` / `nav.cms({ collections })` is a PARENT that nests its\n * collections in the rail (each a focused `cms:<id>` lens); `nav.cms({ collection })`\n * is a single-collection LEAF; `nav.cms({ children })` supplies explicit children.\n * A one-item `collections` group is a parent whose id is `cms-group:<id>`, distinct\n * from its single child by construction (EC-468 — see {@link cmsViewId}).\n */\n cms: (config: CmsViewConfig = {}): SidebarViewNode => {\n const view = withNavPermission(cmsView(config), config)\n // A single-collection lens is a leaf — it opens that collection directly.\n if (config.collection) return viewNode(view)\n // Otherwise a parent that expands its collections as nested rail children (EC-293).\n const node: SidebarViewNode = { kind: 'view', view }\n if (config.children) node.children = config.children\n else\n node.expand = (collections) =>\n focusCollectionList(collections, config.collections).map(cmsCollectionLeaf)\n return node\n },\n /** The component registry browser. */\n components: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(componentsView(config), config)),\n /** The assets rail — the universal list/grid + detail (EC-319); built-in\n * fields customizable by name (EC-311), the same shape as `nav.redirects`. */\n assets: (config: AssetsViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(assetsView(config), config)),\n /** Editorial redirects (EC-303); built-in fields customizable by name (EC-311). */\n redirects: (config: RedirectsViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(redirectsView(config), config)),\n /** Project settings. */\n settings: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(settingsView(config), config)),\n /** The extensions marketplace. */\n extensions: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(extensionsView(config), config)),\n /** The admin users roster (EC-240). */\n users: (config: BuiltinViewConfig = {}): SidebarViewNode =>\n viewNode(withNavPermission(usersView(config), config)),\n /** A fully custom view — pass your own `render` (and optional `parsePath`). */\n view: (config: CustomViewConfig): SidebarViewNode =>\n viewNode(withNavPermission(customView(config), config)),\n /** A labelled group of views (a header with sub-entries). */\n group: (label: string, children: SidebarViewNode[]) => groupNode(label, children),\n /** A divider between rail sections. */\n separator: () => separatorNode(),\n} as const\n\n// --- Surfaces (the view render bodies; CMS gains a focus lens) ---\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={translate('nav.components.label')} />\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 * The CMS schema source for a project mount: a hydrated session wins (Convex),\n * then a kit's imported schema (EC-119), then the active project's\n * config-declared collections (AD-11, `resolveDeploymentCollections`).\n *\n * EC-479 — every rung is the PROJECT's own schema and there is no rung after\n * them. The config rung used to be gated on playground mode, because outside it\n * the `undefined` return let `CmsProvider` fall through to `cms-core`'s\n * `sampleFilterableCollections` fixture — the engine's demo schema, shown as the\n * project's, offering `filterable` fields the project's real backend would\n * reject. With that fallback retired the gate has nothing left to protect: the\n * Convex adapter is itself constructed with `resolveDeploymentCollections()`\n * (`lib/persistence/index.ts`), so config collections are what connected mode\n * serves too. Dropping it also DISSOLVES the EC-467 two-source divergence — the\n * section rail reads `resolveDeploymentCollections()` directly, and now so does\n * the store's last rung, so the two can no longer disagree.\n */\nexport function resolveCmsInitialCollections(\n cms: HydratedCms | null,\n kit: InstantiatedKit | undefined,\n): CollectionDef[] | undefined {\n return cms?.collections ?? kit?.collections ?? resolveDeploymentCollections()\n}\n\n/**\n * The CMS documents source for a project mount — the document-side twin of\n * {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then a\n * kit's imported documents, then **nothing**.\n *\n * EC-479 — the third rung used to be the engine's own `sampleDocuments` fixture,\n * reconciled against the project's schema (EC-424). Reconciling it was already an\n * admission that the content was foreign: EC-424 existed only because the demo\n * declares `post.slug` localized while the shared fixture stores it plain, so the\n * seeded posts carried standing validation noise no project had written. The\n * honest fix is not a better reconciliation but no foreign content at all — a\n * studio whose config did not arrive now shows an empty CMS and a visible issue\n * at the rail, not six documents about Ada Lovelace wearing the project's schema.\n *\n * The playground keeps its world through the ordinary path, not through here: its\n * seeded in-memory adapter IS a persisted backend behind the operations seam\n * (AD-17), so `CmsSessionGate` hydrates it and the first rung answers. What that\n * adapter seeds is `persistence/seed.ts`'s business — EC-475 (project-defined seed\n * content) owns it.\n */\nexport function resolveCmsInitialDocuments(\n cms: HydratedCms | null,\n kit: InstantiatedKit | undefined,\n): CmsDocument[] | undefined {\n return cms?.documents ?? kit?.documents\n}\n\n/**\n * The CMS locale config for a project mount (EC-357 finding #3). Locales are\n * structure and structure is code: when the active `elytra.config.ts` project\n * declares locales, they drive the ambient locale switcher in EVERY mode. The old\n * wiring only applied the project's locale settings on a HYDRATED session, so a\n * playground boot — or a connected FIRST RUN with nothing persisted yet — fell\n * back to a kit's locales or the core's sample `en/de` fixture and the switcher\n * showed `EN·/DE` against a `locales: ['de']` config.\n *\n * EC-479/EC-483 — the ladder is now TOTAL, and every rung is the project's own:\n * config-declared locales, then (hydrated) its persisted settings, then a kit's,\n * then its settings regardless. A `Project` only ever exists for a config-declared\n * project (`studio-context.tsx`: `configProjects()`), so `project.settings` is\n * config-derived too — which is what lets the CMS session require a locale instead\n * of borrowing the engine's `en/de` fixture. The default locale is the literal\n * storage key for every localized value, so a guessed one is a data migration.\n * `configLocales` is a parameter purely so the ladder is unit-testable (same seam\n * pattern as {@link resolveCmsInitialCollections}).\n */\nexport function resolveCmsInitialLocaleConfig(\n cms: HydratedCms | null,\n kit: InstantiatedKit | undefined,\n project: Project,\n configLocales: LocaleConfig | undefined = resolveDeploymentLocaleConfig(),\n): LocaleConfig {\n const projectSettings: LocaleConfig = {\n default: project.settings.defaultLocale,\n locales: [...project.settings.locales],\n }\n if (configLocales) return configLocales\n if (cms) return projectSettings\n return kit?.localeConfig ?? projectSettings\n}\n\n/**\n * CMS section: hydrates the persisted (or kit-seeded) collections + documents, then\n * renders the active screen full-width (index, a collection table, or a document\n * editor). EC-293: the collection list lives in the section rail (nested under the\n * CMS view), so there is no in-surface collections sidebar. Forced states (?state=)\n * replace the whole surface. `focusCollections` (EC-239) narrows the index to a\n * subset, so a focused view (\"Catalog\") reuses this surface with config.\n */\nfunction CmsScreen({\n project,\n collectionId,\n documentId,\n state,\n field,\n focusCollections,\n}: {\n project: Project\n collectionId: string | null\n documentId: string | null\n state?: SectionState\n field?: string\n focusCollections?: readonly string[]\n}) {\n const projectId = project.id\n if (state && state !== 'working') {\n return <SectionStateView state={state} title={translate('nav.cms.label')} />\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: every CMS mount loads the CURRENT persisted schema +\n // documents (both modes since EC-461). Nothing persisted — a connected first\n // run — means an EMPTY session plus the rail's visible issue (EC-479), never\n // the engine's fixtures wearing the project's schema.\n <CmsSessionGate>\n {(cms) => {\n return (\n <CmsProvider\n key={projectId}\n projectId={projectId}\n initialCollections={resolveCmsInitialCollections(cms, kit)}\n initialDocuments={resolveCmsInitialDocuments(cms, kit)}\n initialPublishedKeys={cms?.publishedKeys}\n initialTimestamps={cms?.timestamps}\n initialPublishTimestamps={cms?.publishTimestamps}\n initialChangedKeys={cms?.changedKeys}\n initialLocaleConfig={resolveCmsInitialLocaleConfig(cms, kit, project)}\n // EC-461: a hydrated mount (cms non-null — both modes now) seeds the\n // CURRENT persisted state, so staged docs are superseded by it; only\n // a static kit/fixture seed lets staged entries win.\n seedReflectsWrites={cms !== null}\n >\n <div className=\"flex h-full min-h-0 flex-col\">\n <CmsPersistErrorBanner />\n {/* EC-293: the collection list now lives in the section rail (nested\n under the CMS view), so the surface is full-width — the old 240px\n CollectionsSidebar aside is gone. EC-199: the AI assistant mounts as\n a right-hand sibling (both list + editor), so the content is a row. */}\n <div className=\"flex min-h-0 flex-1 overflow-hidden\">\n <div className=\"min-w-0 flex-1 overflow-hidden\">\n <CmsContent\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n field={field}\n focusCollections={focusCollections}\n />\n </div>\n <AiCmsPanel />\n </div>\n </div>\n </CmsProvider>\n )\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 focusCollections,\n}: {\n projectId: string\n collectionId: string | null\n documentId: string | null\n field?: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n\n if (collectionId === null) return <CmsIndex projectId={projectId} focusCollections={focusCollections} />\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.\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 {translate('appViews.singleton.opening', {\n label: localizedLabel(collection?.form?.label, collectionId),\n })}\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"]}
@@ -56,9 +56,47 @@ export interface AppViewContext {
56
56
  /** The validated `?…` search (forced state, focused field/asset, …). */
57
57
  search: StudioSearch;
58
58
  }
59
+ /**
60
+ * EC-476/477 — how a view's id came to be, when the builder **derived** it instead of
61
+ * taking an explicit `config.id`.
62
+ *
63
+ * Diagnostics only: nothing routes, renders or resolves on this. It exists because the
64
+ * derived-vs-explicit distinction is otherwise UNRECOVERABLE downstream, and the two
65
+ * must not behave the same. `nav.view({ id: 'cms' })` names a built-in on purpose —
66
+ * that is the documented override feature (EC-236). `nav.view({ label: 'CMS' })` names
67
+ * a *label*, and `slugify` happening to produce `cms` is an accident that costs an
68
+ * entire built-in surface. `resolveSidebar` cannot tell those apart from the finished
69
+ * view: every built-in's own English label slugifies onto its own id
70
+ * (`settingsView().label` = "Settings" → `settings`), so inferring "was this derived?"
71
+ * by re-slugifying the label misfires on the DEFAULT RAIL. Hence a carried marker.
72
+ *
73
+ * Present only on a derived id — an explicit `config.id` leaves it absent, which is
74
+ * what makes "the developer meant it" checkable.
75
+ */
76
+ export type DerivedViewId =
77
+ /** Slugified from a custom view's LABEL (`nav.view({ label })`); the label is carried for the message. */
78
+ {
79
+ from: 'label';
80
+ label: string;
81
+ }
82
+ /**
83
+ * Joined from a MULTI-collection CMS focus with the `-` separator
84
+ * (`nav.cms({ collections: ['a','b'] })` → `cms:a-b`). The single-collection leaf and
85
+ * the one-item `cms-group:` parent are deliberately NOT marked: their ids are
86
+ * unambiguous by construction (EC-468), so there is nothing to diagnose.
87
+ */
88
+ | {
89
+ from: 'collection-join';
90
+ collections: readonly string[];
91
+ };
59
92
  export interface AppViewModule {
60
93
  /** Stable id — the first path segment under the project + the registry key. */
61
94
  id: string;
95
+ /**
96
+ * EC-476/477 — DIAGNOSTIC provenance for an id the builder DERIVED rather than took
97
+ * from an explicit `config.id`. Absent → the id was declared. See {@link DerivedViewId}.
98
+ */
99
+ derivedId?: DerivedViewId;
62
100
  label: string;
63
101
  description: string;
64
102
  icon: LucideIcon;
@@ -1 +1 @@
1
- {"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/components/app-views/module.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { availabilityOf } from '@elytracms/core/studio-core'\nimport type { Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\n\n/**\n * The studio **view** contract + the **sidebar node** model — the internal types\n * the consumer-facing `nav.*` builders (EC-237/239, customization seam EC-236)\n * produce.\n *\n * A studio surface (the CMS, Routes, Assets view, or a net-new one) is a\n * self-contained {@link AppViewModule}: a stable id + display metadata + a React\n * `render` + an optional deep-link parser. A {@link SidebarNode} is one rail entry\n * — a view, a labelled group of views, or a separator — and a **view node carries\n * its view object DIRECTLY**, not a string id. This is the redesign's core: there\n * is no separate \"register a view, then reference it by `moduleId`\" indirection\n * and no shared id-keyspace, so the section rail IS the set of views, the router\n * registry is DERIVED from it, and the duplicate-id / both-highlighted class of\n * bug is gone by construction.\n *\n * The boundary principle: everything here is studio-only live code passed by\n * reference (no string keys). A string only crosses the serializable boundary for\n * field controls (`form.control`), where the field lives in delivery-shared schema.\n *\n * Reuse-not-copy (EC-239): a focused view like \"Catalog\" is the CMS surface\n * instantiated with config (`nav.cms({ collections })`), not a fork — the same\n * surface code backs many views, each its own addressable node.\n */\n\n/** The deep-link a view parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */\nexport type AppViewDeepLink = Record<string, string | null>\n\n/**\n * What an OPTIONAL nav-permission predicate sees (EC-240, vision AD-18): the\n * project + the signed-in user's resolved role (`null` while it loads, or for a\n * non-member). Kept minimal — policy that needs more should live server-side.\n */\nexport interface NavPermissionContext {\n project: Project\n role: ProjectRole | null\n}\n\n/**\n * An OPTIONAL, **default-open** per-nav-item visibility predicate (AD-18). Returns\n * `false` to HIDE the item for this principal; absent ⇒ visible. Permissions\n * RESTRICT — they never grant a baseline. This is the general mechanism (applies\n * to EVERY nav item, not a Users special-case); the concrete policy ships in the\n * starter project, never the engine. UI reflection only — the server chokepoint\n * stays the real boundary, and hidden views remain URL-addressable (EC-221).\n */\nexport type NavPermission = (ctx: NavPermissionContext) => boolean\n\n/** Everything a view's surface needs to render (EC-239). */\nexport interface AppViewContext {\n project: Project\n projectId: string\n /** The view's parsed deep-link; `{}` for a flat view with no nested resource. */\n deepLink: AppViewDeepLink\n /** The validated `?…` search (forced state, focused field/asset, …). */\n search: StudioSearch\n}\n\nexport interface AppViewModule {\n /** Stable id — the first path segment under the project + the registry key. */\n id: string\n label: string\n description: string\n icon: LucideIcon\n /** Render the surface; it mounts its own providers/gates. */\n render: (ctx: AppViewContext) => ReactNode\n /**\n * Parse this view's trailing path segments into a deep-link, or `null` for an\n * unroutable path (→ not-found). Omitted → a FLAT view ({@link flatPath}): only\n * the bare path is valid, any trailing segment is not-found.\n */\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n /**\n * The availability section this view rides on (EC-221 gating): an `unavailable`\n * section hides the view from the rail. Omitted → always available (a custom\n * view defaults to visible).\n */\n availabilitySection?: SectionId\n /**\n * OPTIONAL, default-open per-nav-item visibility (EC-240, vision AD-18). Absent\n * → visible. Evaluated with the signed-in user's role so devs can configure\n * role-based sidebars for ANY nav item (not just Users). Distinct from\n * {@link availabilitySection} (project-config gating): this is per-principal.\n * See {@link NavPermission}. (Phase-0 contract; the rail wires it in EC-240c —\n * `resolveSidebar`/`SectionNav` thread the role through and honour a `false`.)\n */\n visible?: NavPermission\n /** A redirect-only / utility view the stock rail does not list (e.g. `builder`). */\n hidden?: boolean\n}\n\n// --- The sidebar node model (what `nav.*` produces) ---\n\n/**\n * A rail entry that mounts a view. Carries the view object directly — no string id.\n *\n * EC-293: a view node may NEST one level. `children` is a static list of sub-views;\n * `expand` derives them at resolve time from the project's collections (CMS\n * auto-expand). Children render indented under the parent — indentation signals\n * parent→child, distinct from a flat labelled {@link SidebarGroupNode}. `children`\n * (explicit) wins over `expand` when both are set.\n */\nexport interface SidebarViewNode {\n kind: 'view'\n view: AppViewModule\n /** Static sub-views, rendered indented under this view (one level of nesting). */\n children?: SidebarViewNode[]\n /**\n * Resolve-time children from the project's collections (e.g. CMS expanding each\n * collection into a focused-lens child). Run by `resolveSidebar` with the\n * config-sourced collections; the result renders as {@link children}.\n */\n expand?: (collections: readonly CollectionDef[]) => SidebarViewNode[]\n}\n/**\n * A labelled group of view entries. Groups are presentation only — never routable,\n * and **flat**: their children render aligned with the top-level items (the heading\n * is the only grouping signal). Group children do not nest — use a parent-with-\n * children ({@link SidebarViewNode.expand}/`children`) for nesting (EC-293).\n */\nexport interface SidebarGroupNode {\n kind: 'group'\n label: string\n children: SidebarViewNode[]\n}\n/** A divider between rail sections. */\nexport interface SidebarSeparatorNode {\n kind: 'separator'\n}\nexport type SidebarNode = SidebarViewNode | SidebarGroupNode | SidebarSeparatorNode\n\n/** Wrap a view as a rail entry. */\nexport function viewNode(view: AppViewModule): SidebarViewNode {\n return { kind: 'view', view }\n}\n/** A labelled group of view entries (sub-views under a header). */\nexport function groupNode(label: string, children: SidebarViewNode[]): SidebarGroupNode {\n return { kind: 'group', label, children }\n}\n/** A rail divider. */\nexport function separatorNode(): SidebarSeparatorNode {\n return { kind: 'separator' }\n}\n\n// --- Deep-link parsers (shared by the built-in views + reused by factories) ---\n\n/** A flat view: only the bare path resolves; any trailing segment is not-found. */\nexport function flatPath(segments: string[]): AppViewDeepLink | null {\n return segments.length === 0 ? {} : null\n}\n\n/** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */\nexport function cmsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { collectionId: null, documentId: null }\n if (segments.length === 1) return { collectionId: segments[0]!, documentId: null }\n if (segments.length === 2) return { collectionId: segments[0]!, documentId: segments[1]! }\n return null\n}\n\n/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */\nexport function componentsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { componentId: null }\n if (segments.length === 1) return { componentId: segments[0]! }\n return null\n}\n\n/** Users arity (EC-240): `[]` list · `[userId]` member detail; deeper is not-found. */\nexport function usersDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { userId: null }\n if (segments.length === 1) return { userId: segments[0]! }\n return null\n}\n\n/** Redirects arity (EC-303): `[]` list · `[redirectId]` detail; deeper is not-found. */\nexport function redirectsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { redirectId: null }\n if (segments.length === 1) return { redirectId: segments[0]! }\n return null\n}\n\n/** Assets arity (EC-319): `[]` list · `[assetId]` detail; deeper is not-found. */\nexport function assetsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { assetId: null }\n if (segments.length === 1) return { assetId: segments[0]! }\n return null\n}\n\n// --- Availability (EC-221) ---\n\n/**\n * A view's availability for a project (EC-221): the section it rides on, or always\n * `available` when it declares none (a custom view). Drives both the rail (hide\n * `unavailable`) and the bare-project landing target.\n */\nexport function moduleAvailability(project: Project, module: AppViewModule): SectionAvailability {\n return module.availabilitySection\n ? availabilityOf(project.settings.sectionAvailability, module.availabilitySection)\n : 'available'\n}\n\n/**\n * The view a project should open to from its bare URL: the first listed\n * (non-hidden), non-`unavailable` view in rail order. Callers pass the resolved\n * registry's view list (rail order); `undefined` only if every view is\n * hidden/unavailable (the resolver guarantees Settings, so in practice never).\n */\nexport function firstAvailableView(\n views: readonly AppViewModule[],\n project: Project,\n): AppViewModule | undefined {\n return views.find(\n (view) => !view.hidden && moduleAvailability(project, view) !== 'unavailable',\n )\n}\n"]}
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/components/app-views/module.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { availabilityOf } from '@elytracms/core/studio-core'\nimport type { Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\n\n/**\n * The studio **view** contract + the **sidebar node** model — the internal types\n * the consumer-facing `nav.*` builders (EC-237/239, customization seam EC-236)\n * produce.\n *\n * A studio surface (the CMS, Routes, Assets view, or a net-new one) is a\n * self-contained {@link AppViewModule}: a stable id + display metadata + a React\n * `render` + an optional deep-link parser. A {@link SidebarNode} is one rail entry\n * — a view, a labelled group of views, or a separator — and a **view node carries\n * its view object DIRECTLY**, not a string id. This is the redesign's core: there\n * is no separate \"register a view, then reference it by `moduleId`\" indirection\n * and no shared id-keyspace, so the section rail IS the set of views, the router\n * registry is DERIVED from it, and the duplicate-id / both-highlighted class of\n * bug is gone by construction.\n *\n * The boundary principle: everything here is studio-only live code passed by\n * reference (no string keys). A string only crosses the serializable boundary for\n * field controls (`form.control`), where the field lives in delivery-shared schema.\n *\n * Reuse-not-copy (EC-239): a focused view like \"Catalog\" is the CMS surface\n * instantiated with config (`nav.cms({ collections })`), not a fork — the same\n * surface code backs many views, each its own addressable node.\n */\n\n/** The deep-link a view parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */\nexport type AppViewDeepLink = Record<string, string | null>\n\n/**\n * What an OPTIONAL nav-permission predicate sees (EC-240, vision AD-18): the\n * project + the signed-in user's resolved role (`null` while it loads, or for a\n * non-member). Kept minimal — policy that needs more should live server-side.\n */\nexport interface NavPermissionContext {\n project: Project\n role: ProjectRole | null\n}\n\n/**\n * An OPTIONAL, **default-open** per-nav-item visibility predicate (AD-18). Returns\n * `false` to HIDE the item for this principal; absent ⇒ visible. Permissions\n * RESTRICT — they never grant a baseline. This is the general mechanism (applies\n * to EVERY nav item, not a Users special-case); the concrete policy ships in the\n * starter project, never the engine. UI reflection only — the server chokepoint\n * stays the real boundary, and hidden views remain URL-addressable (EC-221).\n */\nexport type NavPermission = (ctx: NavPermissionContext) => boolean\n\n/** Everything a view's surface needs to render (EC-239). */\nexport interface AppViewContext {\n project: Project\n projectId: string\n /** The view's parsed deep-link; `{}` for a flat view with no nested resource. */\n deepLink: AppViewDeepLink\n /** The validated `?…` search (forced state, focused field/asset, …). */\n search: StudioSearch\n}\n\n/**\n * EC-476/477 — how a view's id came to be, when the builder **derived** it instead of\n * taking an explicit `config.id`.\n *\n * Diagnostics only: nothing routes, renders or resolves on this. It exists because the\n * derived-vs-explicit distinction is otherwise UNRECOVERABLE downstream, and the two\n * must not behave the same. `nav.view({ id: 'cms' })` names a built-in on purpose —\n * that is the documented override feature (EC-236). `nav.view({ label: 'CMS' })` names\n * a *label*, and `slugify` happening to produce `cms` is an accident that costs an\n * entire built-in surface. `resolveSidebar` cannot tell those apart from the finished\n * view: every built-in's own English label slugifies onto its own id\n * (`settingsView().label` = \"Settings\" → `settings`), so inferring \"was this derived?\"\n * by re-slugifying the label misfires on the DEFAULT RAIL. Hence a carried marker.\n *\n * Present only on a derived id — an explicit `config.id` leaves it absent, which is\n * what makes \"the developer meant it\" checkable.\n */\nexport type DerivedViewId =\n /** Slugified from a custom view's LABEL (`nav.view({ label })`); the label is carried for the message. */\n | { from: 'label'; label: string }\n /**\n * Joined from a MULTI-collection CMS focus with the `-` separator\n * (`nav.cms({ collections: ['a','b'] })` → `cms:a-b`). The single-collection leaf and\n * the one-item `cms-group:` parent are deliberately NOT marked: their ids are\n * unambiguous by construction (EC-468), so there is nothing to diagnose.\n */\n | { from: 'collection-join'; collections: readonly string[] }\n\nexport interface AppViewModule {\n /** Stable id — the first path segment under the project + the registry key. */\n id: string\n /**\n * EC-476/477 — DIAGNOSTIC provenance for an id the builder DERIVED rather than took\n * from an explicit `config.id`. Absent → the id was declared. See {@link DerivedViewId}.\n */\n derivedId?: DerivedViewId\n label: string\n description: string\n icon: LucideIcon\n /** Render the surface; it mounts its own providers/gates. */\n render: (ctx: AppViewContext) => ReactNode\n /**\n * Parse this view's trailing path segments into a deep-link, or `null` for an\n * unroutable path (→ not-found). Omitted → a FLAT view ({@link flatPath}): only\n * the bare path is valid, any trailing segment is not-found.\n */\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n /**\n * The availability section this view rides on (EC-221 gating): an `unavailable`\n * section hides the view from the rail. Omitted → always available (a custom\n * view defaults to visible).\n */\n availabilitySection?: SectionId\n /**\n * OPTIONAL, default-open per-nav-item visibility (EC-240, vision AD-18). Absent\n * → visible. Evaluated with the signed-in user's role so devs can configure\n * role-based sidebars for ANY nav item (not just Users). Distinct from\n * {@link availabilitySection} (project-config gating): this is per-principal.\n * See {@link NavPermission}. (Phase-0 contract; the rail wires it in EC-240c —\n * `resolveSidebar`/`SectionNav` thread the role through and honour a `false`.)\n */\n visible?: NavPermission\n /** A redirect-only / utility view the stock rail does not list (e.g. `builder`). */\n hidden?: boolean\n}\n\n// --- The sidebar node model (what `nav.*` produces) ---\n\n/**\n * A rail entry that mounts a view. Carries the view object directly — no string id.\n *\n * EC-293: a view node may NEST one level. `children` is a static list of sub-views;\n * `expand` derives them at resolve time from the project's collections (CMS\n * auto-expand). Children render indented under the parent — indentation signals\n * parent→child, distinct from a flat labelled {@link SidebarGroupNode}. `children`\n * (explicit) wins over `expand` when both are set.\n */\nexport interface SidebarViewNode {\n kind: 'view'\n view: AppViewModule\n /** Static sub-views, rendered indented under this view (one level of nesting). */\n children?: SidebarViewNode[]\n /**\n * Resolve-time children from the project's collections (e.g. CMS expanding each\n * collection into a focused-lens child). Run by `resolveSidebar` with the\n * config-sourced collections; the result renders as {@link children}.\n */\n expand?: (collections: readonly CollectionDef[]) => SidebarViewNode[]\n}\n/**\n * A labelled group of view entries. Groups are presentation only — never routable,\n * and **flat**: their children render aligned with the top-level items (the heading\n * is the only grouping signal). Group children do not nest — use a parent-with-\n * children ({@link SidebarViewNode.expand}/`children`) for nesting (EC-293).\n */\nexport interface SidebarGroupNode {\n kind: 'group'\n label: string\n children: SidebarViewNode[]\n}\n/** A divider between rail sections. */\nexport interface SidebarSeparatorNode {\n kind: 'separator'\n}\nexport type SidebarNode = SidebarViewNode | SidebarGroupNode | SidebarSeparatorNode\n\n/** Wrap a view as a rail entry. */\nexport function viewNode(view: AppViewModule): SidebarViewNode {\n return { kind: 'view', view }\n}\n/** A labelled group of view entries (sub-views under a header). */\nexport function groupNode(label: string, children: SidebarViewNode[]): SidebarGroupNode {\n return { kind: 'group', label, children }\n}\n/** A rail divider. */\nexport function separatorNode(): SidebarSeparatorNode {\n return { kind: 'separator' }\n}\n\n// --- Deep-link parsers (shared by the built-in views + reused by factories) ---\n\n/** A flat view: only the bare path resolves; any trailing segment is not-found. */\nexport function flatPath(segments: string[]): AppViewDeepLink | null {\n return segments.length === 0 ? {} : null\n}\n\n/** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */\nexport function cmsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { collectionId: null, documentId: null }\n if (segments.length === 1) return { collectionId: segments[0]!, documentId: null }\n if (segments.length === 2) return { collectionId: segments[0]!, documentId: segments[1]! }\n return null\n}\n\n/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */\nexport function componentsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { componentId: null }\n if (segments.length === 1) return { componentId: segments[0]! }\n return null\n}\n\n/** Users arity (EC-240): `[]` list · `[userId]` member detail; deeper is not-found. */\nexport function usersDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { userId: null }\n if (segments.length === 1) return { userId: segments[0]! }\n return null\n}\n\n/** Redirects arity (EC-303): `[]` list · `[redirectId]` detail; deeper is not-found. */\nexport function redirectsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { redirectId: null }\n if (segments.length === 1) return { redirectId: segments[0]! }\n return null\n}\n\n/** Assets arity (EC-319): `[]` list · `[assetId]` detail; deeper is not-found. */\nexport function assetsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { assetId: null }\n if (segments.length === 1) return { assetId: segments[0]! }\n return null\n}\n\n// --- Availability (EC-221) ---\n\n/**\n * A view's availability for a project (EC-221): the section it rides on, or always\n * `available` when it declares none (a custom view). Drives both the rail (hide\n * `unavailable`) and the bare-project landing target.\n */\nexport function moduleAvailability(project: Project, module: AppViewModule): SectionAvailability {\n return module.availabilitySection\n ? availabilityOf(project.settings.sectionAvailability, module.availabilitySection)\n : 'available'\n}\n\n/**\n * The view a project should open to from its bare URL: the first listed\n * (non-hidden), non-`unavailable` view in rail order. Callers pass the resolved\n * registry's view list (rail order); `undefined` only if every view is\n * hidden/unavailable (the resolver guarantees Settings, so in practice never).\n */\nexport function firstAvailableView(\n views: readonly AppViewModule[],\n project: Project,\n): AppViewModule | undefined {\n return views.find(\n (view) => !view.hidden && moduleAvailability(project, view) !== 'unavailable',\n )\n}\n"]}
@@ -20,7 +20,10 @@ import { type AppViewModule, type NavPermission, type NavPermissionContext, type
20
20
  * id, so a group label can never collide with a view (the old duplicate-id bug).
21
21
  * Views resolve through the shared {@link resolveOverrides} core: a duplicate view
22
22
  * id or a view missing `render`/`label` is a visible {@link CustomizationIssue} and
23
- * is dropped — never silent. The active rail item is matched by the view id in the
23
+ * is dropped — never silent. Ids the builders DERIVED are screened BEFORE that core
24
+ * ({@link checkSidebarIdKeyspace}, EC-476/477): to a generic seam an id is just a
25
+ * string, so a slug that happened to equal a built-in id read as an intentional
26
+ * replacement and swallowed the surface. The active rail item is matched by the view id in the
24
27
  * URL, so two views backed by the same surface (Content vs. a focused Categories)
25
28
  * highlight independently.
26
29
  */
@@ -67,7 +70,7 @@ export interface StudioNav {
67
70
  nodes: ResolvedRailNode[];
68
71
  /** The router registry (for the screen resolver + breadcrumb). */
69
72
  registry: ViewRegistry;
70
- /** Customization issues (duplicate view id, invalid view) — visible, never silent. */
73
+ /** Customization issues (duplicate view id, invalid view, derived-id collision) — visible, never silent. */
71
74
  issues: CustomizationIssue[];
72
75
  }
73
76
  /**
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
2
2
  import { resolveOverrides } from '@elytracms/core/studio-core';
3
3
  import { resolveDeploymentCollections, resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace';
4
4
  import { firstAvailableView, moduleAvailability, viewNode, } from './module.js';
5
- import { DEFAULT_VIEWS } from './default-modules.js';
5
+ import { DEFAULT_VIEWS, checkSidebarIdKeyspace } from './default-modules.js';
6
6
  /** The default rail: every non-hidden built-in view as a plain entry, in canonical order. */
7
7
  function defaultRail() {
8
8
  return DEFAULT_VIEWS.filter((view) => !view.hidden).map(viewNode);
@@ -48,20 +48,29 @@ function railViews(nodes, collections) {
48
48
  export function resolveSidebar(entries, project, collections = []) {
49
49
  const nodes = entries ?? defaultRail();
50
50
  const railOrderViews = railViews(nodes, collections);
51
+ // EC-476/477: an id a builder DERIVED is not a request to replace whatever it happens
52
+ // to equal. Screened BEFORE the seam, which sees only strings and so cannot tell an
53
+ // intentional override from a coincidence of spelling (see `checkSidebarIdKeyspace`).
54
+ const keyspace = checkSidebarIdKeyspace(railOrderViews, collections);
51
55
  // Resolve the rail views over the built-in registry: dedup by id + contract
52
56
  // check. Built-ins are the defaults (so omitted ones stay routable); rail views
53
57
  // are overrides (a same-id rail view replaces the default; a new id is added).
54
58
  const resolved = resolveOverrides({
55
59
  point: 'sidebar',
56
60
  defaults: DEFAULT_VIEWS.map((view) => ({ id: view.id, value: view })),
57
- overrides: railOrderViews.map((view) => ({ id: view.id, value: view })),
61
+ overrides: keyspace.views.map((view) => ({ id: view.id, value: view })),
58
62
  allowNew: true,
63
+ // EC-478: a rejection carries its catalog key as well as its English, so the
64
+ // `{reason}` half of `invalid-override` localizes with the sentence around it.
59
65
  validate: (entry) => {
60
66
  const view = entry.value;
61
67
  if (typeof view.render !== 'function')
62
- return 'the view has no render function';
68
+ return {
69
+ message: 'the view has no render function',
70
+ key: 'customization.issue.reason.viewNoRender',
71
+ };
63
72
  if (typeof view.label !== 'string' || view.label.trim().length === 0)
64
- return 'the view has no label';
73
+ return { message: 'the view has no label', key: 'customization.issue.reason.viewNoLabel' };
65
74
  return null;
66
75
  },
67
76
  });
@@ -123,7 +132,9 @@ export function resolveSidebar(entries, project, collections = []) {
123
132
  view.children = children;
124
133
  railNodes.push(view);
125
134
  }
126
- return { nodes: railNodes, registry, issues: resolved.issues };
135
+ // Keyspace findings first: a derived-id collision is the CAUSE of any duplicate that
136
+ // follows it, so it reads before the symptom at the rail foot.
137
+ return { nodes: railNodes, registry, issues: [...keyspace.issues, ...resolved.issues] };
127
138
  }
128
139
  function isRailView(view) {
129
140
  return view !== null;
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../src/components/app-views/sidebar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { resolveOverrides } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { resolveDeploymentCollections, resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace'\nimport {\n firstAvailableView,\n moduleAvailability,\n viewNode,\n type AppViewModule,\n type NavPermission,\n type NavPermissionContext,\n type SidebarNode,\n type SidebarViewNode,\n} from './module'\nimport { DEFAULT_VIEWS } from './default-modules'\n\n/**\n * EC-237 — resolving the configurable section rail, an instance of the EC-236\n * customization seam.\n *\n * A consuming repo supplies its rail as a `SidebarNode[]` (built from `nav.*`);\n * omitted → the studio derives the default rail from its built-in views. The rail\n * IS the set of views — each view node carries its view object directly — so this\n * resolver does two things at once:\n *\n * 1. **Renders the rail.** Walk the nodes into display-ready entries (views,\n * groups, separators) with per-view availability (EC-221) and a route path.\n * 2. **Derives the router registry.** Collect every rail view + the always-on\n * built-ins (so deep-links resolve even for views a rail omits), keyed by id.\n *\n * Only VIEWS occupy the id-keyspace; groups + separators are presentation with no\n * id, so a group label can never collide with a view (the old duplicate-id bug).\n * Views resolve through the shared {@link resolveOverrides} core: a duplicate view\n * id or a view missing `render`/`label` is a visible {@link CustomizationIssue} and\n * is dropped — never silent. The active rail item is matched by the view id in the\n * URL, so two views backed by the same surface (Content vs. a focused Categories)\n * highlight independently.\n */\n\n// --- Resolved (rail-ready) shapes ---\n\nexport interface ResolvedRailView {\n kind: 'view'\n /** The view id — the route segment and the active-state key. */\n id: string\n label: string\n icon: LucideIcon\n /** The `_splat` to navigate to (the view's home route = its id). */\n path: string\n availability: SectionAvailability\n /**\n * EC-240 (AD-18): the view's OPTIONAL, default-open nav-permission predicate,\n * carried through resolution so the rail can evaluate it against the live role\n * (which resolution — memoised on `project` alone — does not have). Absent →\n * always visible.\n */\n visible?: NavPermission\n /**\n * EC-293: nested sub-views (e.g. a CMS view's collections), rendered indented\n * under this view. Present only on a parent that expanded; leaves omit it.\n */\n children?: ResolvedRailView[]\n}\nexport interface ResolvedRailGroup {\n kind: 'group'\n label: string\n views: ResolvedRailView[]\n}\nexport interface ResolvedRailSeparator {\n kind: 'separator'\n}\nexport type ResolvedRailNode = ResolvedRailView | ResolvedRailGroup | ResolvedRailSeparator\n\n/** The router registry derived from the rail: id → view, plus the landing order. */\nexport interface ViewRegistry {\n /** Resolve a view id to its module (rail views + always-on built-ins). */\n get(id: string): AppViewModule | undefined\n /** Views in landing order (rail order first, then non-rail built-ins). */\n views: AppViewModule[]\n}\n\nexport interface StudioNav {\n /** The display-ready rail (for {@link import('../section-nav').SectionNav}). */\n nodes: ResolvedRailNode[]\n /** The router registry (for the screen resolver + breadcrumb). */\n registry: ViewRegistry\n /** Customization issues (duplicate view id, invalid view) — visible, never silent. */\n issues: CustomizationIssue[]\n}\n\n/** The default rail: every non-hidden built-in view as a plain entry, in canonical order. */\nfunction defaultRail(): SidebarNode[] {\n return DEFAULT_VIEWS.filter((view) => !view.hidden).map(viewNode)\n}\n\n/**\n * The sub-views a view node contributes (EC-293): explicit `children`, else\n * `expand(collections)` (CMS auto-expansion), else none. Groups never nest, so this\n * applies to top-level view nodes only.\n */\nfunction childNodesOf(\n node: SidebarViewNode,\n collections: readonly CollectionDef[],\n): SidebarViewNode[] {\n return node.children ?? node.expand?.(collections) ?? []\n}\n\n/**\n * Every view the rail references, in rail order: each top-level view, then its nested\n * children, then group children (groups are flat — their children never nest). This\n * is the set fed to `resolveOverrides` + the derived registry.\n */\nfunction railViews(\n nodes: readonly SidebarNode[],\n collections: readonly CollectionDef[],\n): AppViewModule[] {\n const views: AppViewModule[] = []\n for (const node of nodes) {\n if (node.kind === 'view') {\n views.push(node.view)\n for (const child of childNodesOf(node, collections)) views.push(child.view)\n } else if (node.kind === 'group') {\n for (const child of node.children) views.push(child.view)\n }\n }\n return views\n}\n\n/**\n * Resolve a consumer rail (or the default rail) into the display tree + the router\n * registry through the EC-236 seam. Views are resolved over the built-in defaults\n * (so an omitted built-in stays routable and a rail `cms` REPLACES the default\n * one); groups/separators pass through as presentation.\n *\n * EC-293: a view node may nest. `collections` (config-sourced) materialises a CMS\n * parent's per-collection children; every child view joins the override pass + the\n * derived registry, so a nested `cms:<id>` lens routes and highlights like any view.\n */\nexport function resolveSidebar(\n entries: readonly SidebarNode[] | undefined,\n project: Project,\n collections: readonly CollectionDef[] = [],\n): StudioNav {\n const nodes = entries ?? defaultRail()\n const railOrderViews = railViews(nodes, collections)\n\n // Resolve the rail views over the built-in registry: dedup by id + contract\n // check. Built-ins are the defaults (so omitted ones stay routable); rail views\n // are overrides (a same-id rail view replaces the default; a new id is added).\n const resolved = resolveOverrides<AppViewModule>({\n point: 'sidebar',\n defaults: DEFAULT_VIEWS.map((view) => ({ id: view.id, value: view })),\n overrides: railOrderViews.map((view) => ({ id: view.id, value: view })),\n allowNew: true,\n validate: (entry) => {\n const view = entry.value\n if (typeof view.render !== 'function') return 'the view has no render function'\n if (typeof view.label !== 'string' || view.label.trim().length === 0)\n return 'the view has no label'\n return null\n },\n })\n\n // Rail order for landing: each rail view (resolved), deduped, then the built-ins\n // not in the rail (so Settings — always available — is the final landing fallback).\n const inRail = new Set<string>()\n const landing: AppViewModule[] = []\n for (const view of railOrderViews) {\n const resolvedView = resolved.get(view.id)\n if (resolvedView && !inRail.has(resolvedView.id)) {\n inRail.add(resolvedView.id)\n landing.push(resolvedView)\n }\n }\n for (const view of DEFAULT_VIEWS) if (!inRail.has(view.id)) landing.push(view)\n\n const registry: ViewRegistry = { get: (id) => resolved.get(id), views: landing }\n\n // Build the display tree. Each view id is emitted at most once (the first of a\n // duplicate wins, matching `resolveOverrides`); a view dropped by validation is\n // skipped here too.\n const emitted = new Set<string>()\n const toRailView = (view: AppViewModule): ResolvedRailView | null => {\n const resolvedView = resolved.get(view.id)\n if (!resolvedView || emitted.has(resolvedView.id)) return null\n emitted.add(resolvedView.id)\n return {\n kind: 'view',\n id: resolvedView.id,\n label: resolvedView.label,\n icon: resolvedView.icon,\n path: resolvedView.id,\n availability: moduleAvailability(project, resolvedView),\n ...(resolvedView.visible ? { visible: resolvedView.visible } : {}),\n }\n }\n\n const railNodes: ResolvedRailNode[] = []\n for (const node of nodes) {\n if (node.kind === 'separator') {\n railNodes.push({ kind: 'separator' })\n continue\n }\n if (node.kind === 'group') {\n const views = node.children.map((child) => toRailView(child.view)).filter(isRailView)\n if (views.length > 0) railNodes.push({ kind: 'group', label: node.label, views })\n continue\n }\n // A top-level view, with its nested children (CMS collections) attached. The\n // parent is emitted first so its id wins any duplicate; children follow in order.\n const view = toRailView(node.view)\n if (!view) continue\n const children = childNodesOf(node, collections)\n .map((child) => toRailView(child.view))\n .filter(isRailView)\n if (children.length > 0) view.children = children\n railNodes.push(view)\n }\n\n return { nodes: railNodes, registry, issues: resolved.issues }\n}\n\nfunction isRailView(view: ResolvedRailView | null): view is ResolvedRailView {\n return view !== null\n}\n\n/**\n * Evaluate a rail view's OPTIONAL nav-permission predicate (EC-240, AD-18):\n * default-open — absent → visible; present → the predicate decides against the\n * live role. UI reflection only; the server chokepoint is the real boundary and\n * a hidden view stays URL-addressable (EC-221).\n */\nexport function isNavViewVisible(\n view: Pick<ResolvedRailView, 'visible'>,\n ctx: NavPermissionContext,\n): boolean {\n return view.visible ? view.visible(ctx) : true\n}\n\n/** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */\nexport function activeViewIdFromPath(pathname: string): string | null {\n return pathname.split('/')[2] || null\n}\n\n/** The view a bare project URL should open to: the first navigable rail view (EC-221). */\nexport function landingViewId(nav: StudioNav, project: Project): string {\n return firstAvailableView(nav.registry.views, project)?.id ?? 'settings'\n}\n\n/**\n * The active project's resolved studio nav — the default rail or\n * `studioConfig.sidebar`, resolved over the built-in views through the EC-236 seam\n * (static per studio boot).\n */\nexport function useStudioNav(project: Project): StudioNav {\n // `resolveDeploymentSidebar`/`resolveDeploymentCollections` are config-holder reads\n // (stable per studio boot), so keying the memo on `project` alone is correct.\n return useMemo(\n () => resolveSidebar(resolveDeploymentSidebar(), project, resolveDeploymentCollections()),\n [project],\n )\n}\n"]}
1
+ {"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../src/components/app-views/sidebar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { resolveOverrides } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { resolveDeploymentCollections, resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace'\nimport {\n firstAvailableView,\n moduleAvailability,\n viewNode,\n type AppViewModule,\n type NavPermission,\n type NavPermissionContext,\n type SidebarNode,\n type SidebarViewNode,\n} from './module'\nimport { DEFAULT_VIEWS, checkSidebarIdKeyspace } from './default-modules'\n\n/**\n * EC-237 — resolving the configurable section rail, an instance of the EC-236\n * customization seam.\n *\n * A consuming repo supplies its rail as a `SidebarNode[]` (built from `nav.*`);\n * omitted → the studio derives the default rail from its built-in views. The rail\n * IS the set of views — each view node carries its view object directly — so this\n * resolver does two things at once:\n *\n * 1. **Renders the rail.** Walk the nodes into display-ready entries (views,\n * groups, separators) with per-view availability (EC-221) and a route path.\n * 2. **Derives the router registry.** Collect every rail view + the always-on\n * built-ins (so deep-links resolve even for views a rail omits), keyed by id.\n *\n * Only VIEWS occupy the id-keyspace; groups + separators are presentation with no\n * id, so a group label can never collide with a view (the old duplicate-id bug).\n * Views resolve through the shared {@link resolveOverrides} core: a duplicate view\n * id or a view missing `render`/`label` is a visible {@link CustomizationIssue} and\n * is dropped — never silent. Ids the builders DERIVED are screened BEFORE that core\n * ({@link checkSidebarIdKeyspace}, EC-476/477): to a generic seam an id is just a\n * string, so a slug that happened to equal a built-in id read as an intentional\n * replacement and swallowed the surface. The active rail item is matched by the view id in the\n * URL, so two views backed by the same surface (Content vs. a focused Categories)\n * highlight independently.\n */\n\n// --- Resolved (rail-ready) shapes ---\n\nexport interface ResolvedRailView {\n kind: 'view'\n /** The view id — the route segment and the active-state key. */\n id: string\n label: string\n icon: LucideIcon\n /** The `_splat` to navigate to (the view's home route = its id). */\n path: string\n availability: SectionAvailability\n /**\n * EC-240 (AD-18): the view's OPTIONAL, default-open nav-permission predicate,\n * carried through resolution so the rail can evaluate it against the live role\n * (which resolution — memoised on `project` alone — does not have). Absent →\n * always visible.\n */\n visible?: NavPermission\n /**\n * EC-293: nested sub-views (e.g. a CMS view's collections), rendered indented\n * under this view. Present only on a parent that expanded; leaves omit it.\n */\n children?: ResolvedRailView[]\n}\nexport interface ResolvedRailGroup {\n kind: 'group'\n label: string\n views: ResolvedRailView[]\n}\nexport interface ResolvedRailSeparator {\n kind: 'separator'\n}\nexport type ResolvedRailNode = ResolvedRailView | ResolvedRailGroup | ResolvedRailSeparator\n\n/** The router registry derived from the rail: id → view, plus the landing order. */\nexport interface ViewRegistry {\n /** Resolve a view id to its module (rail views + always-on built-ins). */\n get(id: string): AppViewModule | undefined\n /** Views in landing order (rail order first, then non-rail built-ins). */\n views: AppViewModule[]\n}\n\nexport interface StudioNav {\n /** The display-ready rail (for {@link import('../section-nav').SectionNav}). */\n nodes: ResolvedRailNode[]\n /** The router registry (for the screen resolver + breadcrumb). */\n registry: ViewRegistry\n /** Customization issues (duplicate view id, invalid view, derived-id collision) — visible, never silent. */\n issues: CustomizationIssue[]\n}\n\n/** The default rail: every non-hidden built-in view as a plain entry, in canonical order. */\nfunction defaultRail(): SidebarNode[] {\n return DEFAULT_VIEWS.filter((view) => !view.hidden).map(viewNode)\n}\n\n/**\n * The sub-views a view node contributes (EC-293): explicit `children`, else\n * `expand(collections)` (CMS auto-expansion), else none. Groups never nest, so this\n * applies to top-level view nodes only.\n */\nfunction childNodesOf(\n node: SidebarViewNode,\n collections: readonly CollectionDef[],\n): SidebarViewNode[] {\n return node.children ?? node.expand?.(collections) ?? []\n}\n\n/**\n * Every view the rail references, in rail order: each top-level view, then its nested\n * children, then group children (groups are flat — their children never nest). This\n * is the set fed to `resolveOverrides` + the derived registry.\n */\nfunction railViews(\n nodes: readonly SidebarNode[],\n collections: readonly CollectionDef[],\n): AppViewModule[] {\n const views: AppViewModule[] = []\n for (const node of nodes) {\n if (node.kind === 'view') {\n views.push(node.view)\n for (const child of childNodesOf(node, collections)) views.push(child.view)\n } else if (node.kind === 'group') {\n for (const child of node.children) views.push(child.view)\n }\n }\n return views\n}\n\n/**\n * Resolve a consumer rail (or the default rail) into the display tree + the router\n * registry through the EC-236 seam. Views are resolved over the built-in defaults\n * (so an omitted built-in stays routable and a rail `cms` REPLACES the default\n * one); groups/separators pass through as presentation.\n *\n * EC-293: a view node may nest. `collections` (config-sourced) materialises a CMS\n * parent's per-collection children; every child view joins the override pass + the\n * derived registry, so a nested `cms:<id>` lens routes and highlights like any view.\n */\nexport function resolveSidebar(\n entries: readonly SidebarNode[] | undefined,\n project: Project,\n collections: readonly CollectionDef[] = [],\n): StudioNav {\n const nodes = entries ?? defaultRail()\n const railOrderViews = railViews(nodes, collections)\n\n // EC-476/477: an id a builder DERIVED is not a request to replace whatever it happens\n // to equal. Screened BEFORE the seam, which sees only strings and so cannot tell an\n // intentional override from a coincidence of spelling (see `checkSidebarIdKeyspace`).\n const keyspace = checkSidebarIdKeyspace(railOrderViews, collections)\n\n // Resolve the rail views over the built-in registry: dedup by id + contract\n // check. Built-ins are the defaults (so omitted ones stay routable); rail views\n // are overrides (a same-id rail view replaces the default; a new id is added).\n const resolved = resolveOverrides<AppViewModule>({\n point: 'sidebar',\n defaults: DEFAULT_VIEWS.map((view) => ({ id: view.id, value: view })),\n overrides: keyspace.views.map((view) => ({ id: view.id, value: view })),\n allowNew: true,\n // EC-478: a rejection carries its catalog key as well as its English, so the\n // `{reason}` half of `invalid-override` localizes with the sentence around it.\n validate: (entry) => {\n const view = entry.value\n if (typeof view.render !== 'function')\n return {\n message: 'the view has no render function',\n key: 'customization.issue.reason.viewNoRender',\n }\n if (typeof view.label !== 'string' || view.label.trim().length === 0)\n return { message: 'the view has no label', key: 'customization.issue.reason.viewNoLabel' }\n return null\n },\n })\n\n // Rail order for landing: each rail view (resolved), deduped, then the built-ins\n // not in the rail (so Settings — always available — is the final landing fallback).\n const inRail = new Set<string>()\n const landing: AppViewModule[] = []\n for (const view of railOrderViews) {\n const resolvedView = resolved.get(view.id)\n if (resolvedView && !inRail.has(resolvedView.id)) {\n inRail.add(resolvedView.id)\n landing.push(resolvedView)\n }\n }\n for (const view of DEFAULT_VIEWS) if (!inRail.has(view.id)) landing.push(view)\n\n const registry: ViewRegistry = { get: (id) => resolved.get(id), views: landing }\n\n // Build the display tree. Each view id is emitted at most once (the first of a\n // duplicate wins, matching `resolveOverrides`); a view dropped by validation is\n // skipped here too.\n const emitted = new Set<string>()\n const toRailView = (view: AppViewModule): ResolvedRailView | null => {\n const resolvedView = resolved.get(view.id)\n if (!resolvedView || emitted.has(resolvedView.id)) return null\n emitted.add(resolvedView.id)\n return {\n kind: 'view',\n id: resolvedView.id,\n label: resolvedView.label,\n icon: resolvedView.icon,\n path: resolvedView.id,\n availability: moduleAvailability(project, resolvedView),\n ...(resolvedView.visible ? { visible: resolvedView.visible } : {}),\n }\n }\n\n const railNodes: ResolvedRailNode[] = []\n for (const node of nodes) {\n if (node.kind === 'separator') {\n railNodes.push({ kind: 'separator' })\n continue\n }\n if (node.kind === 'group') {\n const views = node.children.map((child) => toRailView(child.view)).filter(isRailView)\n if (views.length > 0) railNodes.push({ kind: 'group', label: node.label, views })\n continue\n }\n // A top-level view, with its nested children (CMS collections) attached. The\n // parent is emitted first so its id wins any duplicate; children follow in order.\n const view = toRailView(node.view)\n if (!view) continue\n const children = childNodesOf(node, collections)\n .map((child) => toRailView(child.view))\n .filter(isRailView)\n if (children.length > 0) view.children = children\n railNodes.push(view)\n }\n\n // Keyspace findings first: a derived-id collision is the CAUSE of any duplicate that\n // follows it, so it reads before the symptom at the rail foot.\n return { nodes: railNodes, registry, issues: [...keyspace.issues, ...resolved.issues] }\n}\n\nfunction isRailView(view: ResolvedRailView | null): view is ResolvedRailView {\n return view !== null\n}\n\n/**\n * Evaluate a rail view's OPTIONAL nav-permission predicate (EC-240, AD-18):\n * default-open — absent → visible; present → the predicate decides against the\n * live role. UI reflection only; the server chokepoint is the real boundary and\n * a hidden view stays URL-addressable (EC-221).\n */\nexport function isNavViewVisible(\n view: Pick<ResolvedRailView, 'visible'>,\n ctx: NavPermissionContext,\n): boolean {\n return view.visible ? view.visible(ctx) : true\n}\n\n/** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */\nexport function activeViewIdFromPath(pathname: string): string | null {\n return pathname.split('/')[2] || null\n}\n\n/** The view a bare project URL should open to: the first navigable rail view (EC-221). */\nexport function landingViewId(nav: StudioNav, project: Project): string {\n return firstAvailableView(nav.registry.views, project)?.id ?? 'settings'\n}\n\n/**\n * The active project's resolved studio nav — the default rail or\n * `studioConfig.sidebar`, resolved over the built-in views through the EC-236 seam\n * (static per studio boot).\n */\nexport function useStudioNav(project: Project): StudioNav {\n // `resolveDeploymentSidebar`/`resolveDeploymentCollections` are config-holder reads\n // (stable per studio boot), so keying the memo on `project` alone is correct.\n return useMemo(\n () => resolveSidebar(resolveDeploymentSidebar(), project, resolveDeploymentCollections()),\n [project],\n )\n}\n"]}