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