@elytracms/studio 0.0.15 → 0.0.17
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 +41 -0
- package/dist/components/app-views/default-modules.js +201 -0
- package/dist/components/app-views/default-modules.js.map +1 -0
- package/dist/components/app-views/module.d.ts +93 -0
- package/dist/components/app-views/module.js +75 -0
- package/dist/components/app-views/module.js.map +1 -0
- package/dist/components/app-views/sidebar.d.ts +95 -0
- package/dist/components/app-views/sidebar.js +153 -0
- package/dist/components/app-views/sidebar.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +7 -1
- package/dist/components/builder/builder-store.js +1 -1
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
- package/dist/components/builder/canvas-breakpoints.js +55 -0
- package/dist/components/builder/canvas-breakpoints.js.map +1 -0
- package/dist/components/builder/canvas.js +14 -8
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +18 -17
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +23 -5
- package/dist/components/cms/bulk-actions.js +7 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +22 -6
- package/dist/components/cms/bulk-bar.js +46 -9
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/cms-index.d.ts +4 -2
- package/dist/components/cms/cms-index.js +6 -4
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +6 -5
- package/dist/components/cms/cms-store.js +51 -9
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -3
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/collections-sidebar.d.ts +10 -1
- package/dist/components/cms/collections-sidebar.js +13 -2
- package/dist/components/cms/collections-sidebar.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +11 -2
- package/dist/components/cms/field-control.js +22 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.d.ts +71 -0
- package/dist/components/cms/field-renderer.js +67 -0
- package/dist/components/cms/field-renderer.js.map +1 -0
- package/dist/components/cms/filter-builder.js +6 -28
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.d.ts +35 -0
- package/dist/components/cms/scalar-value-editor.js +43 -0
- package/dist/components/cms/scalar-value-editor.js.map +1 -0
- package/dist/components/project-session.js +8 -3
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/section-nav.d.ts +10 -10
- package/dist/components/section-nav.js +48 -25
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-surface.js +2 -3
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/studio-project-screen.d.ts +3 -4
- package/dist/components/studio-project-screen.js +29 -147
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +13 -9
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +19 -1
- package/dist/lib/data/collection-content.js +16 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +27 -1
- package/dist/lib/workspace/index.js +34 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/match.d.ts +22 -34
- package/dist/router/match.js +22 -40
- package/dist/router/match.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
import type { SectionId } from '@elytracms/core/studio-core';
|
|
3
|
+
import { type AppViewModule, type ResolvedAppViews } from './module';
|
|
4
|
+
/**
|
|
5
|
+
* EC-239 — the studio's DEFAULT app-view module registry, plus the surfaces the
|
|
6
|
+
* modules render. Each module here is the former `StudioSectionResolver` case body
|
|
7
|
+
* promoted to a self-contained, config-mountable unit; a consuming repo adds or
|
|
8
|
+
* replaces modules through `config.appViews` (resolved over this registry by
|
|
9
|
+
* {@link resolveAppViews}). The CMS surface is exposed as a factory
|
|
10
|
+
* ({@link cmsAppView}) so a focused view ("Taxonomy", "Pages") is the SAME module
|
|
11
|
+
* instantiated with config, not a copy — the EC-239 reuse proof.
|
|
12
|
+
*/
|
|
13
|
+
/** Config for a CMS-backed module instance — `collections` focuses the lens (EC-239). */
|
|
14
|
+
export interface CmsAppViewConfig {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
icon?: LucideIcon;
|
|
19
|
+
/** Restrict the surface to these collection ids (a focused "Taxonomy"/"Pages" lens). Omitted → all. */
|
|
20
|
+
collections?: readonly string[];
|
|
21
|
+
/** The availability section this instance rides on (EC-221); defaults to `cms`. */
|
|
22
|
+
availabilitySection?: SectionId;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build a CMS app-view module — the default `cms` view, or a focused instance.
|
|
26
|
+
* Every instance shares one surface ({@link CmsScreen}); `collections` narrows the
|
|
27
|
+
* collections sidebar + index to a subset, so "Taxonomy" is `cmsAppView({ id:
|
|
28
|
+
* 'taxonomy', collections: ['category', 'tag'] })`, not a forked screen.
|
|
29
|
+
*/
|
|
30
|
+
export declare function cmsAppView(config: CmsAppViewConfig): AppViewModule;
|
|
31
|
+
/**
|
|
32
|
+
* The studio's built-in modules in canonical rail order (components · CMS · routes
|
|
33
|
+
* · assets · extensions · settings), with the legacy `builder` redirect kept as a
|
|
34
|
+
* hidden, routable module (so `/builder?page=` still lands on the page document).
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEFAULT_APP_VIEWS: readonly AppViewModule[];
|
|
37
|
+
/**
|
|
38
|
+
* The active project's resolved app-view modules — the default registry composed
|
|
39
|
+
* with `config.appViews` through the EC-236 seam (config is static per studio boot).
|
|
40
|
+
*/
|
|
41
|
+
export declare function useAppViews(): ResolvedAppViews;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo } from 'react';
|
|
3
|
+
import { Blocks, Database, Image, Puzzle, Settings as SettingsIcon, Signpost } from 'lucide-react';
|
|
4
|
+
import { isSingleton, resolveSingletonDocument } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Navigate, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
6
|
+
import { Text } from '@elytracms/studio/ui';
|
|
7
|
+
import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
|
|
8
|
+
import { resolveDeploymentAppViews } from '@elytracms/studio/lib/workspace';
|
|
9
|
+
import { cmsDeepLink, componentsDeepLink, resolveAppViews, } from './module';
|
|
10
|
+
import { SectionSurface } from '../section-surface';
|
|
11
|
+
import { SectionStateView } from '../section-state';
|
|
12
|
+
import { SettingsForm } from '../settings-form';
|
|
13
|
+
import { AssetLibrary } from '../assets/asset-library';
|
|
14
|
+
import { RoutesShell } from '../routes/routes-shell';
|
|
15
|
+
import { ExtensionsView } from '../extensions/extensions-view';
|
|
16
|
+
import { RegistryBrowser } from '../components/registry-browser';
|
|
17
|
+
import { ComponentsIndex } from '../components/components-index';
|
|
18
|
+
import { ComponentDetail } from '../components/component-detail';
|
|
19
|
+
import { CmsSessionGate } from '../project-session';
|
|
20
|
+
import { CmsProvider, useCms } from '../cms/cms-store';
|
|
21
|
+
import { CollectionsSidebar } from '../cms/collections-sidebar';
|
|
22
|
+
import { CmsIndex } from '../cms/cms-index';
|
|
23
|
+
import { CollectionTable } from '../cms/collection-table';
|
|
24
|
+
import { DetailEditor } from '../cms/detail-editor';
|
|
25
|
+
import { OperationErrorBanner } from '../operation-error';
|
|
26
|
+
/**
|
|
27
|
+
* Build a CMS app-view module — the default `cms` view, or a focused instance.
|
|
28
|
+
* Every instance shares one surface ({@link CmsScreen}); `collections` narrows the
|
|
29
|
+
* collections sidebar + index to a subset, so "Taxonomy" is `cmsAppView({ id:
|
|
30
|
+
* 'taxonomy', collections: ['category', 'tag'] })`, not a forked screen.
|
|
31
|
+
*/
|
|
32
|
+
export function cmsAppView(config) {
|
|
33
|
+
return {
|
|
34
|
+
id: config.id,
|
|
35
|
+
label: config.label,
|
|
36
|
+
description: config.description ?? 'Model collections, fields, and documents.',
|
|
37
|
+
icon: config.icon ?? Database,
|
|
38
|
+
availabilitySection: config.availabilitySection ?? 'cms',
|
|
39
|
+
parsePath: cmsDeepLink,
|
|
40
|
+
render: (ctx) => (_jsx(CmsScreen, { project: ctx.project, collectionId: ctx.deepLink.collectionId ?? null, documentId: ctx.deepLink.documentId ?? null, state: ctx.search.state, field: ctx.search.field, focusCollections: config.collections })),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The studio's built-in modules in canonical rail order (components · CMS · routes
|
|
45
|
+
* · assets · extensions · settings), with the legacy `builder` redirect kept as a
|
|
46
|
+
* hidden, routable module (so `/builder?page=` still lands on the page document).
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_APP_VIEWS = [
|
|
49
|
+
{
|
|
50
|
+
id: 'components',
|
|
51
|
+
label: 'Components',
|
|
52
|
+
description: 'Browse the registry of primitives and project components.',
|
|
53
|
+
icon: Blocks,
|
|
54
|
+
availabilitySection: 'components',
|
|
55
|
+
parsePath: componentsDeepLink,
|
|
56
|
+
render: (ctx) => (_jsx(ComponentsScreen, { projectId: ctx.projectId, componentId: ctx.deepLink.componentId ?? null, state: ctx.search.state })),
|
|
57
|
+
},
|
|
58
|
+
cmsAppView({ id: 'cms', label: 'CMS' }),
|
|
59
|
+
{
|
|
60
|
+
id: 'routes',
|
|
61
|
+
label: 'Routes',
|
|
62
|
+
description: 'View the resolved URL routes & redirects (defined in elytra.config.ts).',
|
|
63
|
+
icon: Signpost,
|
|
64
|
+
// Routes/redirects are CMS routing concerns, so the surface rides on the CMS availability.
|
|
65
|
+
availabilitySection: 'cms',
|
|
66
|
+
render: (ctx) => ctx.search.state && ctx.search.state !== 'working' ? (_jsx(SectionStateView, { state: ctx.search.state, title: "Routes" })) : (_jsx(RoutesShell, { project: ctx.project })),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'assets',
|
|
70
|
+
label: 'Assets',
|
|
71
|
+
description: 'Manage uploaded media and metadata.',
|
|
72
|
+
icon: Image,
|
|
73
|
+
availabilitySection: 'assets',
|
|
74
|
+
render: (ctx) => (_jsx(SectionSurface, { project: ctx.project, section: "assets", forcedState: ctx.search.state, children: _jsx(AssetLibrary, { projectId: ctx.projectId, focusAssetId: ctx.search.asset }) })),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'extensions',
|
|
78
|
+
label: 'Extensions',
|
|
79
|
+
description: 'Install components, field types, adapters, and templates from the marketplace.',
|
|
80
|
+
icon: Puzzle,
|
|
81
|
+
availabilitySection: 'extensions',
|
|
82
|
+
render: (ctx) => ctx.search.state && ctx.search.state !== 'working' ? (_jsx(SectionStateView, { state: ctx.search.state, title: "Extensions" })) : (_jsx(ExtensionsView, {})),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'settings',
|
|
86
|
+
label: 'Settings',
|
|
87
|
+
description: 'Configure project metadata and locales.',
|
|
88
|
+
icon: SettingsIcon,
|
|
89
|
+
availabilitySection: 'settings',
|
|
90
|
+
render: (ctx) => (_jsx(SectionSurface, { project: ctx.project, section: "settings", forcedState: ctx.search.state, children: _jsx(SettingsForm, { project: ctx.project }) })),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'builder',
|
|
94
|
+
label: 'Builder',
|
|
95
|
+
description: 'Legacy builder route — redirects to the page document editor.',
|
|
96
|
+
icon: Blocks,
|
|
97
|
+
hidden: true,
|
|
98
|
+
render: (ctx) => (
|
|
99
|
+
// Retired surface (EC-187 slice 6): a page is a document in the `page`
|
|
100
|
+
// collection. `?page=<id>` lands on that document; else the page list.
|
|
101
|
+
_jsx(Navigate, { to: STUDIO_PROJECT_ROUTE, params: {
|
|
102
|
+
projectId: ctx.projectId,
|
|
103
|
+
_splat: ctx.search.page
|
|
104
|
+
? studioPath.cmsDocument('page', ctx.search.page)
|
|
105
|
+
: studioPath.cmsCollection('page'),
|
|
106
|
+
}, replace: true })),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* The active project's resolved app-view modules — the default registry composed
|
|
111
|
+
* with `config.appViews` through the EC-236 seam (config is static per studio boot).
|
|
112
|
+
*/
|
|
113
|
+
export function useAppViews() {
|
|
114
|
+
return useMemo(() => resolveAppViews(DEFAULT_APP_VIEWS, resolveDeploymentAppViews()), []);
|
|
115
|
+
}
|
|
116
|
+
// --- Surfaces (moved verbatim from the resolver; CMS gains a focus lens) ---
|
|
117
|
+
/**
|
|
118
|
+
* Components section (EC-050): a registry-browser sidebar beside the active
|
|
119
|
+
* screen — the index (no selection) or a component's detail. Forced states
|
|
120
|
+
* (?state=) replace the whole surface.
|
|
121
|
+
*/
|
|
122
|
+
function ComponentsScreen({ projectId, componentId, state, }) {
|
|
123
|
+
if (state && state !== 'working') {
|
|
124
|
+
return _jsx(SectionStateView, { state: state, title: "Components" });
|
|
125
|
+
}
|
|
126
|
+
return (_jsxs("div", { className: "flex h-full min-h-0", children: [_jsx("aside", { className: "hidden h-full min-h-0 w-[300px] shrink-0 border-r border-border md:block", children: _jsx(RegistryBrowser, { projectId: projectId, activeComponentId: componentId ?? undefined }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: componentId === null ? (_jsx(ComponentsIndex, { projectId: projectId })) : (_jsx(ComponentDetail, { projectId: projectId, componentId: componentId })) })] }));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* CMS section: hydrates the persisted (or kit-seeded) collections + documents,
|
|
130
|
+
* then renders the collections sidebar beside the active screen (index, a
|
|
131
|
+
* collection table, or a document editor). Forced states (?state=) replace the
|
|
132
|
+
* whole surface. `focusCollections` (EC-239) narrows the sidebar + index to a
|
|
133
|
+
* subset, so a focused module ("Taxonomy") reuses this surface with config.
|
|
134
|
+
*/
|
|
135
|
+
function CmsScreen({ project, collectionId, documentId, state, field, focusCollections, }) {
|
|
136
|
+
const projectId = project.id;
|
|
137
|
+
if (state && state !== 'working') {
|
|
138
|
+
return _jsx(SectionStateView, { state: state, title: "CMS" });
|
|
139
|
+
}
|
|
140
|
+
// A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.
|
|
141
|
+
const kit = getInstantiatedKit(projectId);
|
|
142
|
+
return (
|
|
143
|
+
// EC-177 gap 1: in Convex mode every CMS mount loads the CURRENT persisted
|
|
144
|
+
// schema + documents (fixtures only when nothing is persisted — first run).
|
|
145
|
+
// Playground renders synchronously with `null` → fixture seeds, as before.
|
|
146
|
+
_jsx(CmsSessionGate, { children: (cms) => (_jsx(CmsProvider, { projectId: projectId, initialCollections: cms?.collections ?? kit?.collections, initialDocuments: cms?.documents ?? kit?.documents, initialPublishedKeys: cms?.publishedKeys, initialLocaleConfig: cms
|
|
147
|
+
? // Hydrated sessions follow the project's persisted locale settings.
|
|
148
|
+
{ default: project.settings.defaultLocale, locales: [...project.settings.locales] }
|
|
149
|
+
: kit?.localeConfig, children: _jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx(CmsPersistErrorBanner, {}), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx("aside", { className: "hidden h-full min-h-0 w-[240px] shrink-0 border-r border-border md:block", children: _jsx(CollectionsSidebar, { projectId: projectId, activeCollectionId: collectionId ?? undefined, focusCollections: focusCollections }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: _jsx(CmsContent, { projectId: projectId, collectionId: collectionId, documentId: documentId, field: field, focusCollections: focusCollections }) })] })] }) }, projectId)) }));
|
|
150
|
+
}
|
|
151
|
+
/** Resolves the active CMS screen inside the provider (needs the live store). */
|
|
152
|
+
function CmsContent({ projectId, collectionId, documentId, field, focusCollections, }) {
|
|
153
|
+
const cms = useCms();
|
|
154
|
+
if (collectionId === null)
|
|
155
|
+
return _jsx(CmsIndex, { projectId: projectId, focusCollections: focusCollections });
|
|
156
|
+
if (documentId === null) {
|
|
157
|
+
const collection = cms.getCollection(collectionId);
|
|
158
|
+
// EC-274: a singleton is a UI lens — at the collection URL it opens its one
|
|
159
|
+
// document directly *while in its expected shape*, but degrades to the list
|
|
160
|
+
// (with a "for review" lane) on drift. SingletonGate decides per the data.
|
|
161
|
+
if (collection && isSingleton(collection)) {
|
|
162
|
+
return _jsx(SingletonGate, { projectId: projectId, collectionId: collectionId });
|
|
163
|
+
}
|
|
164
|
+
return _jsx(CollectionTable, { projectId: projectId, collectionId: collectionId });
|
|
165
|
+
}
|
|
166
|
+
return (_jsx(DetailEditor, { projectId: projectId, collectionId: collectionId, documentId: documentId, focusPath: field }));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* EC-274 — the singleton lens gate. At a singleton collection's URL, resolve its
|
|
170
|
+
* lone document and open it directly; on drift (> 1 document) fall back to the
|
|
171
|
+
* list with the "for review" lane so an editor can resolve the conflict.
|
|
172
|
+
*/
|
|
173
|
+
function SingletonGate({ projectId, collectionId }) {
|
|
174
|
+
const cms = useCms();
|
|
175
|
+
const collection = cms.getCollection(collectionId);
|
|
176
|
+
const docs = cms.documentsOf(collectionId);
|
|
177
|
+
const resolution = collection
|
|
178
|
+
? resolveSingletonDocument(collection, docs)
|
|
179
|
+
: { kind: 'empty' };
|
|
180
|
+
// Materialize the lone document on first visit (an empty draft, persisted like
|
|
181
|
+
// any create). Idempotent in the store; only fires while genuinely empty.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (resolution.kind === 'empty')
|
|
184
|
+
cms.ensureSingletonDocument(collectionId);
|
|
185
|
+
}, [resolution.kind, cms, collectionId]);
|
|
186
|
+
if (resolution.kind === 'ready') {
|
|
187
|
+
return (_jsx(DetailEditor, { projectId: projectId, collectionId: collectionId, documentId: resolution.doc.id }));
|
|
188
|
+
}
|
|
189
|
+
if (resolution.kind === 'drift') {
|
|
190
|
+
return _jsx(CollectionTable, { projectId: projectId, collectionId: collectionId });
|
|
191
|
+
}
|
|
192
|
+
return (_jsx("div", { className: "grid h-full place-items-center p-8", children: _jsxs(Text, { size: "sm", tone: "muted", children: ["Opening ", collection?.form?.label ?? collectionId, "\u2026"] }) }));
|
|
193
|
+
}
|
|
194
|
+
/** Failed/denied persisted CMS mutations are always visible (EC-141). */
|
|
195
|
+
function CmsPersistErrorBanner() {
|
|
196
|
+
const cms = useCms();
|
|
197
|
+
if (!cms.persistError)
|
|
198
|
+
return null;
|
|
199
|
+
return (_jsx("div", { className: "shrink-0 border-b border-border p-2", children: _jsx(OperationErrorBanner, { error: cms.persistError, onDismiss: cms.clearPersistError }) }));
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=default-modules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-modules.js","sourceRoot":"","sources":["../../../src/components/app-views/default-modules.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { Blocks, Database, Image, Puzzle, Settings as SettingsIcon, Signpost } from 'lucide-react'\nimport { isSingleton, resolveSingletonDocument } from '@elytracms/core/cms-core'\nimport type { Project, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { Navigate, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Text } from '@elytracms/studio/ui'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { resolveDeploymentAppViews } from '@elytracms/studio/lib/workspace'\nimport {\n cmsDeepLink,\n componentsDeepLink,\n resolveAppViews,\n type AppViewModule,\n type ResolvedAppViews,\n} from './module'\nimport { SectionSurface } from '../section-surface'\nimport { SectionStateView } from '../section-state'\nimport { SettingsForm } from '../settings-form'\nimport { AssetLibrary } from '../assets/asset-library'\nimport { RoutesShell } from '../routes/routes-shell'\nimport { ExtensionsView } from '../extensions/extensions-view'\nimport { RegistryBrowser } from '../components/registry-browser'\nimport { ComponentsIndex } from '../components/components-index'\nimport { ComponentDetail } from '../components/component-detail'\nimport { CmsSessionGate } from '../project-session'\nimport { CmsProvider, useCms } from '../cms/cms-store'\nimport { CollectionsSidebar } from '../cms/collections-sidebar'\nimport { CmsIndex } from '../cms/cms-index'\nimport { CollectionTable } from '../cms/collection-table'\nimport { DetailEditor } from '../cms/detail-editor'\nimport { OperationErrorBanner } from '../operation-error'\n\n/**\n * EC-239 — the studio's DEFAULT app-view module registry, plus the surfaces the\n * modules render. Each module here is the former `StudioSectionResolver` case body\n * promoted to a self-contained, config-mountable unit; a consuming repo adds or\n * replaces modules through `config.appViews` (resolved over this registry by\n * {@link resolveAppViews}). The CMS surface is exposed as a factory\n * ({@link cmsAppView}) so a focused view (\"Taxonomy\", \"Pages\") is the SAME module\n * instantiated with config, not a copy — the EC-239 reuse proof.\n */\n\n// --- The CMS surface, reusable via a config factory (the EC-239 reuse seam) ---\n\n/** Config for a CMS-backed module instance — `collections` focuses the lens (EC-239). */\nexport interface CmsAppViewConfig {\n id: string\n label: string\n description?: string\n icon?: LucideIcon\n /** Restrict the surface to these collection ids (a focused \"Taxonomy\"/\"Pages\" lens). Omitted → all. */\n collections?: readonly string[]\n /** The availability section this instance rides on (EC-221); defaults to `cms`. */\n availabilitySection?: SectionId\n}\n\n/**\n * Build a CMS app-view module — the default `cms` view, or a focused instance.\n * Every instance shares one surface ({@link CmsScreen}); `collections` narrows the\n * collections sidebar + index to a subset, so \"Taxonomy\" is `cmsAppView({ id:\n * 'taxonomy', collections: ['category', 'tag'] })`, not a forked screen.\n */\nexport function cmsAppView(config: CmsAppViewConfig): AppViewModule {\n return {\n id: config.id,\n label: config.label,\n description: config.description ?? 'Model collections, fields, and documents.',\n icon: config.icon ?? Database,\n availabilitySection: config.availabilitySection ?? 'cms',\n parsePath: cmsDeepLink,\n render: (ctx) => (\n <CmsScreen\n project={ctx.project}\n collectionId={ctx.deepLink.collectionId ?? null}\n documentId={ctx.deepLink.documentId ?? null}\n state={ctx.search.state}\n field={ctx.search.field}\n focusCollections={config.collections}\n />\n ),\n }\n}\n\n/**\n * The studio's built-in modules in canonical rail order (components · CMS · routes\n * · assets · extensions · settings), with the legacy `builder` redirect kept as a\n * hidden, routable module (so `/builder?page=` still lands on the page document).\n */\nexport const DEFAULT_APP_VIEWS: readonly AppViewModule[] = [\n {\n id: 'components',\n label: 'Components',\n description: 'Browse the registry of primitives and project components.',\n icon: Blocks,\n availabilitySection: 'components',\n parsePath: componentsDeepLink,\n render: (ctx) => (\n <ComponentsScreen\n projectId={ctx.projectId}\n componentId={ctx.deepLink.componentId ?? null}\n state={ctx.search.state}\n />\n ),\n },\n cmsAppView({ id: 'cms', label: 'CMS' }),\n {\n id: 'routes',\n label: 'Routes',\n description: 'View the resolved URL routes & redirects (defined in elytra.config.ts).',\n icon: Signpost,\n // Routes/redirects are CMS routing concerns, so the surface rides on the CMS availability.\n availabilitySection: 'cms',\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title=\"Routes\" />\n ) : (\n <RoutesShell project={ctx.project} />\n ),\n },\n {\n id: 'assets',\n label: 'Assets',\n description: 'Manage uploaded media and metadata.',\n icon: Image,\n availabilitySection: 'assets',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"assets\" forcedState={ctx.search.state}>\n {/* The AssetsProvider is mounted at the project level (EC-195). */}\n <AssetLibrary projectId={ctx.projectId} focusAssetId={ctx.search.asset} />\n </SectionSurface>\n ),\n },\n {\n id: 'extensions',\n label: 'Extensions',\n description: 'Install components, field types, adapters, and templates from the marketplace.',\n icon: Puzzle,\n availabilitySection: 'extensions',\n render: (ctx) =>\n ctx.search.state && ctx.search.state !== 'working' ? (\n <SectionStateView state={ctx.search.state} title=\"Extensions\" />\n ) : (\n <ExtensionsView />\n ),\n },\n {\n id: 'settings',\n label: 'Settings',\n description: 'Configure project metadata and locales.',\n icon: SettingsIcon,\n availabilitySection: 'settings',\n render: (ctx) => (\n <SectionSurface project={ctx.project} section=\"settings\" forcedState={ctx.search.state}>\n <SettingsForm project={ctx.project} />\n </SectionSurface>\n ),\n },\n {\n id: 'builder',\n label: 'Builder',\n description: 'Legacy builder route — redirects to the page document editor.',\n icon: Blocks,\n hidden: true,\n render: (ctx) => (\n // Retired surface (EC-187 slice 6): a page is a document in the `page`\n // collection. `?page=<id>` lands on that document; else the page list.\n <Navigate\n to={STUDIO_PROJECT_ROUTE}\n params={{\n projectId: ctx.projectId,\n _splat: ctx.search.page\n ? studioPath.cmsDocument('page', ctx.search.page)\n : studioPath.cmsCollection('page'),\n }}\n replace\n />\n ),\n },\n]\n\n/**\n * The active project's resolved app-view modules — the default registry composed\n * with `config.appViews` through the EC-236 seam (config is static per studio boot).\n */\nexport function useAppViews(): ResolvedAppViews {\n return useMemo(() => resolveAppViews(DEFAULT_APP_VIEWS, resolveDeploymentAppViews()), [])\n}\n\n// --- Surfaces (moved verbatim from the resolver; CMS gains a focus lens) ---\n\n/**\n * Components section (EC-050): a registry-browser sidebar beside the active\n * screen — the index (no selection) or a component's detail. Forced states\n * (?state=) replace the whole surface.\n */\nfunction ComponentsScreen({\n projectId,\n componentId,\n state,\n}: {\n projectId: string\n componentId: string | null\n state?: SectionState\n}) {\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"Components\" />\n }\n return (\n <div className=\"flex h-full min-h-0\">\n <aside className=\"hidden h-full min-h-0 w-[300px] shrink-0 border-r border-border md:block\">\n <RegistryBrowser projectId={projectId} activeComponentId={componentId ?? undefined} />\n </aside>\n <div className=\"min-h-0 flex-1 overflow-auto\">\n {componentId === null ? (\n <ComponentsIndex projectId={projectId} />\n ) : (\n <ComponentDetail projectId={projectId} componentId={componentId} />\n )}\n </div>\n </div>\n )\n}\n\n/**\n * CMS section: hydrates the persisted (or kit-seeded) collections + documents,\n * then renders the collections sidebar beside the active screen (index, a\n * collection table, or a document editor). Forced states (?state=) replace the\n * whole surface. `focusCollections` (EC-239) narrows the sidebar + index to a\n * subset, so a focused module (\"Taxonomy\") reuses this surface with config.\n */\nfunction CmsScreen({\n project,\n collectionId,\n documentId,\n state,\n field,\n focusCollections,\n}: {\n project: Project\n collectionId: string | null\n documentId: string | null\n state?: SectionState\n field?: string\n focusCollections?: readonly string[]\n}) {\n const projectId = project.id\n if (state && state !== 'working') {\n return <SectionStateView state={state} title=\"CMS\" />\n }\n\n // A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.\n const kit = getInstantiatedKit(projectId)\n\n return (\n // EC-177 gap 1: in Convex mode every CMS mount loads the CURRENT persisted\n // schema + documents (fixtures only when nothing is persisted — first run).\n // Playground renders synchronously with `null` → fixture seeds, as before.\n <CmsSessionGate>\n {(cms) => (\n <CmsProvider\n key={projectId}\n projectId={projectId}\n initialCollections={cms?.collections ?? kit?.collections}\n initialDocuments={cms?.documents ?? kit?.documents}\n initialPublishedKeys={cms?.publishedKeys}\n initialLocaleConfig={\n cms\n ? // Hydrated sessions follow the project's persisted locale settings.\n { default: project.settings.defaultLocale, locales: [...project.settings.locales] }\n : kit?.localeConfig\n }\n >\n <div className=\"flex h-full min-h-0 flex-col\">\n <CmsPersistErrorBanner />\n <div className=\"flex min-h-0 flex-1\">\n <aside className=\"hidden h-full min-h-0 w-[240px] shrink-0 border-r border-border md:block\">\n <CollectionsSidebar\n projectId={projectId}\n activeCollectionId={collectionId ?? undefined}\n focusCollections={focusCollections}\n />\n </aside>\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <CmsContent\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n field={field}\n focusCollections={focusCollections}\n />\n </div>\n </div>\n </div>\n </CmsProvider>\n )}\n </CmsSessionGate>\n )\n}\n\n/** Resolves the active CMS screen inside the provider (needs the live store). */\nfunction CmsContent({\n projectId,\n collectionId,\n documentId,\n field,\n focusCollections,\n}: {\n projectId: string\n collectionId: string | null\n documentId: string | null\n field?: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n\n if (collectionId === null) return <CmsIndex projectId={projectId} focusCollections={focusCollections} />\n\n if (documentId === null) {\n const collection = cms.getCollection(collectionId)\n // EC-274: a singleton is a UI lens — at the collection URL it opens its one\n // document directly *while in its expected shape*, but degrades to the list\n // (with a \"for review\" lane) on drift. SingletonGate decides per the data.\n if (collection && isSingleton(collection)) {\n return <SingletonGate projectId={projectId} collectionId={collectionId} />\n }\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n\n return (\n <DetailEditor\n projectId={projectId}\n collectionId={collectionId}\n documentId={documentId}\n focusPath={field}\n />\n )\n}\n\n/**\n * EC-274 — the singleton lens gate. At a singleton collection's URL, resolve its\n * lone document and open it directly; on drift (> 1 document) fall back to the\n * list with the \"for review\" lane so an editor can resolve the conflict.\n */\nfunction SingletonGate({ projectId, collectionId }: { projectId: string; collectionId: string }) {\n const cms = useCms()\n const collection = cms.getCollection(collectionId)\n const docs = cms.documentsOf(collectionId)\n const resolution = collection\n ? resolveSingletonDocument(collection, docs)\n : ({ kind: 'empty' } as const)\n\n // Materialize the lone document on first visit (an empty draft, persisted like\n // any create). Idempotent in the store; only fires while genuinely empty.\n useEffect(() => {\n if (resolution.kind === 'empty') cms.ensureSingletonDocument(collectionId)\n }, [resolution.kind, cms, collectionId])\n\n if (resolution.kind === 'ready') {\n return (\n <DetailEditor projectId={projectId} collectionId={collectionId} documentId={resolution.doc.id} />\n )\n }\n if (resolution.kind === 'drift') {\n return <CollectionTable projectId={projectId} collectionId={collectionId} />\n }\n return (\n <div className=\"grid h-full place-items-center p-8\">\n <Text size=\"sm\" tone=\"muted\">\n Opening {collection?.form?.label ?? collectionId}…\n </Text>\n </div>\n )\n}\n\n/** Failed/denied persisted CMS mutations are always visible (EC-141). */\nfunction CmsPersistErrorBanner() {\n const cms = useCms()\n if (!cms.persistError) return null\n return (\n <div className=\"shrink-0 border-b border-border p-2\">\n <OperationErrorBanner error={cms.persistError} onDismiss={cms.clearPersistError} />\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
import type { AppViewEntry, CustomizationIssue, Project, SectionAvailability, SectionId } from '@elytracms/core/studio-core';
|
|
4
|
+
import type { StudioSearch } from '@elytracms/studio/router/search';
|
|
5
|
+
/**
|
|
6
|
+
* EC-239 — the app-view module contract, an instance of the EC-236 customization
|
|
7
|
+
* seam.
|
|
8
|
+
*
|
|
9
|
+
* A studio surface (the CMS, Routes, Assets view, or a net-new one) is a
|
|
10
|
+
* self-contained MODULE: id + display metadata + a React `render` + an optional
|
|
11
|
+
* deep-link parser. The configurable sidebar (EC-237) mounts modules; the splat
|
|
12
|
+
* router (`matchStudioRoute`) addresses one by its id as the first path segment
|
|
13
|
+
* and hands its trailing segments to `parsePath`. The studio ships a DEFAULT
|
|
14
|
+
* registry (so the stock studio needs zero config); a consuming repo adds or
|
|
15
|
+
* replaces modules through `config.appViews`, resolved over the defaults by the
|
|
16
|
+
* shared {@link resolveOverrides} core — an unknown/duplicate/contract-violating
|
|
17
|
+
* entry is a visible issue, never a silent drop, and the default block is kept.
|
|
18
|
+
*
|
|
19
|
+
* Reuse-not-copy (the EC-239 headline): a focused view like "Taxonomy" is the CMS
|
|
20
|
+
* module instantiated with config (`cmsAppView({ collections })`), not a fork — so
|
|
21
|
+
* the registry holds module INSTANCES and the same surface code backs many ids.
|
|
22
|
+
*
|
|
23
|
+
* This module is the contract + the pure resolver only; the default registry and
|
|
24
|
+
* the surfaces (which pull the heavy editor providers) live in `default-modules`.
|
|
25
|
+
*/
|
|
26
|
+
/** The deep-link a module parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */
|
|
27
|
+
export type AppViewDeepLink = Record<string, string | null>;
|
|
28
|
+
/** Everything a module's surface needs to render (EC-239). */
|
|
29
|
+
export interface AppViewContext {
|
|
30
|
+
project: Project;
|
|
31
|
+
projectId: string;
|
|
32
|
+
/** The module's parsed deep-link; `{}` for a flat view with no nested resource. */
|
|
33
|
+
deepLink: AppViewDeepLink;
|
|
34
|
+
/** The validated `?…` search (forced state, focused field/asset, …). */
|
|
35
|
+
search: StudioSearch;
|
|
36
|
+
}
|
|
37
|
+
export interface AppViewModule {
|
|
38
|
+
/** Stable id — the first path segment under the project + the registry key. */
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
description: string;
|
|
42
|
+
icon: LucideIcon;
|
|
43
|
+
/** Render the surface; it mounts its own providers/gates (like the old resolver case bodies). */
|
|
44
|
+
render: (ctx: AppViewContext) => ReactNode;
|
|
45
|
+
/**
|
|
46
|
+
* Parse this module's trailing path segments into a deep-link, or `null` for an
|
|
47
|
+
* unroutable path (→ not-found). Omitted → a FLAT module ({@link flatPath}): only
|
|
48
|
+
* the bare path is valid, any trailing segment is not-found.
|
|
49
|
+
*/
|
|
50
|
+
parsePath?: (segments: string[]) => AppViewDeepLink | null;
|
|
51
|
+
/**
|
|
52
|
+
* The availability section this module rides on (EC-221 gating): an
|
|
53
|
+
* `unavailable` section hides the module from the rail. Omitted → always
|
|
54
|
+
* available (a custom module defaults to visible).
|
|
55
|
+
*/
|
|
56
|
+
availabilitySection?: SectionId;
|
|
57
|
+
/** A redirect-only / utility module the stock rail does not list (e.g. `builder`). */
|
|
58
|
+
hidden?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** A flat module: only the bare path resolves; any trailing segment is not-found. */
|
|
61
|
+
export declare function flatPath(segments: string[]): AppViewDeepLink | null;
|
|
62
|
+
/** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */
|
|
63
|
+
export declare function cmsDeepLink(segments: string[]): AppViewDeepLink | null;
|
|
64
|
+
/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */
|
|
65
|
+
export declare function componentsDeepLink(segments: string[]): AppViewDeepLink | null;
|
|
66
|
+
export interface ResolvedAppViews {
|
|
67
|
+
/** Default + custom modules, defaults first in registry order, additions appended. */
|
|
68
|
+
modules: AppViewModule[];
|
|
69
|
+
/** Resolve one id to its module — a custom override beats the default. */
|
|
70
|
+
get(id: string): AppViewModule | undefined;
|
|
71
|
+
/** Resolution issues (unknown target is impossible here — additive; duplicate / invalid). */
|
|
72
|
+
issues: CustomizationIssue[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the config's custom app-view modules over the studio's default registry
|
|
76
|
+
* through the shared customization core (EC-236): an additive registry keyed by
|
|
77
|
+
* module id, rejecting a module with no `render` / no `label` (or a duplicate id)
|
|
78
|
+
* with a visible issue and keeping the default. Pure — the React payloads are
|
|
79
|
+
* opaque here.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveAppViews(defaults: readonly AppViewModule[], entries?: readonly AppViewEntry[]): ResolvedAppViews;
|
|
82
|
+
/**
|
|
83
|
+
* A module's availability for a project (EC-221): the section it rides on, or
|
|
84
|
+
* always `available` when it declares none (a custom module). Drives both the
|
|
85
|
+
* rail (hide `unavailable`) and the bare-project landing target.
|
|
86
|
+
*/
|
|
87
|
+
export declare function moduleAvailability(project: Project, module: AppViewModule): SectionAvailability;
|
|
88
|
+
/**
|
|
89
|
+
* The module a project should open to from its bare URL: the first listed
|
|
90
|
+
* (non-hidden), non-`unavailable` module in registry order. There is always at
|
|
91
|
+
* least one (Settings is always available), so this never returns `null`.
|
|
92
|
+
*/
|
|
93
|
+
export declare function firstAvailableModule(modules: readonly AppViewModule[], project: Project): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { availabilityOf, resolveOverrides } from '@elytracms/core/studio-core';
|
|
2
|
+
// --- Deep-link parsers (shared by the default modules + reused by factories) ---
|
|
3
|
+
/** A flat module: only the bare path resolves; any trailing segment is not-found. */
|
|
4
|
+
export function flatPath(segments) {
|
|
5
|
+
return segments.length === 0 ? {} : null;
|
|
6
|
+
}
|
|
7
|
+
/** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */
|
|
8
|
+
export function cmsDeepLink(segments) {
|
|
9
|
+
if (segments.length === 0)
|
|
10
|
+
return { collectionId: null, documentId: null };
|
|
11
|
+
if (segments.length === 1)
|
|
12
|
+
return { collectionId: segments[0], documentId: null };
|
|
13
|
+
if (segments.length === 2)
|
|
14
|
+
return { collectionId: segments[0], documentId: segments[1] };
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */
|
|
18
|
+
export function componentsDeepLink(segments) {
|
|
19
|
+
if (segments.length === 0)
|
|
20
|
+
return { componentId: null };
|
|
21
|
+
if (segments.length === 1)
|
|
22
|
+
return { componentId: segments[0] };
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the config's custom app-view modules over the studio's default registry
|
|
27
|
+
* through the shared customization core (EC-236): an additive registry keyed by
|
|
28
|
+
* module id, rejecting a module with no `render` / no `label` (or a duplicate id)
|
|
29
|
+
* with a visible issue and keeping the default. Pure — the React payloads are
|
|
30
|
+
* opaque here.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveAppViews(defaults, entries = []) {
|
|
33
|
+
const resolved = resolveOverrides({
|
|
34
|
+
point: 'appViews',
|
|
35
|
+
defaults: defaults.map((module) => ({ id: module.id, value: module })),
|
|
36
|
+
overrides: entries.map((entry) => {
|
|
37
|
+
const module = entry;
|
|
38
|
+
return { id: module.id, value: module };
|
|
39
|
+
}),
|
|
40
|
+
allowNew: true,
|
|
41
|
+
validate: (entry) => {
|
|
42
|
+
const module = entry.value;
|
|
43
|
+
if (typeof module.render !== 'function')
|
|
44
|
+
return 'the module has no render function';
|
|
45
|
+
if (typeof module.label !== 'string' || module.label.trim().length === 0)
|
|
46
|
+
return 'the module has no label';
|
|
47
|
+
return null;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
modules: resolved.entries.map((entry) => entry.value),
|
|
52
|
+
get: (id) => resolved.get(id),
|
|
53
|
+
issues: resolved.issues,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A module's availability for a project (EC-221): the section it rides on, or
|
|
58
|
+
* always `available` when it declares none (a custom module). Drives both the
|
|
59
|
+
* rail (hide `unavailable`) and the bare-project landing target.
|
|
60
|
+
*/
|
|
61
|
+
export function moduleAvailability(project, module) {
|
|
62
|
+
return module.availabilitySection
|
|
63
|
+
? availabilityOf(project.settings.sectionAvailability, module.availabilitySection)
|
|
64
|
+
: 'available';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The module a project should open to from its bare URL: the first listed
|
|
68
|
+
* (non-hidden), non-`unavailable` module in registry order. There is always at
|
|
69
|
+
* least one (Settings is always available), so this never returns `null`.
|
|
70
|
+
*/
|
|
71
|
+
export function firstAvailableModule(modules, project) {
|
|
72
|
+
const found = modules.find((module) => !module.hidden && moduleAvailability(project, module) !== 'unavailable');
|
|
73
|
+
return found?.id ?? 'settings';
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../../../src/components/app-views/module.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { availabilityOf, resolveOverrides } from '@elytracms/core/studio-core'\nimport type {\n AppViewEntry,\n CustomizationIssue,\n Project,\n SectionAvailability,\n SectionId,\n} from '@elytracms/core/studio-core'\nimport type { StudioSearch } from '@elytracms/studio/router/search'\n\n/**\n * EC-239 — the app-view module contract, an instance of the EC-236 customization\n * seam.\n *\n * A studio surface (the CMS, Routes, Assets view, or a net-new one) is a\n * self-contained MODULE: id + display metadata + a React `render` + an optional\n * deep-link parser. The configurable sidebar (EC-237) mounts modules; the splat\n * router (`matchStudioRoute`) addresses one by its id as the first path segment\n * and hands its trailing segments to `parsePath`. The studio ships a DEFAULT\n * registry (so the stock studio needs zero config); a consuming repo adds or\n * replaces modules through `config.appViews`, resolved over the defaults by the\n * shared {@link resolveOverrides} core — an unknown/duplicate/contract-violating\n * entry is a visible issue, never a silent drop, and the default block is kept.\n *\n * Reuse-not-copy (the EC-239 headline): a focused view like \"Taxonomy\" is the CMS\n * module instantiated with config (`cmsAppView({ collections })`), not a fork — so\n * the registry holds module INSTANCES and the same surface code backs many ids.\n *\n * This module is the contract + the pure resolver only; the default registry and\n * the surfaces (which pull the heavy editor providers) live in `default-modules`.\n */\n\n/** The deep-link a module parses out of its trailing path segments (e.g. `{ collectionId, documentId }`). */\nexport type AppViewDeepLink = Record<string, string | null>\n\n/** Everything a module's surface needs to render (EC-239). */\nexport interface AppViewContext {\n project: Project\n projectId: string\n /** The module's parsed deep-link; `{}` for a flat view with no nested resource. */\n deepLink: AppViewDeepLink\n /** The validated `?…` search (forced state, focused field/asset, …). */\n search: StudioSearch\n}\n\nexport interface AppViewModule {\n /** Stable id — the first path segment under the project + the registry key. */\n id: string\n label: string\n description: string\n icon: LucideIcon\n /** Render the surface; it mounts its own providers/gates (like the old resolver case bodies). */\n render: (ctx: AppViewContext) => ReactNode\n /**\n * Parse this module's trailing path segments into a deep-link, or `null` for an\n * unroutable path (→ not-found). Omitted → a FLAT module ({@link flatPath}): only\n * the bare path is valid, any trailing segment is not-found.\n */\n parsePath?: (segments: string[]) => AppViewDeepLink | null\n /**\n * The availability section this module rides on (EC-221 gating): an\n * `unavailable` section hides the module from the rail. Omitted → always\n * available (a custom module defaults to visible).\n */\n availabilitySection?: SectionId\n /** A redirect-only / utility module the stock rail does not list (e.g. `builder`). */\n hidden?: boolean\n}\n\n// --- Deep-link parsers (shared by the default modules + reused by factories) ---\n\n/** A flat module: only the bare path resolves; any trailing segment is not-found. */\nexport function flatPath(segments: string[]): AppViewDeepLink | null {\n return segments.length === 0 ? {} : null\n}\n\n/** CMS arity: `[]` index · `[c]` collection · `[c, d]` document; deeper is not-found. */\nexport function cmsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { collectionId: null, documentId: null }\n if (segments.length === 1) return { collectionId: segments[0]!, documentId: null }\n if (segments.length === 2) return { collectionId: segments[0]!, documentId: segments[1]! }\n return null\n}\n\n/** Components arity: `[]` index · `[id]` detail; deeper is not-found. */\nexport function componentsDeepLink(segments: string[]): AppViewDeepLink | null {\n if (segments.length === 0) return { componentId: null }\n if (segments.length === 1) return { componentId: segments[0]! }\n return null\n}\n\n// --- Resolution (EC-236 seam) ---\n\nexport interface ResolvedAppViews {\n /** Default + custom modules, defaults first in registry order, additions appended. */\n modules: AppViewModule[]\n /** Resolve one id to its module — a custom override beats the default. */\n get(id: string): AppViewModule | undefined\n /** Resolution issues (unknown target is impossible here — additive; duplicate / invalid). */\n issues: CustomizationIssue[]\n}\n\n/**\n * Resolve the config's custom app-view modules over the studio's default registry\n * through the shared customization core (EC-236): an additive registry keyed by\n * module id, rejecting a module with no `render` / no `label` (or a duplicate id)\n * with a visible issue and keeping the default. Pure — the React payloads are\n * opaque here.\n */\nexport function resolveAppViews(\n defaults: readonly AppViewModule[],\n entries: readonly AppViewEntry[] = [],\n): ResolvedAppViews {\n const resolved = resolveOverrides<AppViewModule>({\n point: 'appViews',\n defaults: defaults.map((module) => ({ id: module.id, value: module })),\n overrides: entries.map((entry) => {\n const module = entry as unknown as AppViewModule\n return { id: module.id, value: module }\n }),\n allowNew: true,\n validate: (entry) => {\n const module = entry.value as Partial<AppViewModule>\n if (typeof module.render !== 'function') return 'the module has no render function'\n if (typeof module.label !== 'string' || module.label.trim().length === 0)\n return 'the module has no label'\n return null\n },\n })\n return {\n modules: resolved.entries.map((entry) => entry.value),\n get: (id) => resolved.get(id),\n issues: resolved.issues,\n }\n}\n\n/**\n * A module's availability for a project (EC-221): the section it rides on, or\n * always `available` when it declares none (a custom module). Drives both the\n * rail (hide `unavailable`) and the bare-project landing target.\n */\nexport function moduleAvailability(project: Project, module: AppViewModule): SectionAvailability {\n return module.availabilitySection\n ? availabilityOf(project.settings.sectionAvailability, module.availabilitySection)\n : 'available'\n}\n\n/**\n * The module a project should open to from its bare URL: the first listed\n * (non-hidden), non-`unavailable` module in registry order. There is always at\n * least one (Settings is always available), so this never returns `null`.\n */\nexport function firstAvailableModule(modules: readonly AppViewModule[], project: Project): string {\n const found = modules.find(\n (module) => !module.hidden && moduleAvailability(project, module) !== 'unavailable',\n )\n return found?.id ?? 'settings'\n}\n"]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
import type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core';
|
|
3
|
+
import { type ResolvedAppViews } from './module';
|
|
4
|
+
/**
|
|
5
|
+
* EC-237 — the configurable sidebar, an instance of the EC-236 customization seam.
|
|
6
|
+
*
|
|
7
|
+
* The section rail is a consumer-extendable ARRAY of nav entries — items (each
|
|
8
|
+
* targeting an EC-239 app-view module, optionally deep-linked to a resource),
|
|
9
|
+
* groups of sub-items, and separators. A consuming repo supplies its own array
|
|
10
|
+
* through `config.sidebar`; omitted → the studio derives the default rail from its
|
|
11
|
+
* module registry (every non-hidden module, in order). The supplied array is
|
|
12
|
+
* resolved through the shared {@link resolveOverrides} core: a duplicate id, an
|
|
13
|
+
* item whose target module isn't registered ("unknown view"), or a deep-link that
|
|
14
|
+
* doesn't resolve becomes a visible {@link CustomizationIssue} and the entry is
|
|
15
|
+
* dropped — never a silent failure. Sub-views (a focused "Taxonomy") are EC-239
|
|
16
|
+
* modules; this file decides only how the rail PRESENTS and orders them.
|
|
17
|
+
*/
|
|
18
|
+
/** A nav item targeting an app-view module, optionally deep-linked to a resource. */
|
|
19
|
+
export interface SidebarItemConfig {
|
|
20
|
+
kind: 'item';
|
|
21
|
+
/** Stable id (defaults to `moduleId` via {@link navItem}). */
|
|
22
|
+
id: string;
|
|
23
|
+
/** The app-view module this item mounts — must be in the registry. */
|
|
24
|
+
moduleId: string;
|
|
25
|
+
/** Overrides the module's rail label. */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Overrides the module's rail icon. */
|
|
28
|
+
icon?: LucideIcon;
|
|
29
|
+
/** Trailing path segments targeting a specific resource, e.g. `['category']`. */
|
|
30
|
+
deepLink?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
export interface SidebarGroupConfig {
|
|
33
|
+
kind: 'group';
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
items: SidebarItemConfig[];
|
|
37
|
+
}
|
|
38
|
+
export interface SidebarSeparatorConfig {
|
|
39
|
+
kind: 'separator';
|
|
40
|
+
id?: string;
|
|
41
|
+
}
|
|
42
|
+
export type SidebarNodeConfig = SidebarItemConfig | SidebarGroupConfig | SidebarSeparatorConfig;
|
|
43
|
+
export interface ResolvedSidebarItem {
|
|
44
|
+
kind: 'item';
|
|
45
|
+
id: string;
|
|
46
|
+
moduleId: string;
|
|
47
|
+
label: string;
|
|
48
|
+
icon: LucideIcon;
|
|
49
|
+
/** The `_splat` to navigate to (moduleId + deep-link segments). */
|
|
50
|
+
path: string;
|
|
51
|
+
availability: SectionAvailability;
|
|
52
|
+
}
|
|
53
|
+
export interface ResolvedSidebarGroup {
|
|
54
|
+
kind: 'group';
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
items: ResolvedSidebarItem[];
|
|
58
|
+
}
|
|
59
|
+
export interface ResolvedSidebarSeparator {
|
|
60
|
+
kind: 'separator';
|
|
61
|
+
id: string;
|
|
62
|
+
}
|
|
63
|
+
export type ResolvedSidebarNode = ResolvedSidebarItem | ResolvedSidebarGroup | ResolvedSidebarSeparator;
|
|
64
|
+
export interface ResolvedSidebar {
|
|
65
|
+
nodes: ResolvedSidebarNode[];
|
|
66
|
+
issues: CustomizationIssue[];
|
|
67
|
+
}
|
|
68
|
+
/** A nav item; `id` defaults to `moduleId` when omitted. */
|
|
69
|
+
export declare function navItem(config: Omit<SidebarItemConfig, 'kind' | 'id'> & {
|
|
70
|
+
id?: string;
|
|
71
|
+
}): SidebarItemConfig;
|
|
72
|
+
/** A labelled group of sub-items; `id` defaults to a slug of the label. */
|
|
73
|
+
export declare function navGroup(label: string, items: SidebarItemConfig[], opts?: {
|
|
74
|
+
id?: string;
|
|
75
|
+
}): SidebarGroupConfig;
|
|
76
|
+
/** A divider between rail sections. */
|
|
77
|
+
export declare function navSeparator(id?: string): SidebarSeparatorConfig;
|
|
78
|
+
/** Identity helper that types a consumer's `config.sidebar` array. */
|
|
79
|
+
export declare function defineSidebar(nodes: SidebarNodeConfig[]): SidebarNodeConfig[];
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a consumer sidebar (or the default rail) over the app-view registry
|
|
82
|
+
* through the shared customization core (EC-236). Every node — items, group
|
|
83
|
+
* headers, nested items, separators — is keyed by id and run through
|
|
84
|
+
* {@link resolveOverrides} for dedup + per-node validation, so a duplicate id or
|
|
85
|
+
* an invalid item is a visible issue and is dropped; the surviving nodes are
|
|
86
|
+
* rebuilt into the rail tree (groups keep their surviving sub-items).
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveSidebar(registry: ResolvedAppViews, project: Project, entries?: readonly SidebarNodeConfig[]): ResolvedSidebar;
|
|
89
|
+
/** The active module id parsed from a studio pathname `/<projectId>/<module>[/…]`. */
|
|
90
|
+
export declare function activeModuleFromPath(pathname: string): string | null;
|
|
91
|
+
/**
|
|
92
|
+
* The active project's resolved sidebar — the default rail or `config.sidebar`,
|
|
93
|
+
* resolved over the module registry through the EC-236 seam (static per boot).
|
|
94
|
+
*/
|
|
95
|
+
export declare function useSidebar(project: Project): ResolvedSidebar;
|