@elytracms/studio 0.0.17 → 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.
- package/dist/components/app-views/default-modules.d.ts +89 -26
- package/dist/components/app-views/default-modules.js +132 -49
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +59 -50
- package/dist/components/app-views/module.js +25 -43
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +54 -73
- package/dist/components/app-views/sidebar.js +92 -127
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +77 -0
- package/dist/components/app-views/studio-config.js +31 -0
- package/dist/components/app-views/studio-config.js.map +1 -0
- package/dist/components/cms/field-control.d.ts +8 -5
- package/dist/components/cms/field-control.js +21 -10
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.d.ts +28 -30
- package/dist/components/cms/field-renderer.js +15 -20
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/section-nav.d.ts +8 -7
- package/dist/components/section-nav.js +27 -26
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/studio-project-screen.js +16 -16
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +13 -13
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/customization.d.ts +48 -0
- package/dist/customization.js +46 -0
- package/dist/customization.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +2 -2
- package/dist/lib/cms-views/views-store.d.ts +1 -1
- package/dist/lib/workspace/index.d.ts +32 -24
- package/dist/lib/workspace/index.js +35 -27
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/match.d.ts +16 -16
- package/dist/router/match.js +17 -17
- package/dist/router/match.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- 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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
/**
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
20
|
+
return views;
|
|
59
21
|
}
|
|
60
22
|
/**
|
|
61
|
-
* Resolve a consumer
|
|
62
|
-
* through the
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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(
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
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) =>
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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: '
|
|
97
|
-
id:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
nodes.push({ kind: 'separator', id });
|
|
120
|
-
return;
|
|
84
|
+
railNodes.push({ kind: 'separator' });
|
|
85
|
+
continue;
|
|
121
86
|
}
|
|
122
87
|
if (node.kind === 'group') {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
142
|
-
* resolved over the
|
|
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
|
|
145
|
-
|
|
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
|
|
11
|
-
* field
|
|
12
|
-
* control: a registered renderer renders inside
|
|
13
|
-
* built-in + a visible issue); a rejected
|
|
14
|
-
* shows the built-in with the issue above it;
|
|
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
|
|
15
|
-
* field
|
|
16
|
-
* control: a registered renderer renders inside
|
|
17
|
-
* built-in + a visible issue); a rejected
|
|
18
|
-
* shows the built-in with the issue above it;
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
}
|