@elytracms/studio 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/components/app-views/default-modules.d.ts +102 -26
  2. package/dist/components/app-views/default-modules.js +166 -53
  3. package/dist/components/app-views/default-modules.js.map +1 -1
  4. package/dist/components/app-views/module.d.ts +81 -50
  5. package/dist/components/app-views/module.js +25 -43
  6. package/dist/components/app-views/module.js.map +1 -1
  7. package/dist/components/app-views/sidebar.d.ts +64 -73
  8. package/dist/components/app-views/sidebar.js +120 -125
  9. package/dist/components/app-views/sidebar.js.map +1 -1
  10. package/dist/components/app-views/studio-config.d.ts +77 -0
  11. package/dist/components/app-views/studio-config.js +31 -0
  12. package/dist/components/app-views/studio-config.js.map +1 -0
  13. package/dist/components/cms/cms-index.js +8 -8
  14. package/dist/components/cms/cms-index.js.map +1 -1
  15. package/dist/components/cms/collection-focus.d.ts +10 -0
  16. package/dist/components/cms/collection-focus.js +16 -0
  17. package/dist/components/cms/collection-focus.js.map +1 -0
  18. package/dist/components/cms/field-control.d.ts +8 -5
  19. package/dist/components/cms/field-control.js +21 -10
  20. package/dist/components/cms/field-control.js.map +1 -1
  21. package/dist/components/cms/field-renderer.d.ts +28 -30
  22. package/dist/components/cms/field-renderer.js +15 -20
  23. package/dist/components/cms/field-renderer.js.map +1 -1
  24. package/dist/components/section-nav.d.ts +8 -7
  25. package/dist/components/section-nav.js +36 -26
  26. package/dist/components/section-nav.js.map +1 -1
  27. package/dist/components/studio-project-screen.js +16 -16
  28. package/dist/components/studio-project-screen.js.map +1 -1
  29. package/dist/components/studio-shell.js +13 -13
  30. package/dist/components/studio-shell.js.map +1 -1
  31. package/dist/customization.d.ts +48 -0
  32. package/dist/customization.js +46 -0
  33. package/dist/customization.js.map +1 -0
  34. package/dist/lib/cms-views/view-model.d.ts +2 -2
  35. package/dist/lib/cms-views/views-store.d.ts +1 -1
  36. package/dist/lib/workspace/index.d.ts +32 -24
  37. package/dist/lib/workspace/index.js +35 -27
  38. package/dist/lib/workspace/index.js.map +1 -1
  39. package/dist/router/match.d.ts +16 -16
  40. package/dist/router/match.js +17 -17
  41. package/dist/router/match.js.map +1 -1
  42. package/dist/ui/badge.d.ts +1 -1
  43. package/package.json +6 -2
@@ -1,35 +1,38 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { LucideIcon } from 'lucide-react';
3
- import type { AppViewEntry, CustomizationIssue, Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core';
3
+ import type { Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core';
4
+ import type { CollectionDef } from '@elytracms/core/cms-core';
4
5
  import type { StudioSearch } from '@elytracms/studio/router/search';
5
6
  /**
6
- * EC-239 — the app-view module contract, an instance of the EC-236 customization
7
- * seam.
7
+ * The studio **view** contract + the **sidebar node** model — the internal types
8
+ * the consumer-facing `nav.*` builders (EC-237/239, customization seam EC-236)
9
+ * produce.
8
10
  *
9
11
  * A studio surface (the CMS, Routes, Assets view, or a net-new one) is a
10
- * self-contained MODULE: id + display metadata + a React `render` + an optional
11
- * deep-link parser. The configurable sidebar (EC-237) mounts modules; the splat
12
- * router (`matchStudioRoute`) addresses one by its id as the first path segment
13
- * and hands its trailing segments to `parsePath`. The studio ships a DEFAULT
14
- * registry (so the stock studio needs zero config); a consuming repo adds or
15
- * replaces modules through `config.appViews`, resolved over the defaults by the
16
- * shared {@link resolveOverrides} core — an unknown/duplicate/contract-violating
17
- * entry is a visible issue, never a silent drop, and the default block is kept.
12
+ * self-contained {@link AppViewModule}: a stable id + display metadata + a React
13
+ * `render` + an optional deep-link parser. A {@link SidebarNode} is one rail entry
14
+ * — a view, a labelled group of views, or a separator — and a **view node carries
15
+ * its view object DIRECTLY**, not a string id. This is the redesign's core: there
16
+ * is no separate "register a view, then reference it by `moduleId`" indirection
17
+ * and no shared id-keyspace, so the section rail IS the set of views, the router
18
+ * registry is DERIVED from it, and the duplicate-id / both-highlighted class of
19
+ * bug is gone by construction.
18
20
  *
19
- * Reuse-not-copy (the EC-239 headline): a focused view like "Taxonomy" is the CMS
20
- * module instantiated with config (`cmsAppView({ collections })`), not a fork — so
21
- * the registry holds module INSTANCES and the same surface code backs many ids.
21
+ * The boundary principle: everything here is studio-only live code passed by
22
+ * reference (no string keys). A string only crosses the serializable boundary for
23
+ * field controls (`form.control`), where the field lives in delivery-shared schema.
22
24
  *
23
- * This module is the contract + the pure resolver only; the default registry and
24
- * the surfaces (which pull the heavy editor providers) live in `default-modules`.
25
+ * Reuse-not-copy (EC-239): a focused view like "Catalog" is the CMS surface
26
+ * instantiated with config (`nav.cms({ collections })`), not a fork — the same
27
+ * surface code backs many views, each its own addressable node.
25
28
  */
26
- /** The deep-link a module parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */
29
+ /** The deep-link a view parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */
27
30
  export type AppViewDeepLink = Record<string, string | null>;
28
- /** Everything a module's surface needs to render (EC-239). */
31
+ /** Everything a view's surface needs to render (EC-239). */
29
32
  export interface AppViewContext {
30
33
  project: Project;
31
34
  projectId: string;
32
- /** The module's parsed deep-link; `{}` for a flat view with no nested resource. */
35
+ /** The view's parsed deep-link; `{}` for a flat view with no nested resource. */
33
36
  deepLink: AppViewDeepLink;
34
37
  /** The validated `?…` search (forced state, focused field/asset, …). */
35
38
  search: StudioSearch;
@@ -40,54 +43,82 @@ export interface AppViewModule {
40
43
  label: string;
41
44
  description: string;
42
45
  icon: LucideIcon;
43
- /** Render the surface; it mounts its own providers/gates (like the old resolver case bodies). */
46
+ /** Render the surface; it mounts its own providers/gates. */
44
47
  render: (ctx: AppViewContext) => ReactNode;
45
48
  /**
46
- * Parse this module's trailing path segments into a deep-link, or `null` for an
47
- * unroutable path (→ not-found). Omitted → a FLAT module ({@link flatPath}): only
49
+ * Parse this view's trailing path segments into a deep-link, or `null` for an
50
+ * unroutable path (→ not-found). Omitted → a FLAT view ({@link flatPath}): only
48
51
  * the bare path is valid, any trailing segment is not-found.
49
52
  */
50
53
  parsePath?: (segments: string[]) => AppViewDeepLink | null;
51
54
  /**
52
- * The availability section this module rides on (EC-221 gating): an
53
- * `unavailable` section hides the module from the rail. Omitted → always
54
- * available (a custom module defaults to visible).
55
+ * The availability section this view rides on (EC-221 gating): an `unavailable`
56
+ * section hides the view from the rail. Omitted → always available (a custom
57
+ * view defaults to visible).
55
58
  */
56
59
  availabilitySection?: SectionId;
57
- /** A redirect-only / utility module the stock rail does not list (e.g. `builder`). */
60
+ /** A redirect-only / utility view the stock rail does not list (e.g. `builder`). */
58
61
  hidden?: boolean;
59
62
  }
60
- /** A flat module: only the bare path resolves; any trailing segment is not-found. */
63
+ /**
64
+ * A rail entry that mounts a view. Carries the view object directly — no string id.
65
+ *
66
+ * EC-293: a view node may NEST one level. `children` is a static list of sub-views;
67
+ * `expand` derives them at resolve time from the project's collections (CMS
68
+ * auto-expand). Children render indented under the parent — indentation signals
69
+ * parent→child, distinct from a flat labelled {@link SidebarGroupNode}. `children`
70
+ * (explicit) wins over `expand` when both are set.
71
+ */
72
+ export interface SidebarViewNode {
73
+ kind: 'view';
74
+ view: AppViewModule;
75
+ /** Static sub-views, rendered indented under this view (one level of nesting). */
76
+ children?: SidebarViewNode[];
77
+ /**
78
+ * Resolve-time children from the project's collections (e.g. CMS expanding each
79
+ * collection into a focused-lens child). Run by `resolveSidebar` with the
80
+ * config-sourced collections; the result renders as {@link children}.
81
+ */
82
+ expand?: (collections: readonly CollectionDef[]) => SidebarViewNode[];
83
+ }
84
+ /**
85
+ * A labelled group of view entries. Groups are presentation only — never routable,
86
+ * and **flat**: their children render aligned with the top-level items (the heading
87
+ * is the only grouping signal). Group children do not nest — use a parent-with-
88
+ * children ({@link SidebarViewNode.expand}/`children`) for nesting (EC-293).
89
+ */
90
+ export interface SidebarGroupNode {
91
+ kind: 'group';
92
+ label: string;
93
+ children: SidebarViewNode[];
94
+ }
95
+ /** A divider between rail sections. */
96
+ export interface SidebarSeparatorNode {
97
+ kind: 'separator';
98
+ }
99
+ export type SidebarNode = SidebarViewNode | SidebarGroupNode | SidebarSeparatorNode;
100
+ /** Wrap a view as a rail entry. */
101
+ export declare function viewNode(view: AppViewModule): SidebarViewNode;
102
+ /** A labelled group of view entries (sub-views under a header). */
103
+ export declare function groupNode(label: string, children: SidebarViewNode[]): SidebarGroupNode;
104
+ /** A rail divider. */
105
+ export declare function separatorNode(): SidebarSeparatorNode;
106
+ /** A flat view: only the bare path resolves; any trailing segment is not-found. */
61
107
  export declare function flatPath(segments: string[]): AppViewDeepLink | null;
62
108
  /** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */
63
109
  export declare function cmsDeepLink(segments: string[]): AppViewDeepLink | null;
64
110
  /** Components arity: `[]` index · `[id]` detail; deeper is not-found. */
65
111
  export declare function componentsDeepLink(segments: string[]): AppViewDeepLink | null;
66
- export interface ResolvedAppViews {
67
- /** Default + custom modules, defaults first in registry order, additions appended. */
68
- modules: AppViewModule[];
69
- /** Resolve one id to its module — a custom override beats the default. */
70
- get(id: string): AppViewModule | undefined;
71
- /** Resolution issues (unknown target is impossible here — additive; duplicate / invalid). */
72
- issues: CustomizationIssue[];
73
- }
74
- /**
75
- * Resolve the config's custom app-view modules over the studio's default registry
76
- * through the shared customization core (EC-236): an additive registry keyed by
77
- * module id, rejecting a module with no `render` / no `label` (or a duplicate id)
78
- * with a visible issue and keeping the default. Pure — the React payloads are
79
- * opaque here.
80
- */
81
- export declare function resolveAppViews(defaults: readonly AppViewModule[], entries?: readonly AppViewEntry[]): ResolvedAppViews;
82
112
  /**
83
- * A module's availability for a project (EC-221): the section it rides on, or
84
- * always `available` when it declares none (a custom module). Drives both the
85
- * rail (hide `unavailable`) and the bare-project landing target.
113
+ * A view's availability for a project (EC-221): the section it rides on, or always
114
+ * `available` when it declares none (a custom view). Drives both the rail (hide
115
+ * `unavailable`) and the bare-project landing target.
86
116
  */
87
117
  export declare function moduleAvailability(project: Project, module: AppViewModule): SectionAvailability;
88
118
  /**
89
- * The module a project should open to from its bare URL: the first listed
90
- * (non-hidden), non-`unavailable` module in registry order. There is always at
91
- * least one (Settings is always available), so this never returns `null`.
119
+ * The view a project should open to from its bare URL: the first listed
120
+ * (non-hidden), non-`unavailable` view in rail order. Callers pass the resolved
121
+ * registry's view list (rail order); `undefined` only if every view is
122
+ * hidden/unavailable (the resolver guarantees Settings, so in practice never).
92
123
  */
93
- export declare function firstAvailableModule(modules: readonly AppViewModule[], project: Project): string;
124
+ export declare function firstAvailableView(views: readonly AppViewModule[], project: Project): AppViewModule | undefined;
@@ -1,6 +1,18 @@
1
- import { availabilityOf, resolveOverrides } from '@elytracms/core/studio-core';
2
- // --- Deep-link parsers (shared by the default modules + reused by factories) ---
3
- /** A flat module: only the bare path resolves; any trailing segment is not-found. */
1
+ import { availabilityOf } from '@elytracms/core/studio-core';
2
+ /** Wrap a view as a rail entry. */
3
+ export function viewNode(view) {
4
+ return { kind: 'view', view };
5
+ }
6
+ /** A labelled group of view entries (sub-views under a header). */
7
+ export function groupNode(label, children) {
8
+ return { kind: 'group', label, children };
9
+ }
10
+ /** A rail divider. */
11
+ export function separatorNode() {
12
+ return { kind: 'separator' };
13
+ }
14
+ // --- Deep-link parsers (shared by the built-in views + reused by factories) ---
15
+ /** A flat view: only the bare path resolves; any trailing segment is not-found. */
4
16
  export function flatPath(segments) {
5
17
  return segments.length === 0 ? {} : null;
6
18
  }
@@ -22,41 +34,11 @@ export function componentsDeepLink(segments) {
22
34
  return { componentId: segments[0] };
23
35
  return null;
24
36
  }
37
+ // --- Availability (EC-221) ---
25
38
  /**
26
- * Resolve the config's custom app-view modules over the studio's default registry
27
- * through the shared customization core (EC-236): an additive registry keyed by
28
- * module id, rejecting a module with no `render` / no `label` (or a duplicate id)
29
- * with a visible issue and keeping the default. Pure — the React payloads are
30
- * opaque here.
31
- */
32
- export function resolveAppViews(defaults, entries = []) {
33
- const resolved = resolveOverrides({
34
- point: 'appViews',
35
- defaults: defaults.map((module) => ({ id: module.id, value: module })),
36
- overrides: entries.map((entry) => {
37
- const module = entry;
38
- return { id: module.id, value: module };
39
- }),
40
- allowNew: true,
41
- validate: (entry) => {
42
- const module = entry.value;
43
- if (typeof module.render !== 'function')
44
- return 'the module has no render function';
45
- if (typeof module.label !== 'string' || module.label.trim().length === 0)
46
- return 'the module has no label';
47
- return null;
48
- },
49
- });
50
- return {
51
- modules: resolved.entries.map((entry) => entry.value),
52
- get: (id) => resolved.get(id),
53
- issues: resolved.issues,
54
- };
55
- }
56
- /**
57
- * A module's availability for a project (EC-221): the section it rides on, or
58
- * always `available` when it declares none (a custom module). Drives both the
59
- * rail (hide `unavailable`) and the bare-project landing target.
39
+ * A view's availability for a project (EC-221): the section it rides on, or always
40
+ * `available` when it declares none (a custom view). Drives both the rail (hide
41
+ * `unavailable`) and the bare-project landing target.
60
42
  */
61
43
  export function moduleAvailability(project, module) {
62
44
  return module.availabilitySection
@@ -64,12 +46,12 @@ export function moduleAvailability(project, module) {
64
46
  : 'available';
65
47
  }
66
48
  /**
67
- * The module a project should open to from its bare URL: the first listed
68
- * (non-hidden), non-`unavailable` module in registry order. There is always at
69
- * least one (Settings is always available), so this never returns `null`.
49
+ * The view a project should open to from its bare URL: the first listed
50
+ * (non-hidden), non-`unavailable` view in rail order. Callers pass the resolved
51
+ * registry's view list (rail order); `undefined` only if every view is
52
+ * hidden/unavailable (the resolver guarantees Settings, so in practice never).
70
53
  */
71
- export function firstAvailableModule(modules, project) {
72
- const found = modules.find((module) => !module.hidden && moduleAvailability(project, module) !== 'unavailable');
73
- return found?.id ?? 'settings';
54
+ export function firstAvailableView(views, project) {
55
+ return views.find((view) => !view.hidden && moduleAvailability(project, view) !== 'unavailable');
74
56
  }
75
57
  //# sourceMappingURL=module.js.map
@@ -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, resolveOverrides } from '@elytracms/core/studio-core'\nimport type {\n AppViewEntry,\n CustomizationIssue,\n Project,\n SectionAvailability,\n SectionId,\n} from '@elytracms/core/studio-core'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\n\n/**\n * EC-239 — the app-view module contract, an instance of the EC-236 customization\n * seam.\n *\n * A studio surface (the CMS, Routes, Assets view, or a net-new one) is a\n * self-contained MODULE: id + display metadata + a React `render` + an optional\n * deep-link parser. The configurable sidebar (EC-237) mounts modules; the splat\n * router (`matchStudioRoute`) addresses one by its id as the first path segment\n * and hands its trailing segments to `parsePath`. The studio ships a DEFAULT\n * registry (so the stock studio needs zero config); a consuming repo adds or\n * replaces modules through `config.appViews`, resolved over the defaults by the\n * shared {@link resolveOverrides} core — an unknown/duplicate/contract-violating\n * entry is a visible issue, never a silent drop, and the default block is kept.\n *\n * Reuse-not-copy (the EC-239 headline): a focused view like \"Taxonomy\" is the CMS\n * module instantiated with config (`cmsAppView({ collections })`), not a fork — so\n * the registry holds module INSTANCES and the same surface code backs many ids.\n *\n * This module is the contract + the pure resolver only; the default registry and\n * the surfaces (which pull the heavy editor providers) live in `default-modules`.\n */\n\n/** The deep-link a module parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */\nexport type AppViewDeepLink = Record<string, string | null>\n\n/** Everything a module's surface needs to render (EC-239). */\nexport interface AppViewContext {\n project: Project\n projectId: string\n /** The module'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 (like the old resolver case bodies). */\n render: (ctx: AppViewContext) => ReactNode\n /**\n * Parse this module's trailing path segments into a deep-link, or `null` for an\n * unroutable path (→ not-found). Omitted → a FLAT module ({@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 module rides on (EC-221 gating): an\n * `unavailable` section hides the module from the rail. Omitted → always\n * available (a custom module defaults to visible).\n */\n availabilitySection?: SectionId\n /** A redirect-only / utility module the stock rail does not list (e.g. `builder`). */\n hidden?: boolean\n}\n\n// --- Deep-link parsers (shared by the default modules + reused by factories) ---\n\n/** A flat module: 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// --- Resolution (EC-236 seam) ---\n\nexport interface ResolvedAppViews {\n /** Default + custom modules, defaults first in registry order, additions appended. */\n modules: AppViewModule[]\n /** Resolve one id to its module — a custom override beats the default. */\n get(id: string): AppViewModule | undefined\n /** Resolution issues (unknown target is impossible here — additive; duplicate / invalid). */\n issues: CustomizationIssue[]\n}\n\n/**\n * Resolve the config's custom app-view modules over the studio's default registry\n * through the shared customization core (EC-236): an additive registry keyed by\n * module id, rejecting a module with no `render` / no `label` (or a duplicate id)\n * with a visible issue and keeping the default. Pure — the React payloads are\n * opaque here.\n */\nexport function resolveAppViews(\n defaults: readonly AppViewModule[],\n entries: readonly AppViewEntry[] = [],\n): ResolvedAppViews {\n const resolved = resolveOverrides<AppViewModule>({\n point: 'appViews',\n defaults: defaults.map((module) => ({ id: module.id, value: module })),\n overrides: entries.map((entry) => {\n const module = entry as unknown as AppViewModule\n return { id: module.id, value: module }\n }),\n allowNew: true,\n validate: (entry) => {\n const module = entry.value as Partial<AppViewModule>\n if (typeof module.render !== 'function') return 'the module has no render function'\n if (typeof module.label !== 'string' || module.label.trim().length === 0)\n return 'the module has no label'\n return null\n },\n })\n return {\n modules: resolved.entries.map((entry) => entry.value),\n get: (id) => resolved.get(id),\n issues: resolved.issues,\n }\n}\n\n/**\n * A module's availability for a project (EC-221): the section it rides on, or\n * always `available` when it declares none (a custom module). Drives both the\n * rail (hide `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 module a project should open to from its bare URL: the first listed\n * (non-hidden), non-`unavailable` module in registry order. There is always at\n * least one (Settings is always available), so this never returns `null`.\n */\nexport function firstAvailableModule(modules: readonly AppViewModule[], project: Project): string {\n const found = modules.find(\n (module) => !module.hidden && moduleAvailability(project, module) !== 'unavailable',\n )\n return found?.id ?? 'settings'\n}\n"]}
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/components/app-views/module.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AA0G5D,mCAAmC;AACnC,MAAM,UAAU,QAAQ,CAAC,IAAmB;IAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAC/B,CAAC;AACD,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,QAA2B;IAClE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;AAC3C,CAAC;AACD,sBAAsB;AACtB,MAAM,UAAU,aAAa;IAC3B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;AAC9B,CAAC;AAED,iFAAiF;AAEjF,mFAAmF;AACnF,MAAM,UAAU,QAAQ,CAAC,QAAkB;IACzC,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AAC1C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,QAAkB;IAC5C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IAC1E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IAClF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC1F,OAAO,IAAI,CAAA;AACb,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,kBAAkB,CAAC,QAAkB;IACnD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IACvD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAE,EAAE,CAAA;IAC/D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,gCAAgC;AAEhC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAqB;IACxE,OAAO,MAAM,CAAC,mBAAmB;QAC/B,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC,mBAAmB,CAAC;QAClF,CAAC,CAAC,WAAW,CAAA;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAA+B,EAC/B,OAAgB;IAEhB,OAAO,KAAK,CAAC,IAAI,CACf,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,aAAa,CAC9E,CAAA;AACH,CAAC","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 { 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/** 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 /** 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// --- 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,95 +1,86 @@
1
1
  import type { LucideIcon } from 'lucide-react';
2
2
  import type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core';
3
- import { type ResolvedAppViews } from './module';
3
+ import type { CollectionDef } from '@elytracms/core/cms-core';
4
+ import { type AppViewModule, type SidebarNode } from './module';
4
5
  /**
5
- * EC-237 — the configurable sidebar, an instance of the EC-236 customization seam.
6
+ * EC-237 — resolving the configurable section rail, an instance of the EC-236
7
+ * customization seam.
6
8
  *
7
- * The section rail is a consumer-extendable ARRAY of nav entries — items (each
8
- * targeting an EC-239 app-view module, optionally deep-linked to a resource),
9
- * groups of sub-items, and separators. A consuming repo supplies its own array
10
- * through `config.sidebar`; omitted → the studio derives the default rail from its
11
- * module registry (every non-hidden module, in order). The supplied array is
12
- * resolved through the shared {@link resolveOverrides} core: a duplicate id, an
13
- * item whose target module isn't registered ("unknown view"), or a deep-link that
14
- * doesn't resolve becomes a visible {@link CustomizationIssue} and the entry is
15
- * dropped — never a silent failure. Sub-views (a focused "Taxonomy") are EC-239
16
- * modules; this file decides only how the rail PRESENTS and orders them.
9
+ * A consuming repo supplies its rail as a `SidebarNode[]` (built from `nav.*`);
10
+ * omitted → the studio derives the default rail from its built-in views. The rail
11
+ * IS the set of views — each view node carries its view object directly — so this
12
+ * resolver does two things at once:
13
+ *
14
+ * 1. **Renders the rail.** Walk the nodes into display-ready entries (views,
15
+ * groups, separators) with per-view availability (EC-221) and a route path.
16
+ * 2. **Derives the router registry.** Collect every rail view + the always-on
17
+ * built-ins (so deep-links resolve even for views a rail omits), keyed by id.
18
+ *
19
+ * Only VIEWS occupy the id-keyspace; groups + separators are presentation with no
20
+ * id, so a group label can never collide with a view (the old duplicate-id bug).
21
+ * Views resolve through the shared {@link resolveOverrides} core: a duplicate view
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
24
+ * URL, so two views backed by the same surface (Content vs. a focused Categories)
25
+ * highlight independently.
17
26
  */
18
- /** A nav item targeting an app-view module, optionally deep-linked to a resource. */
19
- export interface SidebarItemConfig {
20
- kind: 'item';
21
- /** Stable id (defaults to `moduleId` via {@link navItem}). */
22
- id: string;
23
- /** The app-view module this item mounts — must be in the registry. */
24
- moduleId: string;
25
- /** Overrides the module's rail label. */
26
- label?: string;
27
- /** Overrides the module's rail icon. */
28
- icon?: LucideIcon;
29
- /** Trailing path segments targeting a specific resource, e.g. `['category']`. */
30
- deepLink?: readonly string[];
31
- }
32
- export interface SidebarGroupConfig {
33
- kind: 'group';
34
- id: string;
35
- label: string;
36
- items: SidebarItemConfig[];
37
- }
38
- export interface SidebarSeparatorConfig {
39
- kind: 'separator';
40
- id?: string;
41
- }
42
- export type SidebarNodeConfig = SidebarItemConfig | SidebarGroupConfig | SidebarSeparatorConfig;
43
- export interface ResolvedSidebarItem {
44
- kind: 'item';
27
+ export interface ResolvedRailView {
28
+ kind: 'view';
29
+ /** The view id — the route segment and the active-state key. */
45
30
  id: string;
46
- moduleId: string;
47
31
  label: string;
48
32
  icon: LucideIcon;
49
- /** The `_splat` to navigate to (moduleId + deep-link segments). */
33
+ /** The `_splat` to navigate to (the view's home route = its id). */
50
34
  path: string;
51
35
  availability: SectionAvailability;
36
+ /**
37
+ * EC-293: nested sub-views (e.g. a CMS view's collections), rendered indented
38
+ * under this view. Present only on a parent that expanded; leaves omit it.
39
+ */
40
+ children?: ResolvedRailView[];
52
41
  }
53
- export interface ResolvedSidebarGroup {
42
+ export interface ResolvedRailGroup {
54
43
  kind: 'group';
55
- id: string;
56
44
  label: string;
57
- items: ResolvedSidebarItem[];
45
+ views: ResolvedRailView[];
58
46
  }
59
- export interface ResolvedSidebarSeparator {
47
+ export interface ResolvedRailSeparator {
60
48
  kind: 'separator';
61
- id: string;
62
49
  }
63
- export type ResolvedSidebarNode = ResolvedSidebarItem | ResolvedSidebarGroup | ResolvedSidebarSeparator;
64
- export interface ResolvedSidebar {
65
- nodes: ResolvedSidebarNode[];
50
+ export type ResolvedRailNode = ResolvedRailView | ResolvedRailGroup | ResolvedRailSeparator;
51
+ /** The router registry derived from the rail: id → view, plus the landing order. */
52
+ export interface ViewRegistry {
53
+ /** Resolve a view id to its module (rail views + always-on built-ins). */
54
+ get(id: string): AppViewModule | undefined;
55
+ /** Views in landing order (rail order first, then non-rail built-ins). */
56
+ views: AppViewModule[];
57
+ }
58
+ export interface StudioNav {
59
+ /** The display-ready rail (for {@link import('../section-nav').SectionNav}). */
60
+ nodes: ResolvedRailNode[];
61
+ /** The router registry (for the screen resolver + breadcrumb). */
62
+ registry: ViewRegistry;
63
+ /** Customization issues (duplicate view id, invalid view) — visible, never silent. */
66
64
  issues: CustomizationIssue[];
67
65
  }
68
- /** A nav item; `id` defaults to `moduleId` when omitted. */
69
- export declare function navItem(config: Omit<SidebarItemConfig, 'kind' | 'id'> & {
70
- id?: string;
71
- }): SidebarItemConfig;
72
- /** A labelled group of sub-items; `id` defaults to a slug of the label. */
73
- export declare function navGroup(label: string, items: SidebarItemConfig[], opts?: {
74
- id?: string;
75
- }): SidebarGroupConfig;
76
- /** A divider between rail sections. */
77
- export declare function navSeparator(id?: string): SidebarSeparatorConfig;
78
- /** Identity helper that types a consumer's `config.sidebar` array. */
79
- export declare function defineSidebar(nodes: SidebarNodeConfig[]): SidebarNodeConfig[];
80
66
  /**
81
- * Resolve a consumer sidebar (or the default rail) over the app-view registry
82
- * through the shared customization core (EC-236). Every node — items, group
83
- * headers, nested items, separators — is keyed by id and run through
84
- * {@link resolveOverrides} for dedup + per-node validation, so a duplicate id or
85
- * an invalid item is a visible issue and is dropped; the surviving nodes are
86
- * rebuilt into the rail tree (groups keep their surviving sub-items).
67
+ * Resolve a consumer rail (or the default rail) into the display tree + the router
68
+ * registry through the EC-236 seam. Views are resolved over the built-in defaults
69
+ * (so an omitted built-in stays routable and a rail `cms` REPLACES the default
70
+ * one); groups/separators pass through as presentation.
71
+ *
72
+ * EC-293: a view node may nest. `collections` (config-sourced) materialises a CMS
73
+ * parent's per-collection children; every child view joins the override pass + the
74
+ * derived registry, so a nested `cms:<id>` lens routes and highlights like any view.
87
75
  */
88
- export declare function resolveSidebar(registry: ResolvedAppViews, project: Project, entries?: readonly SidebarNodeConfig[]): ResolvedSidebar;
89
- /** The active module id parsed from a studio pathname `/<projectId>/<module>[/…]`. */
90
- export declare function activeModuleFromPath(pathname: string): string | null;
76
+ export declare function resolveSidebar(entries: readonly SidebarNode[] | undefined, project: Project, collections?: readonly CollectionDef[]): StudioNav;
77
+ /** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */
78
+ export declare function activeViewIdFromPath(pathname: string): string | null;
79
+ /** The view a bare project URL should open to: the first navigable rail view (EC-221). */
80
+ export declare function landingViewId(nav: StudioNav, project: Project): string;
91
81
  /**
92
- * The active project's resolved sidebar — the default rail or `config.sidebar`,
93
- * resolved over the module registry through the EC-236 seam (static per boot).
82
+ * The active project's resolved studio nav — the default rail or
83
+ * `studioConfig.sidebar`, resolved over the built-in views through the EC-236 seam
84
+ * (static per studio boot).
94
85
  */
95
- export declare function useSidebar(project: Project): ResolvedSidebar;
86
+ export declare function useStudioNav(project: Project): StudioNav;