@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,153 +1,148 @@
1
1
  import { useMemo } from 'react';
2
2
  import { resolveOverrides } from '@elytracms/core/studio-core';
3
- import { resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace';
4
- import { moduleAvailability } from './module';
5
- import { useAppViews } from './default-modules';
6
- // --- Builder helpers (the typed consumer API; carry live icons) ---
7
- /** A nav item; `id` defaults to `moduleId` when omitted. */
8
- export function navItem(config) {
9
- return {
10
- kind: 'item',
11
- id: config.id ?? config.moduleId,
12
- moduleId: config.moduleId,
13
- ...(config.label !== undefined ? { label: config.label } : {}),
14
- ...(config.icon !== undefined ? { icon: config.icon } : {}),
15
- ...(config.deepLink !== undefined ? { deepLink: config.deepLink } : {}),
16
- };
17
- }
18
- /** A labelled group of sub-items; `id` defaults to a slug of the label. */
19
- export function navGroup(label, items, opts = {}) {
20
- return { kind: 'group', id: opts.id ?? slugify(label), label, items };
21
- }
22
- /** A divider between rail sections. */
23
- export function navSeparator(id) {
24
- return { kind: 'separator', ...(id !== undefined ? { id } : {}) };
25
- }
26
- /** Identity helper that types a consumer's `config.sidebar` array. */
27
- export function defineSidebar(nodes) {
28
- return nodes;
29
- }
30
- function slugify(label) {
31
- return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'group';
32
- }
33
- // --- Resolution (EC-236 seam) ---
34
- /** The default rail: every non-hidden module as a plain item, in registry order. */
35
- function defaultSidebar(modules) {
36
- return modules
37
- .filter((module) => !module.hidden)
38
- .map((module) => ({ kind: 'item', id: module.id, moduleId: module.id }));
3
+ import { resolveDeploymentCollections, resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace';
4
+ import { firstAvailableView, moduleAvailability, viewNode, } from './module';
5
+ import { DEFAULT_VIEWS } from './default-modules';
6
+ /** The default rail: every non-hidden built-in view as a plain entry, in canonical order. */
7
+ function defaultRail() {
8
+ return DEFAULT_VIEWS.filter((view) => !view.hidden).map(viewNode);
39
9
  }
40
- /** The `_splat` path for an item (module id + its deep-link segments). */
41
- function itemPath(item) {
42
- return [item.moduleId, ...(item.deepLink ?? [])].join('/');
10
+ /**
11
+ * The sub-views a view node contributes (EC-293): explicit `children`, else
12
+ * `expand(collections)` (CMS auto-expansion), else none. Groups never nest, so this
13
+ * applies to top-level view nodes only.
14
+ */
15
+ function childNodesOf(node, collections) {
16
+ return node.children ?? node.expand?.(collections) ?? [];
43
17
  }
44
- /** Validate one item against the registry — unknown module or unresolvable deep-link. */
45
- function validateItem(registry, item) {
46
- const module = registry.get(item.moduleId);
47
- if (!module)
48
- return `targets module "${item.moduleId}", which is not registered`;
49
- if (item.deepLink && item.deepLink.length > 0) {
50
- const parse = module.parsePath;
51
- if (!parse) {
52
- return `deep-links into module "${item.moduleId}", which takes no path`;
18
+ /**
19
+ * Every view the rail references, in rail order: each top-level view, then its nested
20
+ * children, then group children (groups are flat — their children never nest). This
21
+ * is the set fed to `resolveOverrides` + the derived registry.
22
+ */
23
+ function railViews(nodes, collections) {
24
+ const views = [];
25
+ for (const node of nodes) {
26
+ if (node.kind === 'view') {
27
+ views.push(node.view);
28
+ for (const child of childNodesOf(node, collections))
29
+ views.push(child.view);
53
30
  }
54
- if (parse([...item.deepLink]) === null) {
55
- return `deep-link "${item.deepLink.join('/')}" does not resolve in module "${item.moduleId}"`;
31
+ else if (node.kind === 'group') {
32
+ for (const child of node.children)
33
+ views.push(child.view);
56
34
  }
57
35
  }
58
- return null;
36
+ return views;
59
37
  }
60
38
  /**
61
- * Resolve a consumer sidebar (or the default rail) over the app-view registry
62
- * through the shared customization core (EC-236). Every node — items, group
63
- * headers, nested items, separators — is keyed by id and run through
64
- * {@link resolveOverrides} for dedup + per-node validation, so a duplicate id or
65
- * an invalid item is a visible issue and is dropped; the surviving nodes are
66
- * rebuilt into the rail tree (groups keep their surviving sub-items).
39
+ * Resolve a consumer rail (or the default rail) into the display tree + the router
40
+ * registry through the EC-236 seam. Views are resolved over the built-in defaults
41
+ * (so an omitted built-in stays routable and a rail `cms` REPLACES the default
42
+ * one); groups/separators pass through as presentation.
43
+ *
44
+ * EC-293: a view node may nest. `collections` (config-sourced) materialises a CMS
45
+ * parent's per-collection children; every child view joins the override pass + the
46
+ * derived registry, so a nested `cms:<id>` lens routes and highlights like any view.
67
47
  */
68
- export function resolveSidebar(registry, project, entries) {
69
- const config = entries ?? defaultSidebar(registry.modules);
70
- const flat = [];
71
- config.forEach((node, index) => {
72
- if (node.kind === 'separator') {
73
- flat.push({ id: node.id ?? `__separator:${index}`, value: { node, isItem: false } });
74
- return;
75
- }
76
- if (node.kind === 'group') {
77
- flat.push({ id: node.id, value: { node, isItem: false } });
78
- node.items.forEach((item) => flat.push({ id: item.id, value: { node: item, isItem: true } }));
79
- return;
80
- }
81
- flat.push({ id: node.id, value: { node, isItem: true } });
82
- });
48
+ export function resolveSidebar(entries, project, collections = []) {
49
+ const nodes = entries ?? defaultRail();
50
+ const railOrderViews = railViews(nodes, collections);
51
+ // Resolve the rail views over the built-in registry: dedup by id + contract
52
+ // check. Built-ins are the defaults (so omitted ones stay routable); rail views
53
+ // are overrides (a same-id rail view replaces the default; a new id is added).
83
54
  const resolved = resolveOverrides({
84
55
  point: 'sidebar',
85
- defaults: [],
86
- overrides: flat,
56
+ defaults: DEFAULT_VIEWS.map((view) => ({ id: view.id, value: view })),
57
+ overrides: railOrderViews.map((view) => ({ id: view.id, value: view })),
87
58
  allowNew: true,
88
- validate: (entry) => entry.value.isItem
89
- ? validateItem(registry, entry.value.node)
90
- : null,
59
+ validate: (entry) => {
60
+ const view = entry.value;
61
+ if (typeof view.render !== 'function')
62
+ return 'the view has no render function';
63
+ if (typeof view.label !== 'string' || view.label.trim().length === 0)
64
+ return 'the view has no label';
65
+ return null;
66
+ },
91
67
  });
92
- const survived = (id) => resolved.get(id) !== undefined;
93
- const toItem = (item) => {
94
- const module = registry.get(item.moduleId);
68
+ // Rail order for landing: each rail view (resolved), deduped, then the built-ins
69
+ // not in the rail (so Settings — always available — is the final landing fallback).
70
+ const inRail = new Set();
71
+ const landing = [];
72
+ for (const view of railOrderViews) {
73
+ const resolvedView = resolved.get(view.id);
74
+ if (resolvedView && !inRail.has(resolvedView.id)) {
75
+ inRail.add(resolvedView.id);
76
+ landing.push(resolvedView);
77
+ }
78
+ }
79
+ for (const view of DEFAULT_VIEWS)
80
+ if (!inRail.has(view.id))
81
+ landing.push(view);
82
+ const registry = { get: (id) => resolved.get(id), views: landing };
83
+ // Build the display tree. Each view id is emitted at most once (the first of a
84
+ // duplicate wins, matching `resolveOverrides`); a view dropped by validation is
85
+ // skipped here too.
86
+ const emitted = new Set();
87
+ const toRailView = (view) => {
88
+ const resolvedView = resolved.get(view.id);
89
+ if (!resolvedView || emitted.has(resolvedView.id))
90
+ return null;
91
+ emitted.add(resolvedView.id);
95
92
  return {
96
- kind: 'item',
97
- id: item.id,
98
- moduleId: item.moduleId,
99
- label: item.label ?? module.label,
100
- icon: item.icon ?? module.icon,
101
- path: itemPath(item),
102
- availability: moduleAvailability(project, module),
93
+ kind: 'view',
94
+ id: resolvedView.id,
95
+ label: resolvedView.label,
96
+ icon: resolvedView.icon,
97
+ path: resolvedView.id,
98
+ availability: moduleAvailability(project, resolvedView),
103
99
  };
104
100
  };
105
- // Emit each surviving id at most once — `resolveOverrides` keeps the first of a
106
- // duplicate, so the rebuild must skip later occurrences of that same id too.
107
- const emitted = new Set();
108
- const claim = (id) => {
109
- if (!survived(id) || emitted.has(id))
110
- return false;
111
- emitted.add(id);
112
- return true;
113
- };
114
- const nodes = [];
115
- config.forEach((node, index) => {
101
+ const railNodes = [];
102
+ for (const node of nodes) {
116
103
  if (node.kind === 'separator') {
117
- const id = node.id ?? `__separator:${index}`;
118
- if (claim(id))
119
- nodes.push({ kind: 'separator', id });
120
- return;
104
+ railNodes.push({ kind: 'separator' });
105
+ continue;
121
106
  }
122
107
  if (node.kind === 'group') {
123
- if (!claim(node.id))
124
- return;
125
- const groupItems = node.items.filter((item) => claim(item.id)).map(toItem);
126
- if (groupItems.length > 0) {
127
- nodes.push({ kind: 'group', id: node.id, label: node.label, items: groupItems });
128
- }
129
- return;
108
+ const views = node.children.map((child) => toRailView(child.view)).filter(isRailView);
109
+ if (views.length > 0)
110
+ railNodes.push({ kind: 'group', label: node.label, views });
111
+ continue;
130
112
  }
131
- if (claim(node.id))
132
- nodes.push(toItem(node));
133
- });
134
- return { nodes, issues: resolved.issues };
113
+ // A top-level view, with its nested children (CMS collections) attached. The
114
+ // parent is emitted first so its id wins any duplicate; children follow in order.
115
+ const view = toRailView(node.view);
116
+ if (!view)
117
+ continue;
118
+ const children = childNodesOf(node, collections)
119
+ .map((child) => toRailView(child.view))
120
+ .filter(isRailView);
121
+ if (children.length > 0)
122
+ view.children = children;
123
+ railNodes.push(view);
124
+ }
125
+ return { nodes: railNodes, registry, issues: resolved.issues };
126
+ }
127
+ function isRailView(view) {
128
+ return view !== null;
135
129
  }
136
- /** The active module id parsed from a studio pathname `/<projectId>/<module>[/…]`. */
137
- export function activeModuleFromPath(pathname) {
130
+ /** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */
131
+ export function activeViewIdFromPath(pathname) {
138
132
  return pathname.split('/')[2] || null;
139
133
  }
134
+ /** The view a bare project URL should open to: the first navigable rail view (EC-221). */
135
+ export function landingViewId(nav, project) {
136
+ return firstAvailableView(nav.registry.views, project)?.id ?? 'settings';
137
+ }
140
138
  /**
141
- * The active project's resolved sidebar — the default rail or `config.sidebar`,
142
- * resolved over the module registry through the EC-236 seam (static per boot).
139
+ * The active project's resolved studio nav — the default rail or
140
+ * `studioConfig.sidebar`, resolved over the built-in views through the EC-236 seam
141
+ * (static per studio boot).
143
142
  */
144
- export function useSidebar(project) {
145
- const registry = useAppViews();
146
- return useMemo(() => {
147
- // Config carries the sidebar opaquely (`SidebarEntry` = object); the studio
148
- // owns + validates the rich item/group/separator shape, so cast it back here.
149
- const entries = resolveDeploymentSidebar();
150
- return resolveSidebar(registry, project, entries);
151
- }, [registry, project]);
143
+ export function useStudioNav(project) {
144
+ // `resolveDeploymentSidebar`/`resolveDeploymentCollections` are config-holder reads
145
+ // (stable per studio boot), so keying the memo on `project` alone is correct.
146
+ return useMemo(() => resolveSidebar(resolveDeploymentSidebar(), project, resolveDeploymentCollections()), [project]);
152
147
  }
153
148
  //# sourceMappingURL=sidebar.js.map
@@ -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 { resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace'\nimport { moduleAvailability, type AppViewModule, type ResolvedAppViews } from './module'\nimport { useAppViews } from './default-modules'\n\n/**\n * EC-237 — the configurable sidebar, an instance of the EC-236 customization seam.\n *\n * The section rail is a consumer-extendable ARRAY of nav entries — items (each\n * targeting an EC-239 app-view module, optionally deep-linked to a resource),\n * groups of sub-items, and separators. A consuming repo supplies its own array\n * through `config.sidebar`; omitted → the studio derives the default rail from its\n * module registry (every non-hidden module, in order). The supplied array is\n * resolved through the shared {@link resolveOverrides} core: a duplicate id, an\n * item whose target module isn't registered (\"unknown view\"), or a deep-link that\n * doesn't resolve becomes a visible {@link CustomizationIssue} and the entry is\n * dropped — never a silent failure. Sub-views (a focused \"Taxonomy\") are EC-239\n * modules; this file decides only how the rail PRESENTS and orders them.\n */\n\n// --- Consumer config shapes (typed builder helpers below) ---\n\n/** A nav item targeting an app-view module, optionally deep-linked to a resource. */\nexport interface SidebarItemConfig {\n kind: 'item'\n /** Stable id (defaults to `moduleId` via {@link navItem}). */\n id: string\n /** The app-view module this item mounts — must be in the registry. */\n moduleId: string\n /** Overrides the module's rail label. */\n label?: string\n /** Overrides the module's rail icon. */\n icon?: LucideIcon\n /** Trailing path segments targeting a specific resource, e.g. `['category']`. */\n deepLink?: readonly string[]\n}\n\nexport interface SidebarGroupConfig {\n kind: 'group'\n id: string\n label: string\n items: SidebarItemConfig[]\n}\n\nexport interface SidebarSeparatorConfig {\n kind: 'separator'\n id?: string\n}\n\nexport type SidebarNodeConfig = SidebarItemConfig | SidebarGroupConfig | SidebarSeparatorConfig\n\n// --- Resolved (rail-ready) shapes ---\n\nexport interface ResolvedSidebarItem {\n kind: 'item'\n id: string\n moduleId: string\n label: string\n icon: LucideIcon\n /** The `_splat` to navigate to (moduleId + deep-link segments). */\n path: string\n availability: SectionAvailability\n}\nexport interface ResolvedSidebarGroup {\n kind: 'group'\n id: string\n label: string\n items: ResolvedSidebarItem[]\n}\nexport interface ResolvedSidebarSeparator {\n kind: 'separator'\n id: string\n}\nexport type ResolvedSidebarNode =\n | ResolvedSidebarItem\n | ResolvedSidebarGroup\n | ResolvedSidebarSeparator\n\nexport interface ResolvedSidebar {\n nodes: ResolvedSidebarNode[]\n issues: CustomizationIssue[]\n}\n\n// --- Builder helpers (the typed consumer API; carry live icons) ---\n\n/** A nav item; `id` defaults to `moduleId` when omitted. */\nexport function navItem(\n config: Omit<SidebarItemConfig, 'kind' | 'id'> & { id?: string },\n): SidebarItemConfig {\n return {\n kind: 'item',\n id: config.id ?? config.moduleId,\n moduleId: config.moduleId,\n ...(config.label !== undefined ? { label: config.label } : {}),\n ...(config.icon !== undefined ? { icon: config.icon } : {}),\n ...(config.deepLink !== undefined ? { deepLink: config.deepLink } : {}),\n }\n}\n\n/** A labelled group of sub-items; `id` defaults to a slug of the label. */\nexport function navGroup(\n label: string,\n items: SidebarItemConfig[],\n opts: { id?: string } = {},\n): SidebarGroupConfig {\n return { kind: 'group', id: opts.id ?? slugify(label), label, items }\n}\n\n/** A divider between rail sections. */\nexport function navSeparator(id?: string): SidebarSeparatorConfig {\n return { kind: 'separator', ...(id !== undefined ? { id } : {}) }\n}\n\n/** Identity helper that types a consumer's `config.sidebar` array. */\nexport function defineSidebar(nodes: SidebarNodeConfig[]): SidebarNodeConfig[] {\n return nodes\n}\n\nfunction slugify(label: string): string {\n return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'group'\n}\n\n// --- Resolution (EC-236 seam) ---\n\n/** The default rail: every non-hidden module as a plain item, in registry order. */\nfunction defaultSidebar(modules: readonly AppViewModule[]): SidebarItemConfig[] {\n return modules\n .filter((module) => !module.hidden)\n .map((module) => ({ kind: 'item', id: module.id, moduleId: module.id }))\n}\n\n/** The `_splat` path for an item (module id + its deep-link segments). */\nfunction itemPath(item: SidebarItemConfig): string {\n return [item.moduleId, ...(item.deepLink ?? [])].join('/')\n}\n\n/** Validate one item against the registry — unknown module or unresolvable deep-link. */\nfunction validateItem(registry: ResolvedAppViews, item: SidebarItemConfig): string | null {\n const module = registry.get(item.moduleId)\n if (!module) return `targets module \"${item.moduleId}\", which is not registered`\n if (item.deepLink && item.deepLink.length > 0) {\n const parse = module.parsePath\n if (!parse) {\n return `deep-links into module \"${item.moduleId}\", which takes no path`\n }\n if (parse([...item.deepLink]) === null) {\n return `deep-link \"${item.deepLink.join('/')}\" does not resolve in module \"${item.moduleId}\"`\n }\n }\n return null\n}\n\n/**\n * Resolve a consumer sidebar (or the default rail) over the app-view registry\n * through the shared customization core (EC-236). Every node — items, group\n * headers, nested items, separators — is keyed by id and run through\n * {@link resolveOverrides} for dedup + per-node validation, so a duplicate id or\n * an invalid item is a visible issue and is dropped; the surviving nodes are\n * rebuilt into the rail tree (groups keep their surviving sub-items).\n */\nexport function resolveSidebar(\n registry: ResolvedAppViews,\n project: Project,\n entries?: readonly SidebarNodeConfig[],\n): ResolvedSidebar {\n const config = entries ?? defaultSidebar(registry.modules)\n\n // Flatten the tree into a single keyspace so dedup + validation route through\n // the shared core. Separators without an id get a positional one (never a user\n // collision); groups + items keep theirs. `value` carries the node for validate.\n type Flat = { node: SidebarNodeConfig; isItem: boolean }\n const flat: { id: string; value: Flat }[] = []\n config.forEach((node, index) => {\n if (node.kind === 'separator') {\n flat.push({ id: node.id ?? `__separator:${index}`, value: { node, isItem: false } })\n return\n }\n if (node.kind === 'group') {\n flat.push({ id: node.id, value: { node, isItem: false } })\n node.items.forEach((item) => flat.push({ id: item.id, value: { node: item, isItem: true } }))\n return\n }\n flat.push({ id: node.id, value: { node, isItem: true } })\n })\n\n const resolved = resolveOverrides<Flat>({\n point: 'sidebar',\n defaults: [],\n overrides: flat,\n allowNew: true,\n validate: (entry) =>\n entry.value.isItem\n ? validateItem(registry, entry.value.node as SidebarItemConfig)\n : null,\n })\n const survived = (id: string) => resolved.get(id) !== undefined\n\n const toItem = (item: SidebarItemConfig): ResolvedSidebarItem => {\n const module = registry.get(item.moduleId)!\n return {\n kind: 'item',\n id: item.id,\n moduleId: item.moduleId,\n label: item.label ?? module.label,\n icon: item.icon ?? module.icon,\n path: itemPath(item),\n availability: moduleAvailability(project, module),\n }\n }\n\n // Emit each surviving id at most once — `resolveOverrides` keeps the first of a\n // duplicate, so the rebuild must skip later occurrences of that same id too.\n const emitted = new Set<string>()\n const claim = (id: string) => {\n if (!survived(id) || emitted.has(id)) return false\n emitted.add(id)\n return true\n }\n\n const nodes: ResolvedSidebarNode[] = []\n config.forEach((node, index) => {\n if (node.kind === 'separator') {\n const id = node.id ?? `__separator:${index}`\n if (claim(id)) nodes.push({ kind: 'separator', id })\n return\n }\n if (node.kind === 'group') {\n if (!claim(node.id)) return\n const groupItems = node.items.filter((item) => claim(item.id)).map(toItem)\n if (groupItems.length > 0) {\n nodes.push({ kind: 'group', id: node.id, label: node.label, items: groupItems })\n }\n return\n }\n if (claim(node.id)) nodes.push(toItem(node))\n })\n\n return { nodes, issues: resolved.issues }\n}\n\n/** The active module id parsed from a studio pathname `/<projectId>/<module>[/…]`. */\nexport function activeModuleFromPath(pathname: string): string | null {\n return pathname.split('/')[2] || null\n}\n\n/**\n * The active project's resolved sidebar — the default rail or `config.sidebar`,\n * resolved over the module registry through the EC-236 seam (static per boot).\n */\nexport function useSidebar(project: Project): ResolvedSidebar {\n const registry = useAppViews()\n return useMemo(() => {\n // Config carries the sidebar opaquely (`SidebarEntry` = object); the studio\n // owns + validates the rich item/group/separator shape, so cast it back here.\n const entries = resolveDeploymentSidebar() as readonly SidebarNodeConfig[] | undefined\n return resolveSidebar(registry, project, entries)\n }, [registry, project])\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 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-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 }\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/** 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"]}
@@ -0,0 +1,77 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { FieldDef } from '@elytracms/core/cms-core';
3
+ import type { SidebarNode } from './module';
4
+ /**
5
+ * EC-236 — the studio customization config: the single, studio-only home for
6
+ * everything a consuming repo customizes about the builder UI.
7
+ *
8
+ * This is the "separate studio-only config solution": it lives in the consumer's
9
+ * `studio.config.tsx`, is `defineStudio({...})`-typed, and is passed to
10
+ * `createStudio(config, studioConfig)` at the mount — NEVER mixed into the shared
11
+ * `elytra.config.ts`, so the live React it carries (views, icons, field renderers,
12
+ * future branding) never reaches the `frontend/` delivery bundle.
13
+ *
14
+ * The customization PRINCIPLE it embodies: customization is **code passed
15
+ * directly**, not data referenced by a string key. Every field here holds live
16
+ * values/components by reference — `sidebar` is built from `nav.*` view objects,
17
+ * `fieldRenderers` maps a control name to a component. The only string indirection
18
+ * in the whole surface is `fieldRenderers`' key, and ONLY because its other end
19
+ * (`form.control` on a field) lives in the delivery-shared serializable schema and
20
+ * cannot import studio code. This config grows by adding direct-value keys
21
+ * (branding/logo/panels/theme), never by adding registries.
22
+ */
23
+ export interface StudioConfig {
24
+ /**
25
+ * The section rail (EC-237) as an ordered list of `nav.*` nodes — views, groups,
26
+ * and separators. Omitted → the studio derives the default rail from its built-in
27
+ * views. A rail node carries its view object directly; there are no string ids to
28
+ * keep in sync.
29
+ */
30
+ sidebar?: SidebarNode[];
31
+ /**
32
+ * Custom field renderers (EC-238) keyed by CONTROL name (`form.control`), each a
33
+ * React component replacing the built-in document-editor control. A field opts in
34
+ * per-field via `form: { control: '<name>' }` — so a renderer targets exactly the
35
+ * fields that ask for it, not every field of a datatype. This is the one
36
+ * boundary-crossing seam: the field names its control by string in the shared
37
+ * schema, this map binds that name to a studio-only component.
38
+ */
39
+ fieldRenderers?: Record<string, CustomFieldRenderer>;
40
+ }
41
+ /** The props a custom field renderer receives — identical to the built-in control. */
42
+ export interface CustomFieldRendererProps {
43
+ field: FieldDef;
44
+ value: unknown;
45
+ onChange: (value: unknown) => void;
46
+ readOnly: boolean;
47
+ id: string;
48
+ }
49
+ export type CustomFieldRenderer = (props: CustomFieldRendererProps) => ReactNode;
50
+ /**
51
+ * Typed identity helper for `studio.config.tsx` — keeps the config declarative
52
+ * (like {@link import('@elytracms/core/studio-core').defineElytraConfig}). No
53
+ * validation here; the studio resolves + validates the sidebar and field renderers
54
+ * through the EC-236 seam at mount, surfacing every issue visibly (never throws).
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * // studio.config.tsx
59
+ * import { defineStudio, nav } from '@elytracms/studio/customization'
60
+ * import { ColorField } from './fields/color'
61
+ *
62
+ * export default defineStudio({
63
+ * sidebar: [
64
+ * nav.cms({ label: 'Content' }),
65
+ * nav.group('Catalog', [
66
+ * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),
67
+ * nav.cms({ label: 'Categories', collection: 'productCategory' }),
68
+ * ]),
69
+ * nav.separator(),
70
+ * nav.assets(),
71
+ * nav.settings(),
72
+ * ],
73
+ * fieldRenderers: { color: ColorField },
74
+ * })
75
+ * ```
76
+ */
77
+ export declare function defineStudio(config: StudioConfig): StudioConfig;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Typed identity helper for `studio.config.tsx` — keeps the config declarative
3
+ * (like {@link import('@elytracms/core/studio-core').defineElytraConfig}). No
4
+ * validation here; the studio resolves + validates the sidebar and field renderers
5
+ * through the EC-236 seam at mount, surfacing every issue visibly (never throws).
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // studio.config.tsx
10
+ * import { defineStudio, nav } from '@elytracms/studio/customization'
11
+ * import { ColorField } from './fields/color'
12
+ *
13
+ * export default defineStudio({
14
+ * sidebar: [
15
+ * nav.cms({ label: 'Content' }),
16
+ * nav.group('Catalog', [
17
+ * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),
18
+ * nav.cms({ label: 'Categories', collection: 'productCategory' }),
19
+ * ]),
20
+ * nav.separator(),
21
+ * nav.assets(),
22
+ * nav.settings(),
23
+ * ],
24
+ * fieldRenderers: { color: ColorField },
25
+ * })
26
+ * ```
27
+ */
28
+ export function defineStudio(config) {
29
+ return config;
30
+ }
31
+ //# sourceMappingURL=studio-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AAoDA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport type { SidebarNode } from './module'\n\n/**\n * EC-236 — the studio customization config: the single, studio-only home for\n * everything a consuming repo customizes about the builder UI.\n *\n * This is the \"separate studio-only config solution\": it lives in the consumer's\n * `studio.config.tsx`, is `defineStudio({...})`-typed, and is passed to\n * `createStudio(config, studioConfig)` at the mount — NEVER mixed into the shared\n * `elytra.config.ts`, so the live React it carries (views, icons, field renderers,\n * future branding) never reaches the `frontend/` delivery bundle.\n *\n * The customization PRINCIPLE it embodies: customization is **code passed\n * directly**, not data referenced by a string key. Every field here holds live\n * values/components by reference — `sidebar` is built from `nav.*` view objects,\n * `fieldRenderers` maps a control name to a component. The only string indirection\n * in the whole surface is `fieldRenderers`' key, and ONLY because its other end\n * (`form.control` on a field) lives in the delivery-shared serializable schema and\n * cannot import studio code. This config grows by adding direct-value keys\n * (branding/logo/panels/theme), never by adding registries.\n */\nexport interface StudioConfig {\n /**\n * The section rail (EC-237) as an ordered list of `nav.*` nodes — views, groups,\n * and separators. Omitted → the studio derives the default rail from its built-in\n * views. A rail node carries its view object directly; there are no string ids to\n * keep in sync.\n */\n sidebar?: SidebarNode[]\n /**\n * Custom field renderers (EC-238) keyed by CONTROL name (`form.control`), each a\n * React component replacing the built-in document-editor control. A field opts in\n * per-field via `form: { control: '<name>' }` — so a renderer targets exactly the\n * fields that ask for it, not every field of a datatype. This is the one\n * boundary-crossing seam: the field names its control by string in the shared\n * schema, this map binds that name to a studio-only component.\n */\n fieldRenderers?: Record<string, CustomFieldRenderer>\n}\n\n/** The props a custom field renderer receives — identical to the built-in control. */\nexport interface CustomFieldRendererProps {\n field: FieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}\nexport type CustomFieldRenderer = (props: CustomFieldRendererProps) => ReactNode\n\n/**\n * Typed identity helper for `studio.config.tsx` — keeps the config declarative\n * (like {@link import('@elytracms/core/studio-core').defineElytraConfig}). No\n * validation here; the studio resolves + validates the sidebar and field renderers\n * through the EC-236 seam at mount, surfacing every issue visibly (never throws).\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n * ```\n */\nexport function defineStudio(config: StudioConfig): StudioConfig {\n return config\n}\n"]}
@@ -3,7 +3,7 @@ import { Database, FileCog } from 'lucide-react';
3
3
  import { isSingleton } from '@elytracms/core/cms-core';
4
4
  import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
5
5
  import { useCms } from '@elytracms/studio/components/cms/cms-store';
6
- import { CollectionsSidebar, focusCollectionList, } from '@elytracms/studio/components/cms/collections-sidebar';
6
+ import { focusCollectionList } from './collection-focus';
7
7
  import { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui';
8
8
  /**
9
9
  * The CMS section landing view (EC-223 Stage 6): the collection grid, shown when
@@ -14,12 +14,12 @@ import { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui';
14
14
  export function CmsIndex({ projectId, focusCollections, }) {
15
15
  const cms = useCms();
16
16
  const collections = focusCollectionList(cms.collections, focusCollections);
17
- return (_jsxs("div", { className: "mx-auto w-full max-w-3xl p-4 sm:p-6", children: [_jsx("div", { className: "mb-6 h-72 overflow-hidden rounded-lg border border-border md:hidden", children: _jsx(CollectionsSidebar, { projectId: projectId, focusCollections: focusCollections }) }), _jsxs(Stack, { gap: "6", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Heading, { size: "display", children: "Content" }), _jsx(Text, { tone: "muted", children: "Schema-driven collections. Open a collection to browse its documents as a table and edit them in the detail editor." })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: collections.map((collection) => {
18
- // EC-217: a singleton reads as one fixed document — a distinct icon
19
- // and a "Single document" note instead of a (always-1) row count.
20
- const singleton = isSingleton(collection);
21
- const Icon = singleton ? FileCog : Database;
22
- return (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.cmsCollection(collection.id) }, children: _jsxs(Card, { className: "flex items-center gap-3 p-4 transition-colors hover:border-primary/30", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground", children: _jsx(Icon, { className: "size-4" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: collection.form?.label ?? collection.id }), _jsxs(Code, { tone: "muted", className: "w-fit", children: [collection.id, " \u00B7 ", collection.fields.length, " fields"] })] }), _jsx(Spacer, {}), singleton ? (_jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", children: "Single document" })) : (_jsx(Text, { size: "sm", tone: "muted", mono: true, children: cms.documentsOf(collection.id).length }))] }) }, collection.id));
23
- }) })] })] }));
17
+ return (_jsx("div", { className: "mx-auto w-full max-w-3xl p-4 sm:p-6", children: _jsxs(Stack, { gap: "6", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Heading, { size: "display", children: "Content" }), _jsx(Text, { tone: "muted", children: "Schema-driven collections. Open a collection to browse its documents as a table and edit them in the detail editor." })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: collections.map((collection) => {
18
+ // EC-217: a singleton reads as one fixed document — a distinct icon
19
+ // and a "Single document" note instead of a (always-1) row count.
20
+ const singleton = isSingleton(collection);
21
+ const Icon = singleton ? FileCog : Database;
22
+ return (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: studioPath.cmsCollection(collection.id) }, children: _jsxs(Card, { className: "flex items-center gap-3 p-4 transition-colors hover:border-primary/30", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground", children: _jsx(Icon, { className: "size-4" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: collection.form?.label ?? collection.id }), _jsxs(Code, { tone: "muted", className: "w-fit", children: [collection.id, " \u00B7 ", collection.fields.length, " fields"] })] }), _jsx(Spacer, {}), singleton ? (_jsx(Text, { size: "xs", tone: "muted", className: "whitespace-nowrap", children: "Single document" })) : (_jsx(Text, { size: "sm", tone: "muted", mono: true, children: cms.documentsOf(collection.id).length }))] }) }, collection.id));
23
+ }) })] }) }));
24
24
  }
25
25
  //# sourceMappingURL=cms-index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cms-index.js","sourceRoot":"","sources":["../../../src/components/cms/cms-index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,4CAA4C,CAAA;AACnE,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,sDAAsD,CAAA;AAC7D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAE/E;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,SAAS,EACT,gBAAgB,GAIjB;IACC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,WAAW,GAAG,mBAAmB,CAAC,GAAG,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;IAE1E,OAAO,CACL,eAAK,SAAS,EAAC,qCAAqC,aAClD,cAAK,SAAS,EAAC,qEAAqE,YAClF,KAAC,kBAAkB,IAAC,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,GAAI,GAC5E,EACN,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,OAAO,IAAC,IAAI,EAAC,SAAS,wBAAkB,EACzC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,oIAGX,IACD,EACR,cAAK,SAAS,EAAC,2BAA2B,YACvC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE;4BAC9B,oEAAoE;4BACpE,kEAAkE;4BAClE,MAAM,SAAS,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;4BACzC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;4BAC3C,OAAO,CACL,KAAC,IAAI,IAEH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,YAEtE,MAAC,IAAI,IAAC,SAAS,EAAC,uEAAuE,aACrF,eAAM,SAAS,EAAC,mFAAmF,YACjG,KAAC,IAAI,IAAC,SAAS,EAAC,QAAQ,GAAG,GACtB,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,MAAM,EAAC,SAAS,EAAC,SAAS,aACnC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,UAAU,CAAC,IAAI,EAAE,KAAK,IAAI,UAAU,CAAC,EAAE,GACnC,EACP,MAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,OAAO,aACjC,UAAU,CAAC,EAAE,cAAK,UAAU,CAAC,MAAM,CAAC,MAAM,eACtC,IACD,EACR,KAAC,MAAM,KAAG,EACT,SAAS,CAAC,CAAC,CAAC,CACX,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,mBAAmB,gCAEnD,CACR,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,IAAI,kBAC9B,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,MAAM,GACjC,CACR,IACI,IA1BF,UAAU,CAAC,EAAE,CA2Bb,CACR,CAAA;wBACH,CAAC,CAAC,GACE,IACA,IACJ,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Database, FileCog } from 'lucide-react'\nimport { isSingleton } from '@elytracms/core/cms-core'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { useCms } from '@elytracms/studio/components/cms/cms-store'\nimport {\n CollectionsSidebar,\n focusCollectionList,\n} from '@elytracms/studio/components/cms/collections-sidebar'\nimport { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui'\n\n/**\n * The CMS section landing view (EC-223 Stage 6): the collection grid, shown when\n * no collection is selected. Takes `projectId` as a prop; the splat resolver\n * mounts it inside the CMS provider so {@link useCms} resolves. `focusCollections`\n * (EC-239) narrows the grid for a focused CMS module instance.\n */\nexport function CmsIndex({\n projectId,\n focusCollections,\n}: {\n projectId: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n const collections = focusCollectionList(cms.collections, focusCollections)\n\n return (\n <div className=\"mx-auto w-full max-w-3xl p-4 sm:p-6\">\n <div className=\"mb-6 h-72 overflow-hidden rounded-lg border border-border md:hidden\">\n <CollectionsSidebar projectId={projectId} focusCollections={focusCollections} />\n </div>\n <Stack gap=\"6\">\n <Stack gap=\"1\">\n <Heading size=\"display\">Content</Heading>\n <Text tone=\"muted\">\n Schema-driven collections. Open a collection to browse its documents as a table and edit\n them in the detail editor.\n </Text>\n </Stack>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {collections.map((collection) => {\n // EC-217: a singleton reads as one fixed document — a distinct icon\n // and a \"Single document\" note instead of a (always-1) row count.\n const singleton = isSingleton(collection)\n const Icon = singleton ? FileCog : Database\n return (\n <Link\n key={collection.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsCollection(collection.id) }}\n >\n <Card className=\"flex items-center gap-3 p-4 transition-colors hover:border-primary/30\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n <Icon className=\"size-4\" />\n </span>\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {collection.form?.label ?? collection.id}\n </Text>\n <Code tone=\"muted\" className=\"w-fit\">\n {collection.id} · {collection.fields.length} fields\n </Code>\n </Stack>\n <Spacer />\n {singleton ? (\n <Text size=\"xs\" tone=\"muted\" className=\"whitespace-nowrap\">\n Single document\n </Text>\n ) : (\n <Text size=\"sm\" tone=\"muted\" mono>\n {cms.documentsOf(collection.id).length}\n </Text>\n )}\n </Card>\n </Link>\n )\n })}\n </div>\n </Stack>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"cms-index.js","sourceRoot":"","sources":["../../../src/components/cms/cms-index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,4CAA4C,CAAA;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAE/E;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,SAAS,EACT,gBAAgB,GAIjB;IACC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,WAAW,GAAG,mBAAmB,CAAC,GAAG,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;IAE1E,OAAO,CACL,cAAK,SAAS,EAAC,qCAAqC,YAClD,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,OAAO,IAAC,IAAI,EAAC,SAAS,wBAAkB,EACzC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,oIAGX,IACD,EACR,cAAK,SAAS,EAAC,2BAA2B,YACvC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE;wBAC9B,oEAAoE;wBACpE,kEAAkE;wBAClE,MAAM,SAAS,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;wBACzC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;wBAC3C,OAAO,CACL,KAAC,IAAI,IAEH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,YAEtE,MAAC,IAAI,IAAC,SAAS,EAAC,uEAAuE,aACrF,eAAM,SAAS,EAAC,mFAAmF,YACjG,KAAC,IAAI,IAAC,SAAS,EAAC,QAAQ,GAAG,GACtB,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,MAAM,EAAC,SAAS,EAAC,SAAS,aACnC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,UAAU,CAAC,IAAI,EAAE,KAAK,IAAI,UAAU,CAAC,EAAE,GACnC,EACP,MAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,OAAO,aACjC,UAAU,CAAC,EAAE,cAAK,UAAU,CAAC,MAAM,CAAC,MAAM,eACtC,IACD,EACR,KAAC,MAAM,KAAG,EACT,SAAS,CAAC,CAAC,CAAC,CACX,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,mBAAmB,gCAEnD,CACR,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,IAAI,kBAC9B,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,MAAM,GACjC,CACR,IACI,IA1BF,UAAU,CAAC,EAAE,CA2Bb,CACR,CAAA;oBACH,CAAC,CAAC,GACE,IACA,GACJ,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Database, FileCog } from 'lucide-react'\nimport { isSingleton } from '@elytracms/core/cms-core'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { useCms } from '@elytracms/studio/components/cms/cms-store'\nimport { focusCollectionList } from './collection-focus'\nimport { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui'\n\n/**\n * The CMS section landing view (EC-223 Stage 6): the collection grid, shown when\n * no collection is selected. Takes `projectId` as a prop; the splat resolver\n * mounts it inside the CMS provider so {@link useCms} resolves. `focusCollections`\n * (EC-239) narrows the grid for a focused CMS module instance.\n */\nexport function CmsIndex({\n projectId,\n focusCollections,\n}: {\n projectId: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n const collections = focusCollectionList(cms.collections, focusCollections)\n\n return (\n <div className=\"mx-auto w-full max-w-3xl p-4 sm:p-6\">\n <Stack gap=\"6\">\n <Stack gap=\"1\">\n <Heading size=\"display\">Content</Heading>\n <Text tone=\"muted\">\n Schema-driven collections. Open a collection to browse its documents as a table and edit\n them in the detail editor.\n </Text>\n </Stack>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {collections.map((collection) => {\n // EC-217: a singleton reads as one fixed document — a distinct icon\n // and a \"Single document\" note instead of a (always-1) row count.\n const singleton = isSingleton(collection)\n const Icon = singleton ? FileCog : Database\n return (\n <Link\n key={collection.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsCollection(collection.id) }}\n >\n <Card className=\"flex items-center gap-3 p-4 transition-colors hover:border-primary/30\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n <Icon className=\"size-4\" />\n </span>\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {collection.form?.label ?? collection.id}\n </Text>\n <Code tone=\"muted\" className=\"w-fit\">\n {collection.id} · {collection.fields.length} fields\n </Code>\n </Stack>\n <Spacer />\n {singleton ? (\n <Text size=\"xs\" tone=\"muted\" className=\"whitespace-nowrap\">\n Single document\n </Text>\n ) : (\n <Text size=\"sm\" tone=\"muted\" mono>\n {cms.documentsOf(collection.id).length}\n </Text>\n )}\n </Card>\n </Link>\n )\n })}\n </div>\n </Stack>\n </div>\n )\n}\n"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * EC-239 focus lens (EC-293: relocated here from the retired `collections-sidebar`).
3
+ *
4
+ * A focused CMS view shows only `focus` collections, in that order (config order is
5
+ * honoured); ids not present are dropped. Unset → all collections. Used by the CMS
6
+ * index grid and by the rail's collection auto-expansion (`nav.cms`).
7
+ */
8
+ export declare function focusCollectionList<T extends {
9
+ id: string;
10
+ }>(all: readonly T[], focus?: readonly string[]): T[];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * EC-239 focus lens (EC-293: relocated here from the retired `collections-sidebar`).
3
+ *
4
+ * A focused CMS view shows only `focus` collections, in that order (config order is
5
+ * honoured); ids not present are dropped. Unset → all collections. Used by the CMS
6
+ * index grid and by the rail's collection auto-expansion (`nav.cms`).
7
+ */
8
+ export function focusCollectionList(all, focus) {
9
+ if (!focus)
10
+ return [...all];
11
+ const byId = new Map(all.map((collection) => [collection.id, collection]));
12
+ return focus
13
+ .map((id) => byId.get(id))
14
+ .filter((collection) => collection !== undefined);
15
+ }
16
+ //# sourceMappingURL=collection-focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-focus.js","sourceRoot":"","sources":["../../../src/components/cms/collection-focus.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAiB,EACjB,KAAyB;IAEzB,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,GAAG,GAAG,CAAC,CAAA;IAC3B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IAC1E,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;SACzB,MAAM,CAAC,CAAC,UAAU,EAAmB,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,CAAA;AACtE,CAAC","sourcesContent":["/**\n * EC-239 focus lens (EC-293: relocated here from the retired `collections-sidebar`).\n *\n * A focused CMS view shows only `focus` collections, in that order (config order is\n * honoured); ids not present are dropped. Unset → all collections. Used by the CMS\n * index grid and by the rail's collection auto-expansion (`nav.cms`).\n */\nexport function focusCollectionList<T extends { id: string }>(\n all: readonly T[],\n focus?: readonly string[],\n): T[] {\n if (!focus) return [...all]\n const byId = new Map(all.map((collection) => [collection.id, collection]))\n return focus\n .map((id) => byId.get(id))\n .filter((collection): collection is T => collection !== undefined)\n}\n"]}
@@ -7,11 +7,14 @@ interface FieldControlProps {
7
7
  id: string;
8
8
  }
9
9
  /**
10
- * The document-editor field control. Resolves a project's custom renderer for the
11
- * field type (EC-238, customization seam EC-236) and falls back to the built-in
12
- * control: a registered renderer renders inside an error boundary (a throw → the
13
- * built-in + a visible issue); a rejected registration (unknown type / non-component)
14
- * shows the built-in with the issue above it; otherwise the built-in renders alone.
10
+ * The document-editor field control. Resolves a project's custom renderer by the
11
+ * field's `form.control` (EC-238, customization seam EC-236) and falls back to the
12
+ * built-in control for the field's datatype: a registered renderer renders inside
13
+ * an error boundary (a throw → the built-in + a visible issue); a rejected
14
+ * registration (non-component) shows the built-in with the issue above it; a field
15
+ * naming a control with NO renderer and NO built-in name (a typo) shows the
16
+ * built-in with a loud issue — never a silent fall-through; otherwise the built-in
17
+ * renders alone.
15
18
  */
16
19
  export declare function FieldControl(props: FieldControlProps): import("react").JSX.Element;
17
20
  export {};