@elytracms/studio 0.0.16 → 0.0.18

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 (38) hide show
  1. package/dist/components/app-views/default-modules.d.ts +89 -26
  2. package/dist/components/app-views/default-modules.js +132 -49
  3. package/dist/components/app-views/default-modules.js.map +1 -1
  4. package/dist/components/app-views/module.d.ts +59 -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 +54 -73
  8. package/dist/components/app-views/sidebar.js +92 -127
  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/field-control.d.ts +8 -5
  14. package/dist/components/cms/field-control.js +21 -10
  15. package/dist/components/cms/field-control.js.map +1 -1
  16. package/dist/components/cms/field-renderer.d.ts +28 -30
  17. package/dist/components/cms/field-renderer.js +15 -20
  18. package/dist/components/cms/field-renderer.js.map +1 -1
  19. package/dist/components/section-nav.d.ts +8 -7
  20. package/dist/components/section-nav.js +27 -26
  21. package/dist/components/section-nav.js.map +1 -1
  22. package/dist/components/studio-project-screen.js +16 -16
  23. package/dist/components/studio-project-screen.js.map +1 -1
  24. package/dist/components/studio-shell.js +13 -13
  25. package/dist/components/studio-shell.js.map +1 -1
  26. package/dist/customization.d.ts +48 -0
  27. package/dist/customization.js +46 -0
  28. package/dist/customization.js.map +1 -0
  29. package/dist/lib/cms-views/view-model.d.ts +2 -2
  30. package/dist/lib/cms-views/views-store.d.ts +1 -1
  31. package/dist/lib/workspace/index.d.ts +32 -24
  32. package/dist/lib/workspace/index.js +35 -27
  33. package/dist/lib/workspace/index.js.map +1 -1
  34. package/dist/router/match.d.ts +16 -16
  35. package/dist/router/match.js +17 -17
  36. package/dist/router/match.js.map +1 -1
  37. package/dist/ui/badge.d.ts +1 -1
  38. package/package.json +6 -2
@@ -1,153 +1,118 @@
1
1
  import { useMemo } from 'react';
2
2
  import { resolveOverrides } from '@elytracms/core/studio-core';
3
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 }));
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('/');
43
- }
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`;
53
- }
54
- if (parse([...item.deepLink]) === null) {
55
- return `deep-link "${item.deepLink.join('/')}" does not resolve in module "${item.moduleId}"`;
56
- }
10
+ /** Every view in the rail (top-level + group children), in rail order. */
11
+ function railViews(nodes) {
12
+ const views = [];
13
+ for (const node of nodes) {
14
+ if (node.kind === 'view')
15
+ views.push(node.view);
16
+ else if (node.kind === 'group')
17
+ for (const child of node.children)
18
+ views.push(child.view);
57
19
  }
58
- return null;
20
+ return views;
59
21
  }
60
22
  /**
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).
23
+ * Resolve a consumer rail (or the default rail) into the display tree + the router
24
+ * registry through the EC-236 seam. Views are resolved over the built-in defaults
25
+ * (so an omitted built-in stays routable and a rail `cms` REPLACES the default
26
+ * one); groups/separators pass through as presentation.
67
27
  */
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
- });
28
+ export function resolveSidebar(entries, project) {
29
+ const nodes = entries ?? defaultRail();
30
+ const railOrderViews = railViews(nodes);
31
+ // Resolve the rail views over the built-in registry: dedup by id + contract
32
+ // check. Built-ins are the defaults (so omitted ones stay routable); rail views
33
+ // are overrides (a same-id rail view replaces the default; a new id is added).
83
34
  const resolved = resolveOverrides({
84
35
  point: 'sidebar',
85
- defaults: [],
86
- overrides: flat,
36
+ defaults: DEFAULT_VIEWS.map((view) => ({ id: view.id, value: view })),
37
+ overrides: railOrderViews.map((view) => ({ id: view.id, value: view })),
87
38
  allowNew: true,
88
- validate: (entry) => entry.value.isItem
89
- ? validateItem(registry, entry.value.node)
90
- : null,
39
+ validate: (entry) => {
40
+ const view = entry.value;
41
+ if (typeof view.render !== 'function')
42
+ return 'the view has no render function';
43
+ if (typeof view.label !== 'string' || view.label.trim().length === 0)
44
+ return 'the view has no label';
45
+ return null;
46
+ },
91
47
  });
92
- const survived = (id) => resolved.get(id) !== undefined;
93
- const toItem = (item) => {
94
- const module = registry.get(item.moduleId);
48
+ // Rail order for landing: each rail view (resolved), deduped, then the built-ins
49
+ // not in the rail (so Settings — always available — is the final landing fallback).
50
+ const inRail = new Set();
51
+ const landing = [];
52
+ for (const view of railOrderViews) {
53
+ const resolvedView = resolved.get(view.id);
54
+ if (resolvedView && !inRail.has(resolvedView.id)) {
55
+ inRail.add(resolvedView.id);
56
+ landing.push(resolvedView);
57
+ }
58
+ }
59
+ for (const view of DEFAULT_VIEWS)
60
+ if (!inRail.has(view.id))
61
+ landing.push(view);
62
+ const registry = { get: (id) => resolved.get(id), views: landing };
63
+ // Build the display tree. Each view id is emitted at most once (the first of a
64
+ // duplicate wins, matching `resolveOverrides`); a view dropped by validation is
65
+ // skipped here too.
66
+ const emitted = new Set();
67
+ const toRailView = (view) => {
68
+ const resolvedView = resolved.get(view.id);
69
+ if (!resolvedView || emitted.has(resolvedView.id))
70
+ return null;
71
+ emitted.add(resolvedView.id);
95
72
  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),
73
+ kind: 'view',
74
+ id: resolvedView.id,
75
+ label: resolvedView.label,
76
+ icon: resolvedView.icon,
77
+ path: resolvedView.id,
78
+ availability: moduleAvailability(project, resolvedView),
103
79
  };
104
80
  };
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) => {
81
+ const railNodes = [];
82
+ for (const node of nodes) {
116
83
  if (node.kind === 'separator') {
117
- const id = node.id ?? `__separator:${index}`;
118
- if (claim(id))
119
- nodes.push({ kind: 'separator', id });
120
- return;
84
+ railNodes.push({ kind: 'separator' });
85
+ continue;
121
86
  }
122
87
  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;
88
+ const views = node.children.map((child) => toRailView(child.view)).filter(isRailView);
89
+ if (views.length > 0)
90
+ railNodes.push({ kind: 'group', label: node.label, views });
91
+ continue;
130
92
  }
131
- if (claim(node.id))
132
- nodes.push(toItem(node));
133
- });
134
- return { nodes, issues: resolved.issues };
93
+ const view = toRailView(node.view);
94
+ if (view)
95
+ railNodes.push(view);
96
+ }
97
+ return { nodes: railNodes, registry, issues: resolved.issues };
135
98
  }
136
- /** The active module id parsed from a studio pathname `/<projectId>/<module>[/…]`. */
137
- export function activeModuleFromPath(pathname) {
99
+ function isRailView(view) {
100
+ return view !== null;
101
+ }
102
+ /** The active view id parsed from a studio pathname `/<projectId>/<viewId>[/…]`. */
103
+ export function activeViewIdFromPath(pathname) {
138
104
  return pathname.split('/')[2] || null;
139
105
  }
106
+ /** The view a bare project URL should open to: the first navigable rail view (EC-221). */
107
+ export function landingViewId(nav, project) {
108
+ return firstAvailableView(nav.registry.views, project)?.id ?? 'settings';
109
+ }
140
110
  /**
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).
111
+ * The active project's resolved studio nav — the default rail or
112
+ * `studioConfig.sidebar`, resolved over the built-in views through the EC-236 seam
113
+ * (static per studio boot).
143
114
  */
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]);
115
+ export function useStudioNav(project) {
116
+ return useMemo(() => resolveSidebar(resolveDeploymentSidebar(), project), [project]);
152
117
  }
153
118
  //# 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 { resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace'\nimport {\n firstAvailableView,\n moduleAvailability,\n viewNode,\n type AppViewModule,\n type SidebarNode,\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}\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/** Every view in the rail (top-level + group children), in rail order. */\nfunction railViews(nodes: readonly SidebarNode[]): AppViewModule[] {\n const views: AppViewModule[] = []\n for (const node of nodes) {\n if (node.kind === 'view') views.push(node.view)\n else if (node.kind === 'group') for (const child of node.children) views.push(child.view)\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 */\nexport function resolveSidebar(\n entries: readonly SidebarNode[] | undefined,\n project: Project,\n): StudioNav {\n const nodes = entries ?? defaultRail()\n const railOrderViews = railViews(nodes)\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 const view = toRailView(node.view)\n if (view) 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 return useMemo(() => resolveSidebar(resolveDeploymentSidebar(), project), [project])\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"]}
@@ -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 {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDown, ChevronUp, Image, Link2, Pencil, Plus, Trash2, X } from 'lucide-react';
3
3
  import { useMemo, useRef, useState } from 'react';
4
- import { descendantIds, isFieldHidden, parentRelationField } from '@elytracms/core/cms-core';
4
+ import { BUILTIN_FIELD_CONTROLS, descendantIds, isFieldHidden, parentRelationField } from '@elytracms/core/cms-core';
5
5
  import { Badge, Button, Card, Code, Field, Input, Row, Select, Spacer, Stack, Text } from '@elytracms/studio/ui';
6
6
  import { applyAssetSelection, assetIdsOf, singleAssetIdOf } from '../assets/asset-selection';
7
7
  import { fieldLabel, useCms } from './cms-store';
@@ -11,22 +11,33 @@ import { RelationPicker } from './relation-picker';
11
11
  import { AssetPicker } from './asset-picker';
12
12
  import { FieldRendererBoundary, FieldRendererIssue, useFieldRenderers, } from './field-renderer';
13
13
  /**
14
- * The document-editor field control. Resolves a project's custom renderer for the
15
- * field type (EC-238, customization seam EC-236) and falls back to the built-in
16
- * control: a registered renderer renders inside an error boundary (a throw → the
17
- * built-in + a visible issue); a rejected registration (unknown type / non-component)
18
- * shows the built-in with the issue above it; otherwise the built-in renders alone.
14
+ * The document-editor field control. Resolves a project's custom renderer by the
15
+ * field's `form.control` (EC-238, customization seam EC-236) and falls back to the
16
+ * built-in control for the field's datatype: a registered renderer renders inside
17
+ * an error boundary (a throw → the built-in + a visible issue); a rejected
18
+ * registration (non-component) shows the built-in with the issue above it; a field
19
+ * naming a control with NO renderer and NO built-in name (a typo) shows the
20
+ * built-in with a loud issue — never a silent fall-through; otherwise the built-in
21
+ * renders alone.
19
22
  */
20
23
  export function FieldControl(props) {
21
24
  const renderers = useFieldRenderers();
22
25
  const builtIn = _jsx(BuiltInFieldControl, { ...props });
23
- const Custom = renderers.get(props.field.type);
26
+ const control = props.field.form?.control;
27
+ const Custom = control ? renderers.get(control) : undefined;
24
28
  if (Custom) {
25
29
  return (_jsx(FieldRendererBoundary, { fallback: builtIn, issue: _jsx(FieldRendererIssue, { message: "Custom renderer crashed \u2014 showing the built-in control." }), children: _jsx(Custom, { ...props }) }));
26
30
  }
27
- const rejected = renderers.issues.find((issue) => issue.id === props.field.type);
28
- if (rejected) {
29
- return (_jsxs(Stack, { gap: "1", children: [_jsx(FieldRendererIssue, { message: rejected.message }), builtIn] }));
31
+ if (control) {
32
+ const rejected = renderers.issues.find((issue) => issue.id === control);
33
+ if (rejected) {
34
+ return (_jsxs(Stack, { gap: "1", children: [_jsx(FieldRendererIssue, { message: rejected.message }), builtIn] }));
35
+ }
36
+ // The field asks for a control with no registered renderer and no built-in
37
+ // name — surface it (a typo'd `control` would otherwise read as a plain input).
38
+ if (!BUILTIN_FIELD_CONTROLS.includes(control)) {
39
+ return (_jsxs(Stack, { gap: "1", children: [_jsx(FieldRendererIssue, { message: `No field renderer registered for control “${control}”.` }), builtIn] }));
40
+ }
30
41
  }
31
42
  return builtIn;
32
43
  }