@elytracms/studio 0.0.99 → 0.0.101
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/convex/analytics-cost.test.ts +1 -1
- package/convex/cms.ts +29 -2
- package/convex/records-column-transform.convex.test.ts +5 -3
- package/convex/records-one-write.convex.test.ts +218 -0
- package/convex/redirect-migration.test.ts +1 -1
- package/convex/redirects.ts +2 -2
- package/convex/shared-capture.convex.test.ts +165 -0
- package/convex/version-kind-stamping.test.ts +118 -0
- package/dist/components/ai/ai-chat-panel-lazy.js +7 -2
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-unfilled.d.ts +14 -0
- package/dist/components/ai/proposal-unfilled.js +33 -0
- package/dist/components/ai/proposal-unfilled.js.map +1 -0
- package/dist/components/ai/translate-document-action.js +8 -6
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +12 -12
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +37 -1
- package/dist/components/builder/builder-store.js +21 -6
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.d.ts +1 -1
- package/dist/components/builder/canvas-chrome-css.js +1 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +1 -1
- package/dist/components/builder/canvas-chrome.js +1 -1
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-rebase.d.ts +4 -4
- package/dist/components/builder/canvas-rebase.js +3 -2
- package/dist/components/builder/canvas-rebase.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +1 -1
- package/dist/components/builder/canvas-render-context.js +1 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +2 -1
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +8 -2
- package/dist/components/builder/reuse-context.js +62 -7
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +106 -70
- package/dist/components/builder/reuse.js +125 -108
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +9 -1
- package/dist/components/cms/cms-store.js +1 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +13 -13
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/prop-fields.js +1 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.js +2 -1
- package/dist/components/cms/rich-text/lazy.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/use-field-focus.js +3 -3
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.js +2 -1
- package/dist/components/cms/version-compare-lazy.js.map +1 -1
- package/dist/components/components/components-index.js +1 -1
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +1 -1
- package/dist/components/deployment-setup-gate.js +1 -1
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/load-failure.d.ts +62 -0
- package/dist/components/load-failure.js +110 -0
- package/dist/components/load-failure.js.map +1 -0
- package/dist/components/notifications/shell-findings.js +3 -3
- package/dist/components/notifications/shell-findings.js.map +1 -1
- package/dist/components/records/record-rail.js +16 -5
- package/dist/components/records/record-rail.js.map +1 -1
- package/dist/customization.d.ts +3 -3
- package/dist/customization.js.map +1 -1
- package/dist/lib/ai/apply-content.js +99 -28
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +73 -7
- package/dist/lib/ai/chat-context.js +133 -14
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +7 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +5 -0
- package/dist/lib/ai/constraints.js +21 -6
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +76 -26
- package/dist/lib/ai/fake-provider.js +210 -66
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +1 -0
- package/dist/lib/ai/index.js +1 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/keyless-assistant.d.ts +37 -0
- package/dist/lib/ai/keyless-assistant.js +193 -0
- package/dist/lib/ai/keyless-assistant.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +25 -2
- package/dist/lib/ai/shape.js +58 -7
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +11 -1
- package/dist/lib/ai/tools.js +51 -6
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.js +6 -5
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +22 -7
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +9 -6
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +11 -3
- package/dist/lib/ai/writer-form.js +105 -18
- package/dist/lib/ai/writer-form.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +1 -1
- package/dist/lib/auth-errors.js +1 -1
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.d.ts +1 -1
- package/dist/lib/i18n/messages.de.js +24 -7
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +19 -3
- package/dist/lib/i18n/messages.en.js +30 -9
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +18 -0
- package/dist/lib/operations/cms-persistence.js +56 -15
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +2 -0
- package/dist/lib/persistence/convex-adapter.js +67 -2
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +3 -3
- package/dist/lib/workspace/index.js +3 -3
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/navigate-once.d.ts +1 -1
- package/dist/router/navigate-once.js.map +1 -1
- package/dist/router/root.d.ts +3 -2
- package/dist/router/root.js +25 -4
- package/dist/router/root.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
/** Is this error a lazily imported module that failed to download? (pure) */
|
|
4
|
+
export declare function isModuleLoadFailure(error: unknown): boolean;
|
|
5
|
+
/** Where the notice renders: the whole page (the root catch-all) or in place of one surface. */
|
|
6
|
+
export type FailurePlacement = 'page' | 'panel';
|
|
7
|
+
export interface FailureNoticeProps {
|
|
8
|
+
error: unknown;
|
|
9
|
+
placement: FailurePlacement;
|
|
10
|
+
/** Playground mode — Reload then states that it empties the playground. */
|
|
11
|
+
playground: boolean;
|
|
12
|
+
onBack: () => void;
|
|
13
|
+
onReload: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The notice itself — presentational, every effect injected, so its two shapes are pinned
|
|
17
|
+
* with `renderToStaticMarkup`. {@link StudioFailure} wires it to the router and the page.
|
|
18
|
+
*/
|
|
19
|
+
export declare function FailureNotice({ error, placement, playground, onBack, onReload }: FailureNoticeProps): import("react").JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* The wired notice. **Back** goes one step back in the router's own history — in-app, no
|
|
22
|
+
* reload — or, when this tab has no earlier studio entry (a deep link), to the studio home;
|
|
23
|
+
* leaving the studio would unload the playground, which is exactly what Back is for avoiding.
|
|
24
|
+
* **Reload** is the full page reload it says it is.
|
|
25
|
+
*/
|
|
26
|
+
export declare function StudioFailure({ error, placement }: {
|
|
27
|
+
error: unknown;
|
|
28
|
+
placement: FailurePlacement;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* The root route's `errorComponent` — the catch-all for any error no surface boundary took.
|
|
32
|
+
* It renders inside the root `shellComponent` (the `<html>` document with the stylesheet),
|
|
33
|
+
* so it is themed; outside the provider stack, so it needs none of it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function StudioRouteError({ error }: {
|
|
36
|
+
error: unknown;
|
|
37
|
+
}): import("react").JSX.Element;
|
|
38
|
+
export interface LazySurfaceBoundaryProps {
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Frame the notice the way the surface frames itself (the AI panel's aside, the translate
|
|
42
|
+
* dialog) so the failure shows exactly where the surface would have. Default: unframed.
|
|
43
|
+
*/
|
|
44
|
+
frame?: (notice: ReactNode) => ReactNode;
|
|
45
|
+
}
|
|
46
|
+
interface LazySurfaceBoundaryState {
|
|
47
|
+
/** `[error]`, so a thrown `undefined` still counts as caught. */
|
|
48
|
+
caught: [unknown] | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The ONE boundary each lazily loaded surface sits in (EC-717): a failed panel shows the
|
|
52
|
+
* notice IN PLACE and the rest of the studio stays usable. Measured before it existed: one
|
|
53
|
+
* failed translate chunk blanked the whole studio. It never resets on its own — the lazy
|
|
54
|
+
* import's rejection is cached, so re-rendering would only throw again; the surface's own
|
|
55
|
+
* unmount (closing the panel or dialog, leaving the document) is the reset.
|
|
56
|
+
*/
|
|
57
|
+
export declare class LazySurfaceBoundary extends Component<LazySurfaceBoundaryProps, LazySurfaceBoundaryState> {
|
|
58
|
+
state: LazySurfaceBoundaryState;
|
|
59
|
+
static getDerivedStateFromError(error: unknown): LazySurfaceBoundaryState;
|
|
60
|
+
render(): ReactNode;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import { useRouter } from '@tanstack/react-router';
|
|
4
|
+
import { CircleAlert, PackageX } from 'lucide-react';
|
|
5
|
+
import { Button, Heading, Row, Stack, Text } from '@elytracms/studio/ui';
|
|
6
|
+
import { cn } from '../lib/utils.js';
|
|
7
|
+
import { useT } from '../lib/i18n/index.js';
|
|
8
|
+
import { isPlaygroundMode } from '../lib/persistence/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* The studio's failure notice (EC-717) — what a render error shows instead of the whole
|
|
11
|
+
* studio disappearing. Two places use it: the root route's `errorComponent` (the catch-all,
|
|
12
|
+
* rendered inside the themed document shell) and {@link LazySurfaceBoundary}, which sits
|
|
13
|
+
* inside each of the five lazily loaded surfaces so a failed panel stays a local notice.
|
|
14
|
+
*
|
|
15
|
+
* **The failure it names first** is a module that did not download. Measured (EC-567 V6): a
|
|
16
|
+
* CLI command beside `pnpm dev` swapped the dev server's dep cache, every chunk URL the page
|
|
17
|
+
* already knew answered 504, and the next lazily imported surface threw "Failed to fetch
|
|
18
|
+
* dynamically imported module". A dependency added while the studio is open does the same.
|
|
19
|
+
*
|
|
20
|
+
* **Why there is no retry button.** A retry cannot re-import a failed module without a
|
|
21
|
+
* reload (measured in Chromium for the operator's decision): the browser never re-fetches a
|
|
22
|
+
* failed `import()` URL, a cache-busted parent still fails on its failed static dependency,
|
|
23
|
+
* and `React.lazy` caches the rejection. So the notice offers what does work:
|
|
24
|
+
* - **Back** — in-app navigation, no reload, so the playground's in-memory content survives
|
|
25
|
+
* (its adapter is module-cached); the failed surface stays unavailable until a reload.
|
|
26
|
+
* - **Reload** — saying what it costs in the playground: everything in this tab.
|
|
27
|
+
* - **Restart the dev server** — when a reload does not help, because the dep cache itself was
|
|
28
|
+
* destroyed and the chunks it named are gone until Vite re-optimizes.
|
|
29
|
+
*
|
|
30
|
+
* Any other error gets a plain message and Reload. Its message is always shown — a notice
|
|
31
|
+
* that hid what broke would be the silent failure the studio never ships.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The browsers' messages for a dynamic import whose module (or one of its static
|
|
35
|
+
* dependencies) failed to download — Chromium, Firefox, Safari. The same three
|
|
36
|
+
* TanStack Router recognises (`isModuleNotFoundError`, used by its lazy route components).
|
|
37
|
+
*/
|
|
38
|
+
const MODULE_LOAD_FAILURE_PREFIXES = [
|
|
39
|
+
'Failed to fetch dynamically imported module',
|
|
40
|
+
'error loading dynamically imported module',
|
|
41
|
+
'Importing a module script failed',
|
|
42
|
+
];
|
|
43
|
+
/** Is this error a lazily imported module that failed to download? (pure) */
|
|
44
|
+
export function isModuleLoadFailure(error) {
|
|
45
|
+
const message = errorMessage(error);
|
|
46
|
+
return message !== undefined && MODULE_LOAD_FAILURE_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
47
|
+
}
|
|
48
|
+
function errorMessage(error) {
|
|
49
|
+
if (error instanceof Error)
|
|
50
|
+
return error.message;
|
|
51
|
+
if (typeof error === 'object' && error !== null && typeof error.message === 'string') {
|
|
52
|
+
return error.message;
|
|
53
|
+
}
|
|
54
|
+
return typeof error === 'string' ? error : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The notice itself — presentational, every effect injected, so its two shapes are pinned
|
|
58
|
+
* with `renderToStaticMarkup`. {@link StudioFailure} wires it to the router and the page.
|
|
59
|
+
*/
|
|
60
|
+
export function FailureNotice({ error, placement, playground, onBack, onReload }) {
|
|
61
|
+
const t = useT();
|
|
62
|
+
const moduleFailure = isModuleLoadFailure(error);
|
|
63
|
+
const Icon = moduleFailure ? PackageX : CircleAlert;
|
|
64
|
+
const detail = errorMessage(error);
|
|
65
|
+
return (_jsx("div", { "data-studio-failure": moduleFailure ? 'module' : 'error', role: "alert", className: cn('grid place-items-center', placement === 'page' ? 'min-h-dvh bg-background p-8' : 'min-h-0 w-full flex-1 p-4'), children: _jsxs(Stack, { gap: "3", className: cn('w-full rounded-xl border border-dashed border-destructive/30 bg-destructive/5', placement === 'page' ? 'max-w-lg px-8 py-8' : 'max-w-md px-5 py-5'), children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsx(Icon, { "aria-hidden": true, className: "size-5 shrink-0 text-destructive" }), _jsx(Heading, { level: placement === 'page' ? 1 : 2, size: "h3", children: t(moduleFailure ? 'shell.failure.module.title' : 'shell.failure.error.title') })] }), _jsx(Text, { size: "sm", tone: "muted", children: t(moduleFailure ? 'shell.failure.module.body' : 'shell.failure.error.body') }), detail ? (_jsx(Text, { as: "div", size: "xs", tone: "muted", mono: true, className: "max-h-24 overflow-auto break-all rounded-md bg-muted/60 px-2 py-1.5", children: detail })) : null, _jsxs(Row, { gap: "2", wrap: true, children: [moduleFailure ? (_jsx(Button, { size: "sm", onClick: onBack, "data-testid": "studio-failure-back", children: t('shell.failure.back') })) : null, _jsx(Button, { size: "sm", variant: moduleFailure ? 'outline' : 'default', onClick: onReload, "data-testid": "studio-failure-reload", children: t('shell.failure.reload') })] }), playground ? (_jsx(Text, { size: "xs", tone: "muted", children: t('shell.failure.reloadCost') })) : null, moduleFailure ? (_jsx(Text, { size: "xs", tone: "muted", children: t('shell.failure.module.restartHint') })) : null] }) }));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The wired notice. **Back** goes one step back in the router's own history — in-app, no
|
|
69
|
+
* reload — or, when this tab has no earlier studio entry (a deep link), to the studio home;
|
|
70
|
+
* leaving the studio would unload the playground, which is exactly what Back is for avoiding.
|
|
71
|
+
* **Reload** is the full page reload it says it is.
|
|
72
|
+
*/
|
|
73
|
+
export function StudioFailure({ error, placement }) {
|
|
74
|
+
const router = useRouter();
|
|
75
|
+
return (_jsx(FailureNotice, { error: error, placement: placement, playground: isPlaygroundMode(), onBack: () => {
|
|
76
|
+
if (router.history.canGoBack())
|
|
77
|
+
router.history.back();
|
|
78
|
+
else
|
|
79
|
+
void router.navigate({ to: '/' });
|
|
80
|
+
}, onReload: () => window.location.reload() }));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The root route's `errorComponent` — the catch-all for any error no surface boundary took.
|
|
84
|
+
* It renders inside the root `shellComponent` (the `<html>` document with the stylesheet),
|
|
85
|
+
* so it is themed; outside the provider stack, so it needs none of it.
|
|
86
|
+
*/
|
|
87
|
+
export function StudioRouteError({ error }) {
|
|
88
|
+
return _jsx(StudioFailure, { error: error, placement: "page" });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The ONE boundary each lazily loaded surface sits in (EC-717): a failed panel shows the
|
|
92
|
+
* notice IN PLACE and the rest of the studio stays usable. Measured before it existed: one
|
|
93
|
+
* failed translate chunk blanked the whole studio. It never resets on its own — the lazy
|
|
94
|
+
* import's rejection is cached, so re-rendering would only throw again; the surface's own
|
|
95
|
+
* unmount (closing the panel or dialog, leaving the document) is the reset.
|
|
96
|
+
*/
|
|
97
|
+
export class LazySurfaceBoundary extends Component {
|
|
98
|
+
state = { caught: null };
|
|
99
|
+
static getDerivedStateFromError(error) {
|
|
100
|
+
return { caught: [error] };
|
|
101
|
+
}
|
|
102
|
+
render() {
|
|
103
|
+
const { caught } = this.state;
|
|
104
|
+
if (!caught)
|
|
105
|
+
return this.props.children;
|
|
106
|
+
const notice = _jsx(StudioFailure, { error: caught[0], placement: "panel" });
|
|
107
|
+
return this.props.frame ? this.props.frame(notice) : notice;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=load-failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-failure.js","sourceRoot":"","sources":["../../src/components/load-failure.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component } from 'react'\nimport type { ReactNode } from 'react'\nimport { useRouter } from '@tanstack/react-router'\nimport { CircleAlert, PackageX } from 'lucide-react'\nimport { Button, Heading, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '../lib/utils'\nimport { useT } from '../lib/i18n'\nimport { isPlaygroundMode } from '../lib/persistence'\n\n/**\n * The studio's failure notice (EC-717) — what a render error shows instead of the whole\n * studio disappearing. Two places use it: the root route's `errorComponent` (the catch-all,\n * rendered inside the themed document shell) and {@link LazySurfaceBoundary}, which sits\n * inside each of the five lazily loaded surfaces so a failed panel stays a local notice.\n *\n * **The failure it names first** is a module that did not download. Measured (EC-567 V6): a\n * CLI command beside `pnpm dev` swapped the dev server's dep cache, every chunk URL the page\n * already knew answered 504, and the next lazily imported surface threw \"Failed to fetch\n * dynamically imported module\". A dependency added while the studio is open does the same.\n *\n * **Why there is no retry button.** A retry cannot re-import a failed module without a\n * reload (measured in Chromium for the operator's decision): the browser never re-fetches a\n * failed `import()` URL, a cache-busted parent still fails on its failed static dependency,\n * and `React.lazy` caches the rejection. So the notice offers what does work:\n * - **Back** — in-app navigation, no reload, so the playground's in-memory content survives\n * (its adapter is module-cached); the failed surface stays unavailable until a reload.\n * - **Reload** — saying what it costs in the playground: everything in this tab.\n * - **Restart the dev server** — when a reload does not help, because the dep cache itself was\n * destroyed and the chunks it named are gone until Vite re-optimizes.\n *\n * Any other error gets a plain message and Reload. Its message is always shown — a notice\n * that hid what broke would be the silent failure the studio never ships.\n */\n\n/**\n * The browsers' messages for a dynamic import whose module (or one of its static\n * dependencies) failed to download — Chromium, Firefox, Safari. The same three\n * TanStack Router recognises (`isModuleNotFoundError`, used by its lazy route components).\n */\nconst MODULE_LOAD_FAILURE_PREFIXES = [\n 'Failed to fetch dynamically imported module',\n 'error loading dynamically imported module',\n 'Importing a module script failed',\n] as const\n\n/** Is this error a lazily imported module that failed to download? (pure) */\nexport function isModuleLoadFailure(error: unknown): boolean {\n const message = errorMessage(error)\n return message !== undefined && MODULE_LOAD_FAILURE_PREFIXES.some((prefix) => message.startsWith(prefix))\n}\n\nfunction errorMessage(error: unknown): string | undefined {\n if (error instanceof Error) return error.message\n if (typeof error === 'object' && error !== null && typeof (error as { message?: unknown }).message === 'string') {\n return (error as { message: string }).message\n }\n return typeof error === 'string' ? error : undefined\n}\n\n/** Where the notice renders: the whole page (the root catch-all) or in place of one surface. */\nexport type FailurePlacement = 'page' | 'panel'\n\nexport interface FailureNoticeProps {\n error: unknown\n placement: FailurePlacement\n /** Playground mode — Reload then states that it empties the playground. */\n playground: boolean\n onBack: () => void\n onReload: () => void\n}\n\n/**\n * The notice itself — presentational, every effect injected, so its two shapes are pinned\n * with `renderToStaticMarkup`. {@link StudioFailure} wires it to the router and the page.\n */\nexport function FailureNotice({ error, placement, playground, onBack, onReload }: FailureNoticeProps) {\n const t = useT()\n const moduleFailure = isModuleLoadFailure(error)\n const Icon = moduleFailure ? PackageX : CircleAlert\n const detail = errorMessage(error)\n return (\n <div\n data-studio-failure={moduleFailure ? 'module' : 'error'}\n role=\"alert\"\n className={cn(\n 'grid place-items-center',\n placement === 'page' ? 'min-h-dvh bg-background p-8' : 'min-h-0 w-full flex-1 p-4',\n )}\n >\n <Stack\n gap=\"3\"\n className={cn(\n 'w-full rounded-xl border border-dashed border-destructive/30 bg-destructive/5',\n placement === 'page' ? 'max-w-lg px-8 py-8' : 'max-w-md px-5 py-5',\n )}\n >\n <Row gap=\"2\" align=\"center\">\n <Icon aria-hidden className=\"size-5 shrink-0 text-destructive\" />\n <Heading level={placement === 'page' ? 1 : 2} size=\"h3\">\n {t(moduleFailure ? 'shell.failure.module.title' : 'shell.failure.error.title')}\n </Heading>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {t(moduleFailure ? 'shell.failure.module.body' : 'shell.failure.error.body')}\n </Text>\n {detail ? (\n <Text as=\"div\" size=\"xs\" tone=\"muted\" mono className=\"max-h-24 overflow-auto break-all rounded-md bg-muted/60 px-2 py-1.5\">\n {detail}\n </Text>\n ) : null}\n <Row gap=\"2\" wrap>\n {moduleFailure ? (\n <Button size=\"sm\" onClick={onBack} data-testid=\"studio-failure-back\">\n {t('shell.failure.back')}\n </Button>\n ) : null}\n <Button size=\"sm\" variant={moduleFailure ? 'outline' : 'default'} onClick={onReload} data-testid=\"studio-failure-reload\">\n {t('shell.failure.reload')}\n </Button>\n </Row>\n {playground ? (\n <Text size=\"xs\" tone=\"muted\">\n {t('shell.failure.reloadCost')}\n </Text>\n ) : null}\n {moduleFailure ? (\n <Text size=\"xs\" tone=\"muted\">\n {t('shell.failure.module.restartHint')}\n </Text>\n ) : null}\n </Stack>\n </div>\n )\n}\n\n/**\n * The wired notice. **Back** goes one step back in the router's own history — in-app, no\n * reload — or, when this tab has no earlier studio entry (a deep link), to the studio home;\n * leaving the studio would unload the playground, which is exactly what Back is for avoiding.\n * **Reload** is the full page reload it says it is.\n */\nexport function StudioFailure({ error, placement }: { error: unknown; placement: FailurePlacement }) {\n const router = useRouter()\n return (\n <FailureNotice\n error={error}\n placement={placement}\n playground={isPlaygroundMode()}\n onBack={() => {\n if (router.history.canGoBack()) router.history.back()\n else void router.navigate({ to: '/' })\n }}\n onReload={() => window.location.reload()}\n />\n )\n}\n\n/**\n * The root route's `errorComponent` — the catch-all for any error no surface boundary took.\n * It renders inside the root `shellComponent` (the `<html>` document with the stylesheet),\n * so it is themed; outside the provider stack, so it needs none of it.\n */\nexport function StudioRouteError({ error }: { error: unknown }) {\n return <StudioFailure error={error} placement=\"page\" />\n}\n\nexport interface LazySurfaceBoundaryProps {\n children: ReactNode\n /**\n * Frame the notice the way the surface frames itself (the AI panel's aside, the translate\n * dialog) so the failure shows exactly where the surface would have. Default: unframed.\n */\n frame?: (notice: ReactNode) => ReactNode\n}\n\ninterface LazySurfaceBoundaryState {\n /** `[error]`, so a thrown `undefined` still counts as caught. */\n caught: [unknown] | null\n}\n\n/**\n * The ONE boundary each lazily loaded surface sits in (EC-717): a failed panel shows the\n * notice IN PLACE and the rest of the studio stays usable. Measured before it existed: one\n * failed translate chunk blanked the whole studio. It never resets on its own — the lazy\n * import's rejection is cached, so re-rendering would only throw again; the surface's own\n * unmount (closing the panel or dialog, leaving the document) is the reset.\n */\nexport class LazySurfaceBoundary extends Component<LazySurfaceBoundaryProps, LazySurfaceBoundaryState> {\n override state: LazySurfaceBoundaryState = { caught: null }\n\n static getDerivedStateFromError(error: unknown): LazySurfaceBoundaryState {\n return { caught: [error] }\n }\n\n override render(): ReactNode {\n const { caught } = this.state\n if (!caught) return this.props.children\n const notice = <StudioFailure error={caught[0]} placement=\"panel\" />\n return this.props.frame ? this.props.frame(notice) : notice\n }\n}\n"]}
|
|
@@ -108,9 +108,9 @@ function useProjectConfigIssues() {
|
|
|
108
108
|
return useMemo(() => projectConfigIssues(resolveDeploymentCollections()), []);
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
* EC-467 — the project's reusable-elements config issues: a declaration that cannot
|
|
112
|
-
* resolve
|
|
113
|
-
* boot (registry + config collections + studio config). The pure core is `resolveReuse`.
|
|
111
|
+
* EC-467 / EC-728 — the project's reusable-elements config issues: a declaration that cannot
|
|
112
|
+
* resolve (an undeclared project has none — the embed is declared, never inferred). Static per
|
|
113
|
+
* studio boot (registry + config collections + studio config). The pure core is `resolveReuse`.
|
|
114
114
|
*/
|
|
115
115
|
function useReuseIssues() {
|
|
116
116
|
return useMemo(() => resolveReuse(getStudioRegistry().registry.list(), resolveDeploymentCollections(), resolveDeploymentReuse())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-findings.js","sourceRoot":"","sources":["../../../src/components/notifications/shell-findings.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useSyncExternalStore } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { redirectRecordKind, roleGrantsCapability } from '@elytracms/core/persistence'\nimport { getStudioRegistry } from '../../lib/registry'\nimport {\n projectConfigIssues,\n resolveDeploymentCollections,\n resolveDeploymentReuse,\n resolveDeploymentRoutes,\n} from '../../lib/workspace'\nimport { routeValidationIssues } from '../../lib/routes/route-issues'\nimport { contentSessionOrphans, serverContentSessionOrphans, subscribeContentSession } from '../../lib/data'\nimport { projectRolePolicyFor } from '../../lib/operations/project-runtime-config'\nimport { customizationItems, orphanedCollectionItems, routeIssueItems } from '../nav-issues'\nimport type { NavIssueItem } from '../nav-issues'\nimport { useContributedNavIssues, useNavIssues } from '../nav-issues-registry'\nimport { resolveReuse } from '../builder/reuse'\nimport { useRecordList } from '../records/record-rail'\nimport { useFieldRenderers } from '../cms/field-renderer'\nimport { useProjectRole } from '../auth-context'\nimport { isNavViewVisible, useStudioNav } from '../app-views/sidebar'\nimport type { AppViewModule } from '../app-views/module'\nimport { useDeliveryHealthFindings } from '../delivery-health-banner'\nimport type { DeliveryHealthFinding } from '../delivery-health-banner'\nimport { canSeeFinding, orderFindings } from './notice-model'\nimport type { EngineFindingSource, Finding, FindingAudience } from './notice-model'\n\n/**\n * EC-690 — every persistent finding of the studio shell, for the ONE issues tray.\n *\n * The ENGINE's own sources are computed here, in render — config (EC-479), customization\n * (EC-236/467/478), the URL map (EC-469/590/628), orphaned collections (EC-606) and\n * instant-publish health (EC-268) — so they are there on the first paint (no effect has to\n * run before the badge can count them) and no contributor can overwrite them: they are not\n * in the registry's namespace at all. Contributions (`useNavIssues`, a view's `useIssues`)\n * come from the registry. Then the viewer's audience filter (owner ruling (b)) and the ONE\n * order ({@link orderFindings}).\n *\n * The findings hooks moved here from `SectionNav`, which renders none of them any more (the\n * rail foot is only its collapse toggle — owner ruling (a)).\n */\nexport function useShellFindings(project: Project): Finding[] {\n const nav = useStudioNav(project)\n const { issues: rendererIssues } = useFieldRenderers()\n const configIssues = useProjectConfigIssues()\n const reuseIssues = useReuseIssues()\n const routeIssues = useRouteIssues(project.id)\n const orphanIssues = useOrphanedCollectionIssues()\n const deliveryIssues = useDeliveryHealthFindings(project.id)\n const contributed = useContributedNavIssues()\n const audience = useFindingAudience(project)\n\n return useMemo(() => {\n const engine: Finding[] = [\n // Config issues lead: \"this project declares no collections\" explains every other\n // emptiness on the screen, so it is the first thing to read.\n ...asFindings('config', customizationItems(configIssues)),\n ...asFindings('customization', customizationItems([...nav.issues, ...rendererIssues, ...reuseIssues])),\n ...asFindings('routes', routeIssues),\n ...asFindings('orphans', orphanIssues),\n ...asFindings('delivery', deliveryIssues),\n ]\n return orderFindings([...engine, ...contributed]).filter((finding) => canSeeFinding(finding, audience))\n }, [configIssues, nav.issues, rendererIssues, reuseIssues, routeIssues, orphanIssues, deliveryIssues, contributed, audience])\n}\n\nfunction asFindings(source: EngineFindingSource, items: readonly (NavIssueItem | DeliveryHealthFinding)[]): Finding[] {\n return items.map((item) => ({ id: `${source}:${item.key}`, source, ...item }))\n}\n\n/**\n * Who is reading (owner ruling (b)): an admin (`project.administer`) sees config and\n * deployment findings; a located finding shows to whoever can open its location. While the\n * role is still loading nobody is an admin and no view is visible, so a gated finding\n * appears once the role is known, never to the wrong reader first.\n */\nexport function useFindingAudience(project: Project): FindingAudience {\n const nav = useStudioNav(project)\n const role = useProjectRole(project.id)\n return useMemo(() => {\n const policy = projectRolePolicyFor(project.id)\n const ctx = { project, role }\n return {\n isAdmin: role !== null && roleGrantsCapability(role, 'project.administer', policy),\n canReadContent: role !== null && roleGrantsCapability(role, 'content.read', policy),\n views: nav.registry.views.map((view) => ({\n id: view.id,\n ...(view.recordKind !== undefined ? { recordKind: view.recordKind } : {}),\n visible: role !== null && isNavViewVisible(view, ctx),\n })),\n }\n }, [nav, role, project])\n}\n\n/**\n * EC-616 → EC-690: runs every view's always-on findings hook (`useIssues`) — whether or not\n * the view is open — and publishes what it returns. One component per view (each hook keeps\n * its own hook order), each its OWN publisher with the source `view:<id>`: before EC-690 every\n * probe published under the one source `view:`, so two such views overwrote each other and\n * one unmount retracted both (critic point 1).\n */\nexport function ViewIssuesProbes({ project }: { project: Project }) {\n const nav = useStudioNav(project)\n const issueViews = useMemo(() => nav.registry.views.filter((view) => view.useIssues), [nav])\n return (\n <>\n {issueViews.map((view) => (\n <ViewIssuesProbe key={view.id} view={view} project={project} />\n ))}\n </>\n )\n}\n\nfunction ViewIssuesProbe({ view, project }: { view: AppViewModule; project: Project }) {\n const issues = view.useIssues!({ project, projectId: project.id })\n useNavIssues(viewIssuesSource(view.id), issues)\n return null\n}\n\n/** The source a view's always-on findings publish under. */\nexport function viewIssuesSource(viewId: string): string {\n return `view:${viewId}`\n}\n\n/**\n * EC-479 — what the project has not declared but the studio cannot work without. Read from\n * the config holder alone (static per studio boot), so it needs no CMS provider and is\n * memoised once. The pure decision core is `projectConfigIssues`.\n */\nfunction useProjectConfigIssues(): CustomizationIssue[] {\n return useMemo(() => projectConfigIssues(resolveDeploymentCollections()), [])\n}\n\n/**\n * EC-467 — the project's reusable-elements config issues: a declaration that cannot\n * resolve, or an ambiguous inference the studio refuses to guess through. Static per studio\n * boot (registry + config collections + studio config). The pure core is `resolveReuse`.\n */\nfunction useReuseIssues(): CustomizationIssue[] {\n return useMemo(\n () =>\n resolveReuse(getStudioRegistry().registry.list(), resolveDeploymentCollections(), resolveDeploymentReuse())\n .issues,\n [],\n )\n}\n\n/**\n * EC-469 — the project's URL-map issues: two routes that match the same URLs, a redirect\n * chain that loops, a duplicate or unreadable redirect. Routes are CODE (config, static per\n * boot); redirects are editorial DATA (the rail, live) — the read shares the rail's own query\n * key, so a finding disappears the moment it is fixed; while that query is loading or failed\n * only the code-side conflicts are reported (the rail surfaces its own load error).\n */\nfunction useRouteIssues(projectId: string): NavIssueItem[] {\n const redirects = useRecordList(redirectRecordKind, projectId).data\n return useMemo(\n () =>\n routeIssueItems(\n routeValidationIssues(\n { routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() },\n redirects ?? [],\n ),\n ),\n [redirects],\n )\n}\n\n/**\n * EC-606 — stored documents whose collection the project does not declare. Read from the\n * content session (subscribable, configured from the same hydration in both modes); the\n * server snapshot is empty — there is no session during SSR. The pure mapping is\n * {@link orphanedCollectionItems}.\n */\nfunction useOrphanedCollectionIssues(): NavIssueItem[] {\n const orphans = useSyncExternalStore(subscribeContentSession, contentSessionOrphans, serverContentSessionOrphans)\n return useMemo(() => orphanedCollectionItems(orphans), [orphans])\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shell-findings.js","sourceRoot":"","sources":["../../../src/components/notifications/shell-findings.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useSyncExternalStore } from 'react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { redirectRecordKind, roleGrantsCapability } from '@elytracms/core/persistence'\nimport { getStudioRegistry } from '../../lib/registry'\nimport {\n projectConfigIssues,\n resolveDeploymentCollections,\n resolveDeploymentReuse,\n resolveDeploymentRoutes,\n} from '../../lib/workspace'\nimport { routeValidationIssues } from '../../lib/routes/route-issues'\nimport { contentSessionOrphans, serverContentSessionOrphans, subscribeContentSession } from '../../lib/data'\nimport { projectRolePolicyFor } from '../../lib/operations/project-runtime-config'\nimport { customizationItems, orphanedCollectionItems, routeIssueItems } from '../nav-issues'\nimport type { NavIssueItem } from '../nav-issues'\nimport { useContributedNavIssues, useNavIssues } from '../nav-issues-registry'\nimport { resolveReuse } from '../builder/reuse'\nimport { useRecordList } from '../records/record-rail'\nimport { useFieldRenderers } from '../cms/field-renderer'\nimport { useProjectRole } from '../auth-context'\nimport { isNavViewVisible, useStudioNav } from '../app-views/sidebar'\nimport type { AppViewModule } from '../app-views/module'\nimport { useDeliveryHealthFindings } from '../delivery-health-banner'\nimport type { DeliveryHealthFinding } from '../delivery-health-banner'\nimport { canSeeFinding, orderFindings } from './notice-model'\nimport type { EngineFindingSource, Finding, FindingAudience } from './notice-model'\n\n/**\n * EC-690 — every persistent finding of the studio shell, for the ONE issues tray.\n *\n * The ENGINE's own sources are computed here, in render — config (EC-479), customization\n * (EC-236/467/478), the URL map (EC-469/590/628), orphaned collections (EC-606) and\n * instant-publish health (EC-268) — so they are there on the first paint (no effect has to\n * run before the badge can count them) and no contributor can overwrite them: they are not\n * in the registry's namespace at all. Contributions (`useNavIssues`, a view's `useIssues`)\n * come from the registry. Then the viewer's audience filter (owner ruling (b)) and the ONE\n * order ({@link orderFindings}).\n *\n * The findings hooks moved here from `SectionNav`, which renders none of them any more (the\n * rail foot is only its collapse toggle — owner ruling (a)).\n */\nexport function useShellFindings(project: Project): Finding[] {\n const nav = useStudioNav(project)\n const { issues: rendererIssues } = useFieldRenderers()\n const configIssues = useProjectConfigIssues()\n const reuseIssues = useReuseIssues()\n const routeIssues = useRouteIssues(project.id)\n const orphanIssues = useOrphanedCollectionIssues()\n const deliveryIssues = useDeliveryHealthFindings(project.id)\n const contributed = useContributedNavIssues()\n const audience = useFindingAudience(project)\n\n return useMemo(() => {\n const engine: Finding[] = [\n // Config issues lead: \"this project declares no collections\" explains every other\n // emptiness on the screen, so it is the first thing to read.\n ...asFindings('config', customizationItems(configIssues)),\n ...asFindings('customization', customizationItems([...nav.issues, ...rendererIssues, ...reuseIssues])),\n ...asFindings('routes', routeIssues),\n ...asFindings('orphans', orphanIssues),\n ...asFindings('delivery', deliveryIssues),\n ]\n return orderFindings([...engine, ...contributed]).filter((finding) => canSeeFinding(finding, audience))\n }, [configIssues, nav.issues, rendererIssues, reuseIssues, routeIssues, orphanIssues, deliveryIssues, contributed, audience])\n}\n\nfunction asFindings(source: EngineFindingSource, items: readonly (NavIssueItem | DeliveryHealthFinding)[]): Finding[] {\n return items.map((item) => ({ id: `${source}:${item.key}`, source, ...item }))\n}\n\n/**\n * Who is reading (owner ruling (b)): an admin (`project.administer`) sees config and\n * deployment findings; a located finding shows to whoever can open its location. While the\n * role is still loading nobody is an admin and no view is visible, so a gated finding\n * appears once the role is known, never to the wrong reader first.\n */\nexport function useFindingAudience(project: Project): FindingAudience {\n const nav = useStudioNav(project)\n const role = useProjectRole(project.id)\n return useMemo(() => {\n const policy = projectRolePolicyFor(project.id)\n const ctx = { project, role }\n return {\n isAdmin: role !== null && roleGrantsCapability(role, 'project.administer', policy),\n canReadContent: role !== null && roleGrantsCapability(role, 'content.read', policy),\n views: nav.registry.views.map((view) => ({\n id: view.id,\n ...(view.recordKind !== undefined ? { recordKind: view.recordKind } : {}),\n visible: role !== null && isNavViewVisible(view, ctx),\n })),\n }\n }, [nav, role, project])\n}\n\n/**\n * EC-616 → EC-690: runs every view's always-on findings hook (`useIssues`) — whether or not\n * the view is open — and publishes what it returns. One component per view (each hook keeps\n * its own hook order), each its OWN publisher with the source `view:<id>`: before EC-690 every\n * probe published under the one source `view:`, so two such views overwrote each other and\n * one unmount retracted both (critic point 1).\n */\nexport function ViewIssuesProbes({ project }: { project: Project }) {\n const nav = useStudioNav(project)\n const issueViews = useMemo(() => nav.registry.views.filter((view) => view.useIssues), [nav])\n return (\n <>\n {issueViews.map((view) => (\n <ViewIssuesProbe key={view.id} view={view} project={project} />\n ))}\n </>\n )\n}\n\nfunction ViewIssuesProbe({ view, project }: { view: AppViewModule; project: Project }) {\n const issues = view.useIssues!({ project, projectId: project.id })\n useNavIssues(viewIssuesSource(view.id), issues)\n return null\n}\n\n/** The source a view's always-on findings publish under. */\nexport function viewIssuesSource(viewId: string): string {\n return `view:${viewId}`\n}\n\n/**\n * EC-479 — what the project has not declared but the studio cannot work without. Read from\n * the config holder alone (static per studio boot), so it needs no CMS provider and is\n * memoised once. The pure decision core is `projectConfigIssues`.\n */\nfunction useProjectConfigIssues(): CustomizationIssue[] {\n return useMemo(() => projectConfigIssues(resolveDeploymentCollections()), [])\n}\n\n/**\n * EC-467 / EC-728 — the project's reusable-elements config issues: a declaration that cannot\n * resolve (an undeclared project has none — the embed is declared, never inferred). Static per\n * studio boot (registry + config collections + studio config). The pure core is `resolveReuse`.\n */\nfunction useReuseIssues(): CustomizationIssue[] {\n return useMemo(\n () =>\n resolveReuse(getStudioRegistry().registry.list(), resolveDeploymentCollections(), resolveDeploymentReuse())\n .issues,\n [],\n )\n}\n\n/**\n * EC-469 — the project's URL-map issues: two routes that match the same URLs, a redirect\n * chain that loops, a duplicate or unreadable redirect. Routes are CODE (config, static per\n * boot); redirects are editorial DATA (the rail, live) — the read shares the rail's own query\n * key, so a finding disappears the moment it is fixed; while that query is loading or failed\n * only the code-side conflicts are reported (the rail surfaces its own load error).\n */\nfunction useRouteIssues(projectId: string): NavIssueItem[] {\n const redirects = useRecordList(redirectRecordKind, projectId).data\n return useMemo(\n () =>\n routeIssueItems(\n routeValidationIssues(\n { routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() },\n redirects ?? [],\n ),\n ),\n [redirects],\n )\n}\n\n/**\n * EC-606 — stored documents whose collection the project does not declare. Read from the\n * content session (subscribable, configured from the same hydration in both modes); the\n * server snapshot is empty — there is no session during SSR. The pure mapping is\n * {@link orphanedCollectionItems}.\n */\nfunction useOrphanedCollectionIssues(): NavIssueItem[] {\n const orphans = useSyncExternalStore(subscribeContentSession, contentSessionOrphans, serverContentSessionOrphans)\n return useMemo(() => orphanedCollectionItems(orphans), [orphans])\n}\n"]}
|
|
@@ -185,10 +185,18 @@ function sortForRail(kind, records) {
|
|
|
185
185
|
* Keyed by record kind; a kind with no set rule has no set findings. Redirects: core's one
|
|
186
186
|
* normalised redirect set and router (`routeValidationIssues` — duplicates after
|
|
187
187
|
* trailing-slash normalisation, loops, unreadable rows), over the project's routes.
|
|
188
|
+
*
|
|
189
|
+
* EC-703 (Codex round 6 F34): a `Map`, so a lookup finds only a rule declared here. As a plain
|
|
190
|
+
* object, a kind named `constructor` (`toString`, `valueOf`, …) found the inherited function and
|
|
191
|
+
* called it as a validator, and the rail threw (measured). `defineRecordKind` refuses those names
|
|
192
|
+
* since EC-697; the registry is own-safe regardless, for a kind object built below it.
|
|
188
193
|
*/
|
|
189
|
-
const RECORD_SET_RULES =
|
|
190
|
-
[
|
|
191
|
-
|
|
194
|
+
const RECORD_SET_RULES = new Map([
|
|
195
|
+
[
|
|
196
|
+
redirectRecordKind.kind,
|
|
197
|
+
(records) => routeValidationIssues({ routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() }, records),
|
|
198
|
+
],
|
|
199
|
+
]);
|
|
192
200
|
/** The set rule's issues located AT one record (`[<domain>, <id>]`), keyed by record id. */
|
|
193
201
|
function issuesById(issues) {
|
|
194
202
|
const byId = new Map();
|
|
@@ -208,7 +216,7 @@ function issuesById(issues) {
|
|
|
208
216
|
* report that, field by field). Pure; exported for the tests.
|
|
209
217
|
*/
|
|
210
218
|
export function recordSetIssues(kind, records, doc) {
|
|
211
|
-
const rule = RECORD_SET_RULES
|
|
219
|
+
const rule = RECORD_SET_RULES.get(kind.kind);
|
|
212
220
|
if (!rule)
|
|
213
221
|
return [];
|
|
214
222
|
const columns = recordColumnNames(kind);
|
|
@@ -256,7 +264,10 @@ export function useRecordCollectionViewSource(kind, projectId, schema, paths, ca
|
|
|
256
264
|
// the stored set's verdict is computed once per list; a draft that changed a column is
|
|
257
265
|
// re-judged against its siblings.
|
|
258
266
|
const records = store.records;
|
|
259
|
-
const storedSetIssues = useMemo(() =>
|
|
267
|
+
const storedSetIssues = useMemo(() => {
|
|
268
|
+
const rule = RECORD_SET_RULES.get(kind.kind);
|
|
269
|
+
return rule ? issuesById(rule(records)) : undefined;
|
|
270
|
+
}, [kind, records]);
|
|
260
271
|
const validate = useCallback((doc) => {
|
|
261
272
|
const own = validateDocument(registry, doc, { localeConfig: RAIL_LOCALE_CONFIG });
|
|
262
273
|
if (!storedSetIssues)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-rail.js","sourceRoot":"","sources":["../../../src/components/records/record-rail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { createCollectionRegistry, parseLocaleConfig, validateDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CmsValidationIssue, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { redirectRecordKind } from '@elytracms/core/persistence'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf, RedirectEntry } from '@elytracms/core/persistence'\nimport { routeValidationIssues } from '../../lib/routes/route-issues'\nimport { resolveDeploymentCollections, resolveDeploymentRoutes } from '../../lib/workspace'\nimport { createUuidIdFactory } from '@elytracms/core/determinism'\nimport { getStudioOperations } from '../../lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '../../lib/query'\nimport { createRequiredContext } from '../../lib/required-context'\nimport { localizedLabel, translate } from '../../lib/i18n'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '../../lib/collection-view/collection-view-source'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { RailStoreStatus } from '../rail-store-status'\nimport { CollectionViewSurface } from '../collection-view/collection-view-surface'\nimport { lensDocumentToRecordWrite, recordColumnNames, recordToLensDocument } from './record-lens'\n\n/**\n * EC-616 — the generic RECORD RAIL: a record kind (`defineRecordKind`) on the portable\n * list/edit surfaces (AD-19), read and written through the operations chokepoint\n * (`operations.records(kind)`) and the one query cache (AD-17). A rail is `nav.rail(kind)`;\n * the engine's redirects rail is exactly this over `redirectRecordKind`.\n *\n * - **Store** ({@link RecordRailProvider}): the kind's records through ONE query key per\n * kind and project ({@link useRecordList} — the issues tray and the rail share it); the\n * \"New\" flow keeps an unsaved record as a LOCAL draft until the first explicit save, so\n * an abandoned \"New\" never writes a stub and never shows in the list.\n * - **Source** ({@link useRecordCollectionViewSource}): the lens projections\n * (`record-lens.ts`), the lens's own validation, the rail's capabilities.\n * - **Surface** ({@link RecordRailModule}): the list, or one record's detail.\n */\n\n/** A record rail is not localized — one implicit locale keeps the lens honest. */\nconst RAIL_LOCALE: Locale = 'en'\nconst RAIL_LOCALE_CONFIG: LocaleConfig = parseLocaleConfig({ default: RAIL_LOCALE, locales: [RAIL_LOCALE] })\n\n/** The default capability profile of a record rail: create, edit, remove, bulk delete — content, not administration. */\nexport const RECORD_RAIL_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true,\n duplicate: false,\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** The one query key of a kind's records in a project. */\nexport function recordListKey(kind: AnyRecordKind, projectId: string): readonly [string, string, string] {\n return ['records', kind.kind, projectId]\n}\n\n/**\n * A kind's records in a project, read through the shared operations cache — the ONE query\n * a rail and any reader outside it (the issues tray's URL-map validation reads redirects)\n * share. One key, one cache entry: a save's `cache.patch` updates both at once.\n */\nexport function useRecordList<K extends AnyRecordKind>(kind: K, projectId: string) {\n return useOperationQuery<RecordEntryOf<K>[]>({\n queryKey: recordListKey(kind, projectId),\n loader: async (operations) => {\n const result = await operations.records(kind).list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n },\n })\n}\n\nexport interface RecordRailStore<K extends AnyRecordKind = AnyRecordKind> {\n kind: K\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted records, in the kind's order. Unsaved drafts are excluded. */\n records: readonly RecordEntryOf<K>[]\n /** One record: a local unsaved draft if present, else a persisted record. */\n get(id: string): RecordEntryOf<K> | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(input: RecordWriteOf<K>): Promise<{ ok: true; record: RecordEntryOf<K> } | { ok: false; message: string }>\n /** Delete a persisted record, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst { Context: RecordRailContext, useRequired: useRecordRailContext } =\n createRequiredContext<RecordRailStore>('RecordRail', { article: 'a' })\n\n/** The record rail store of the nearest {@link RecordRailProvider}. */\nexport function useRecordRail<K extends AnyRecordKind>(): RecordRailStore<K> {\n return useRecordRailContext() as unknown as RecordRailStore<K>\n}\n\nlet mintDraftUuid: (() => string) | undefined\n\nexport function RecordRailProvider<K extends AnyRecordKind>({\n kind,\n projectId,\n children,\n}: {\n kind: K\n projectId: string\n children: ReactNode\n}) {\n const query = useRecordList(kind, projectId)\n const cache = useOperationCache()\n const persisted = query.data ?? EMPTY\n const { status, errorMessage } = listStoreState(query)\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RecordEntryOf<K>>>({})\n const draftsRef = useRef(drafts)\n draftsRef.current = drafts\n\n const refetch = query.refetch\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback(\n (id: string) =>\n drafts[id] ?? (persisted as RecordEntryOf<K>[]).find((record) => (record as { id: string }).id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback(() => {\n // The draft id becomes the record's real id on its first save: the production id\n // factory (EC-578 — never a clock or a per-session counter, which collide).\n mintDraftUuid ??= createUuidIdFactory()\n const id = `${kind.idPrefix}_${mintDraftUuid()}`\n const now = new Date().toISOString()\n const draft = { ...recordDraftColumns(kind), id, projectId, createdAt: now, updatedAt: now } as unknown as RecordEntryOf<K>\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [kind, projectId])\n\n const save = useCallback<RecordRailStore<K>['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).save({ projectId, ...input } as never)\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value as RecordEntryOf<K>\n const savedId = (saved as { id: string }).id\n // The server returns the kind's order on the next read; place the save now and\n // let the patched list keep its order key (re-read on the next fetch).\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) => {\n const rest = (prev ?? []).filter((record) => (record as { id: string }).id !== savedId)\n return sortForRail(kind, [...rest, saved])\n })\n setDrafts((prev) => {\n if (!(savedId in prev)) return prev\n const next = { ...prev }\n delete next[savedId]\n return next\n })\n return { ok: true, record: saved }\n },\n [kind, projectId, cache],\n )\n\n const remove = useCallback<RecordRailStore<K>['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (draftsRef.current[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) =>\n (prev ?? []).filter((record) => record.id !== id),\n )\n return { ok: true }\n },\n [kind, projectId, cache],\n )\n\n const store = useMemo<RecordRailStore<K>>(\n () => ({\n kind,\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n records: persisted as RecordEntryOf<K>[],\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [kind, projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RecordRailContext.Provider value={store as unknown as RecordRailStore}>{children}</RecordRailContext.Provider>\n}\n\nconst EMPTY: readonly never[] = []\n\n/**\n * The columns a blank \"New\" draft shows: each column's CREATE default (a new redirect\n * reads enabled + permanent — the reason you create one is for it to work), and `''` for\n * a required text column. The save then sends what the editor saw; the adapter still\n * applies the defaults itself for a write that leaves them out (EC-577).\n */\nexport function recordDraftColumns(kind: AnyRecordKind): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n for (const [name, field] of Object.entries(kind.fields)) {\n const parsed = (field as { safeParse(value: unknown): { success: boolean; data?: unknown } }).safeParse(undefined)\n if (parsed.success && parsed.data !== undefined) columns[name] = parsed.data\n else if ((field as { _zod: { def: { type: string } } })._zod.def.type === 'string') columns[name] = ''\n }\n return columns\n}\n\n/** The kind's own list order for a locally patched list (`orderBy`, else creation time). */\nfunction sortForRail<R>(kind: AnyRecordKind, records: R[]): R[] {\n const column = kind.orderBy ?? 'createdAt'\n return [...records].sort((a, b) => {\n const left = String((a as Record<string, unknown>)[column] ?? '')\n const right = String((b as Record<string, unknown>)[column] ?? '')\n return left < right ? -1 : left > right ? 1 : 0\n })\n}\n\n/**\n * EC-690 (critic point 5) — the findings a record has only as a member of its kind's SET,\n * judged by the ONE producer the studio's issues tray reads, so the record's own Issues pane\n * and the tray can never disagree. Before this, the losing row of a duplicate redirect pair\n * said \"No issues\" while the rail foot named it (measured live, wave-0100): the rail\n * validated one record without its siblings.\n *\n * Keyed by record kind; a kind with no set rule has no set findings. Redirects: core's one\n * normalised redirect set and router (`routeValidationIssues` — duplicates after\n * trailing-slash normalisation, loops, unreadable rows), over the project's routes.\n */\nconst RECORD_SET_RULES: Readonly<Record<string, (records: readonly Record<string, unknown>[]) => readonly CmsValidationIssue[]>> = {\n [redirectRecordKind.kind]: (records) =>\n routeValidationIssues(\n { routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() },\n records as unknown as readonly RedirectEntry[],\n ),\n}\n\n/** The set rule's issues located AT one record (`[<domain>, <id>]`), keyed by record id. */\nfunction issuesById(issues: readonly CmsValidationIssue[]): Map<string, CmsValidationIssue[]> {\n const byId = new Map<string, CmsValidationIssue[]>()\n for (const issue of issues) {\n const id = issue.path[1]\n if (typeof id !== 'string') continue\n byId.set(id, [...(byId.get(id) ?? []), issue])\n }\n return byId\n}\n\n/**\n * The set findings of `doc` as the editor sees it NOW: the stored set with this record's\n * columns replaced by the draft's, so fixing a duplicate `from` clears the finding as you\n * type. An unsaved \"New\" draft joins the set as an extra row; it is only ever told about a\n * DUPLICATE (a half-typed draft is not an \"unreadable stored redirect\" — its required fields\n * report that, field by field). Pure; exported for the tests.\n */\nexport function recordSetIssues(\n kind: AnyRecordKind,\n records: readonly Record<string, unknown>[],\n doc: CmsDocument,\n): CmsValidationIssue[] {\n const rule = RECORD_SET_RULES[kind.kind]\n if (!rule) return []\n const columns = recordColumnNames(kind)\n const index = records.findIndex((record) => record.id === doc.id)\n const draftColumns = Object.fromEntries(Object.entries(doc.values ?? {}).filter(([name]) => columns.has(name)))\n const own = { ...(index === -1 ? {} : records[index]), ...draftColumns, id: doc.id }\n const set = index === -1 ? [...records, own] : records.map((record, i) => (i === index ? own : record))\n const located = issuesById(rule(set)).get(doc.id) ?? []\n return index === -1 ? located.filter((issue) => issue.meta?.duplicateOf !== undefined) : located\n}\n\n/** True when the draft's columns are the stored record's — the memoised stored verdict applies. */\nfunction draftMatchesStored(kind: AnyRecordKind, stored: Record<string, unknown>, doc: CmsDocument): boolean {\n const values = doc.values ?? {}\n for (const column of recordColumnNames(kind)) {\n if (!Object.is(values[column], stored[column])) return false\n }\n return true\n}\n\n/** How a rail addresses its list and one record's detail (the view's own route segment). */\nexport interface RecordRailPaths {\n listPath: string\n detailPath(id: string): string\n}\n\n/**\n * The {@link CollectionViewSource} of a record rail (must be mounted under its\n * {@link RecordRailProvider}). `schema` is the rail's lens — the kind's default lens or a\n * mount-customized one (EC-311 overrides by name; additive fields persist in `meta`).\n */\nexport function useRecordCollectionViewSource<K extends AnyRecordKind>(\n kind: K,\n projectId: string,\n schema: CollectionDef,\n paths: RecordRailPaths,\n capabilities: CollectionViewCapabilities = RECORD_RAIL_CAPABILITIES,\n): CollectionViewSource {\n const store = useRecordRail<K>()\n // Run the (possibly customized) lens's validation — required/pattern and custom\n // `validate` fns on an overridden field must actually fire in the editor (EC-311).\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n const singular = schema.form?.labelSingular\n const titleField = typeof schema.titleField === 'string' ? schema.titleField : undefined\n\n const documents = useCallback(\n () => store.records.map((record) => recordToLensDocument(kind, record)),\n [kind, store.records],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? recordToLensDocument(kind, record) : undefined\n },\n [kind, store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.get(ref.id) as Record<string, unknown> | undefined\n const title = titleField ? record?.[titleField] : undefined\n if (typeof title === 'string' && title.length > 0) return title\n return translate('records.untitled', { label: localizedLabel(singular) ?? kind.kind })\n },\n [store, titleField, singular, kind],\n )\n // EC-690: the record's own fields AND its set-level findings (a duplicate redirect, a loop) —\n // the stored set's verdict is computed once per list; a draft that changed a column is\n // re-judged against its siblings.\n const records = store.records as unknown as readonly Record<string, unknown>[]\n const storedSetIssues = useMemo(\n () => (RECORD_SET_RULES[kind.kind] ? issuesById(RECORD_SET_RULES[kind.kind]!(records)) : undefined),\n [kind, records],\n )\n const validate = useCallback(\n (doc: CmsDocument) => {\n const own = validateDocument(registry, doc, { localeConfig: RAIL_LOCALE_CONFIG })\n if (!storedSetIssues) return own\n const stored = records.find((record) => record.id === doc.id)\n const set =\n stored && draftMatchesStored(kind, stored, doc)\n ? (storedSetIssues.get(doc.id) ?? [])\n : recordSetIssues(kind, records, doc)\n return set.length > 0 ? [...own, ...set] : own\n },\n [registry, storedSetIssues, records, kind],\n )\n const create = useCallback((): DocumentRef => ({ collection: kind.kind, id: store.createDraft() }), [kind, store])\n // EC-616 (multi-session): an edit of a STORED record writes only what the editor changed\n // against the record they opened (`base`, from the edit shell) — the EC-577 rule keeps every\n // other column, so another writer's change made meanwhile survives this save. A record the\n // editor is creating (an unsaved draft) is written whole.\n const save = useCallback(\n (doc: CmsDocument, base?: CmsDocument) => {\n const stored = store.records.some((record) => (record as { id: string }).id === doc.id)\n return store.save(lensDocumentToRecordWrite(kind, doc, stored ? base : undefined))\n },\n [kind, store],\n )\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig: RAIL_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n detailPath: paths.detailPath,\n listPath: paths.listPath,\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n\nexport interface RecordRailModuleProps<K extends AnyRecordKind> {\n kind: K\n projectId: string\n /** `null` → the list; an id → that record's detail. */\n recordId: string | null\n schema: CollectionDef\n paths: RecordRailPaths\n capabilities?: CollectionViewCapabilities\n /** The rail's label, for its loading / failure states. */\n label: string\n /** Optional copy for the loading / failure states (default: the generic catalog keys). */\n messages?: { loading?: () => string; loadFailed?: () => string }\n}\n\n/** The surface of a record rail: the portable list, or one record's detail. */\nexport function RecordRailModule<K extends AnyRecordKind>(props: RecordRailModuleProps<K>) {\n return (\n <RecordRailProvider kind={props.kind} projectId={props.projectId}>\n <RecordRailSurface {...props} />\n </RecordRailProvider>\n )\n}\n\nfunction RecordRailSurface<K extends AnyRecordKind>({\n kind,\n projectId,\n recordId,\n schema,\n paths,\n capabilities,\n label,\n messages,\n}: RecordRailModuleProps<K>) {\n const store = useRecordRail<K>()\n const source = useRecordCollectionViewSource(kind, projectId, schema, paths, capabilities)\n\n if (store.status !== 'ready') {\n return (\n <RailStoreStatus\n store={store}\n errorLabel={messages?.loadFailed?.() ?? translate('records.loadFailed', { label })}\n loadingLabel={messages?.loading?.() ?? translate('records.loading', { label })}\n errorTestId={`${kind.kind}-rail-error`}\n />\n )\n }\n // One record's detail opens in the portable edit shell (AD-19); the list otherwise.\n return <CollectionViewSurface source={source} projectId={projectId} documentId={recordId} />\n}\n"]}
|
|
1
|
+
{"version":3,"file":"record-rail.js","sourceRoot":"","sources":["../../../src/components/records/record-rail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { createCollectionRegistry, parseLocaleConfig, validateDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CmsValidationIssue, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { redirectRecordKind } from '@elytracms/core/persistence'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf, RedirectEntry } from '@elytracms/core/persistence'\nimport { routeValidationIssues } from '../../lib/routes/route-issues'\nimport { resolveDeploymentCollections, resolveDeploymentRoutes } from '../../lib/workspace'\nimport { createUuidIdFactory } from '@elytracms/core/determinism'\nimport { getStudioOperations } from '../../lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '../../lib/query'\nimport { createRequiredContext } from '../../lib/required-context'\nimport { localizedLabel, translate } from '../../lib/i18n'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '../../lib/collection-view/collection-view-source'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\nimport { RailStoreStatus } from '../rail-store-status'\nimport { CollectionViewSurface } from '../collection-view/collection-view-surface'\nimport { lensDocumentToRecordWrite, recordColumnNames, recordToLensDocument } from './record-lens'\n\n/**\n * EC-616 — the generic RECORD RAIL: a record kind (`defineRecordKind`) on the portable\n * list/edit surfaces (AD-19), read and written through the operations chokepoint\n * (`operations.records(kind)`) and the one query cache (AD-17). A rail is `nav.rail(kind)`;\n * the engine's redirects rail is exactly this over `redirectRecordKind`.\n *\n * - **Store** ({@link RecordRailProvider}): the kind's records through ONE query key per\n * kind and project ({@link useRecordList} — the issues tray and the rail share it); the\n * \"New\" flow keeps an unsaved record as a LOCAL draft until the first explicit save, so\n * an abandoned \"New\" never writes a stub and never shows in the list.\n * - **Source** ({@link useRecordCollectionViewSource}): the lens projections\n * (`record-lens.ts`), the lens's own validation, the rail's capabilities.\n * - **Surface** ({@link RecordRailModule}): the list, or one record's detail.\n */\n\n/** A record rail is not localized — one implicit locale keeps the lens honest. */\nconst RAIL_LOCALE: Locale = 'en'\nconst RAIL_LOCALE_CONFIG: LocaleConfig = parseLocaleConfig({ default: RAIL_LOCALE, locales: [RAIL_LOCALE] })\n\n/** The default capability profile of a record rail: create, edit, remove, bulk delete — content, not administration. */\nexport const RECORD_RAIL_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true,\n duplicate: false,\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** The one query key of a kind's records in a project. */\nexport function recordListKey(kind: AnyRecordKind, projectId: string): readonly [string, string, string] {\n return ['records', kind.kind, projectId]\n}\n\n/**\n * A kind's records in a project, read through the shared operations cache — the ONE query\n * a rail and any reader outside it (the issues tray's URL-map validation reads redirects)\n * share. One key, one cache entry: a save's `cache.patch` updates both at once.\n */\nexport function useRecordList<K extends AnyRecordKind>(kind: K, projectId: string) {\n return useOperationQuery<RecordEntryOf<K>[]>({\n queryKey: recordListKey(kind, projectId),\n loader: async (operations) => {\n const result = await operations.records(kind).list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n },\n })\n}\n\nexport interface RecordRailStore<K extends AnyRecordKind = AnyRecordKind> {\n kind: K\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted records, in the kind's order. Unsaved drafts are excluded. */\n records: readonly RecordEntryOf<K>[]\n /** One record: a local unsaved draft if present, else a persisted record. */\n get(id: string): RecordEntryOf<K> | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(input: RecordWriteOf<K>): Promise<{ ok: true; record: RecordEntryOf<K> } | { ok: false; message: string }>\n /** Delete a persisted record, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst { Context: RecordRailContext, useRequired: useRecordRailContext } =\n createRequiredContext<RecordRailStore>('RecordRail', { article: 'a' })\n\n/** The record rail store of the nearest {@link RecordRailProvider}. */\nexport function useRecordRail<K extends AnyRecordKind>(): RecordRailStore<K> {\n return useRecordRailContext() as unknown as RecordRailStore<K>\n}\n\nlet mintDraftUuid: (() => string) | undefined\n\nexport function RecordRailProvider<K extends AnyRecordKind>({\n kind,\n projectId,\n children,\n}: {\n kind: K\n projectId: string\n children: ReactNode\n}) {\n const query = useRecordList(kind, projectId)\n const cache = useOperationCache()\n const persisted = query.data ?? EMPTY\n const { status, errorMessage } = listStoreState(query)\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RecordEntryOf<K>>>({})\n const draftsRef = useRef(drafts)\n draftsRef.current = drafts\n\n const refetch = query.refetch\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback(\n (id: string) =>\n drafts[id] ?? (persisted as RecordEntryOf<K>[]).find((record) => (record as { id: string }).id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback(() => {\n // The draft id becomes the record's real id on its first save: the production id\n // factory (EC-578 — never a clock or a per-session counter, which collide).\n mintDraftUuid ??= createUuidIdFactory()\n const id = `${kind.idPrefix}_${mintDraftUuid()}`\n const now = new Date().toISOString()\n const draft = { ...recordDraftColumns(kind), id, projectId, createdAt: now, updatedAt: now } as unknown as RecordEntryOf<K>\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [kind, projectId])\n\n const save = useCallback<RecordRailStore<K>['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).save({ projectId, ...input } as never)\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value as RecordEntryOf<K>\n const savedId = (saved as { id: string }).id\n // The server returns the kind's order on the next read; place the save now and\n // let the patched list keep its order key (re-read on the next fetch).\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) => {\n const rest = (prev ?? []).filter((record) => (record as { id: string }).id !== savedId)\n return sortForRail(kind, [...rest, saved])\n })\n setDrafts((prev) => {\n if (!(savedId in prev)) return prev\n const next = { ...prev }\n delete next[savedId]\n return next\n })\n return { ok: true, record: saved }\n },\n [kind, projectId, cache],\n )\n\n const remove = useCallback<RecordRailStore<K>['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (draftsRef.current[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.records(kind).remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RecordEntryOf<K>[]>(recordListKey(kind, projectId), (prev) =>\n (prev ?? []).filter((record) => record.id !== id),\n )\n return { ok: true }\n },\n [kind, projectId, cache],\n )\n\n const store = useMemo<RecordRailStore<K>>(\n () => ({\n kind,\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n records: persisted as RecordEntryOf<K>[],\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [kind, projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RecordRailContext.Provider value={store as unknown as RecordRailStore}>{children}</RecordRailContext.Provider>\n}\n\nconst EMPTY: readonly never[] = []\n\n/**\n * The columns a blank \"New\" draft shows: each column's CREATE default (a new redirect\n * reads enabled + permanent — the reason you create one is for it to work), and `''` for\n * a required text column. The save then sends what the editor saw; the adapter still\n * applies the defaults itself for a write that leaves them out (EC-577).\n */\nexport function recordDraftColumns(kind: AnyRecordKind): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n for (const [name, field] of Object.entries(kind.fields)) {\n const parsed = (field as { safeParse(value: unknown): { success: boolean; data?: unknown } }).safeParse(undefined)\n if (parsed.success && parsed.data !== undefined) columns[name] = parsed.data\n else if ((field as { _zod: { def: { type: string } } })._zod.def.type === 'string') columns[name] = ''\n }\n return columns\n}\n\n/** The kind's own list order for a locally patched list (`orderBy`, else creation time). */\nfunction sortForRail<R>(kind: AnyRecordKind, records: R[]): R[] {\n const column = kind.orderBy ?? 'createdAt'\n return [...records].sort((a, b) => {\n const left = String((a as Record<string, unknown>)[column] ?? '')\n const right = String((b as Record<string, unknown>)[column] ?? '')\n return left < right ? -1 : left > right ? 1 : 0\n })\n}\n\n/**\n * EC-690 (critic point 5) — the findings a record has only as a member of its kind's SET,\n * judged by the ONE producer the studio's issues tray reads, so the record's own Issues pane\n * and the tray can never disagree. Before this, the losing row of a duplicate redirect pair\n * said \"No issues\" while the rail foot named it (measured live, wave-0100): the rail\n * validated one record without its siblings.\n *\n * Keyed by record kind; a kind with no set rule has no set findings. Redirects: core's one\n * normalised redirect set and router (`routeValidationIssues` — duplicates after\n * trailing-slash normalisation, loops, unreadable rows), over the project's routes.\n *\n * EC-703 (Codex round 6 F34): a `Map`, so a lookup finds only a rule declared here. As a plain\n * object, a kind named `constructor` (`toString`, `valueOf`, …) found the inherited function and\n * called it as a validator, and the rail threw (measured). `defineRecordKind` refuses those names\n * since EC-697; the registry is own-safe regardless, for a kind object built below it.\n */\nconst RECORD_SET_RULES: ReadonlyMap<string, (records: readonly Record<string, unknown>[]) => readonly CmsValidationIssue[]> = new Map([\n [\n redirectRecordKind.kind,\n (records: readonly Record<string, unknown>[]) =>\n routeValidationIssues(\n { routes: resolveDeploymentRoutes(), collections: resolveDeploymentCollections() },\n records as unknown as readonly RedirectEntry[],\n ),\n ],\n])\n\n/** The set rule's issues located AT one record (`[<domain>, <id>]`), keyed by record id. */\nfunction issuesById(issues: readonly CmsValidationIssue[]): Map<string, CmsValidationIssue[]> {\n const byId = new Map<string, CmsValidationIssue[]>()\n for (const issue of issues) {\n const id = issue.path[1]\n if (typeof id !== 'string') continue\n byId.set(id, [...(byId.get(id) ?? []), issue])\n }\n return byId\n}\n\n/**\n * The set findings of `doc` as the editor sees it NOW: the stored set with this record's\n * columns replaced by the draft's, so fixing a duplicate `from` clears the finding as you\n * type. An unsaved \"New\" draft joins the set as an extra row; it is only ever told about a\n * DUPLICATE (a half-typed draft is not an \"unreadable stored redirect\" — its required fields\n * report that, field by field). Pure; exported for the tests.\n */\nexport function recordSetIssues(\n kind: AnyRecordKind,\n records: readonly Record<string, unknown>[],\n doc: CmsDocument,\n): CmsValidationIssue[] {\n const rule = RECORD_SET_RULES.get(kind.kind)\n if (!rule) return []\n const columns = recordColumnNames(kind)\n const index = records.findIndex((record) => record.id === doc.id)\n const draftColumns = Object.fromEntries(Object.entries(doc.values ?? {}).filter(([name]) => columns.has(name)))\n const own = { ...(index === -1 ? {} : records[index]), ...draftColumns, id: doc.id }\n const set = index === -1 ? [...records, own] : records.map((record, i) => (i === index ? own : record))\n const located = issuesById(rule(set)).get(doc.id) ?? []\n return index === -1 ? located.filter((issue) => issue.meta?.duplicateOf !== undefined) : located\n}\n\n/** True when the draft's columns are the stored record's — the memoised stored verdict applies. */\nfunction draftMatchesStored(kind: AnyRecordKind, stored: Record<string, unknown>, doc: CmsDocument): boolean {\n const values = doc.values ?? {}\n for (const column of recordColumnNames(kind)) {\n if (!Object.is(values[column], stored[column])) return false\n }\n return true\n}\n\n/** How a rail addresses its list and one record's detail (the view's own route segment). */\nexport interface RecordRailPaths {\n listPath: string\n detailPath(id: string): string\n}\n\n/**\n * The {@link CollectionViewSource} of a record rail (must be mounted under its\n * {@link RecordRailProvider}). `schema` is the rail's lens — the kind's default lens or a\n * mount-customized one (EC-311 overrides by name; additive fields persist in `meta`).\n */\nexport function useRecordCollectionViewSource<K extends AnyRecordKind>(\n kind: K,\n projectId: string,\n schema: CollectionDef,\n paths: RecordRailPaths,\n capabilities: CollectionViewCapabilities = RECORD_RAIL_CAPABILITIES,\n): CollectionViewSource {\n const store = useRecordRail<K>()\n // Run the (possibly customized) lens's validation — required/pattern and custom\n // `validate` fns on an overridden field must actually fire in the editor (EC-311).\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n const singular = schema.form?.labelSingular\n const titleField = typeof schema.titleField === 'string' ? schema.titleField : undefined\n\n const documents = useCallback(\n () => store.records.map((record) => recordToLensDocument(kind, record)),\n [kind, store.records],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? recordToLensDocument(kind, record) : undefined\n },\n [kind, store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.get(ref.id) as Record<string, unknown> | undefined\n const title = titleField ? record?.[titleField] : undefined\n if (typeof title === 'string' && title.length > 0) return title\n return translate('records.untitled', { label: localizedLabel(singular) ?? kind.kind })\n },\n [store, titleField, singular, kind],\n )\n // EC-690: the record's own fields AND its set-level findings (a duplicate redirect, a loop) —\n // the stored set's verdict is computed once per list; a draft that changed a column is\n // re-judged against its siblings.\n const records = store.records as unknown as readonly Record<string, unknown>[]\n const storedSetIssues = useMemo(() => {\n const rule = RECORD_SET_RULES.get(kind.kind)\n return rule ? issuesById(rule(records)) : undefined\n }, [kind, records])\n const validate = useCallback(\n (doc: CmsDocument) => {\n const own = validateDocument(registry, doc, { localeConfig: RAIL_LOCALE_CONFIG })\n if (!storedSetIssues) return own\n const stored = records.find((record) => record.id === doc.id)\n const set =\n stored && draftMatchesStored(kind, stored, doc)\n ? (storedSetIssues.get(doc.id) ?? [])\n : recordSetIssues(kind, records, doc)\n return set.length > 0 ? [...own, ...set] : own\n },\n [registry, storedSetIssues, records, kind],\n )\n const create = useCallback((): DocumentRef => ({ collection: kind.kind, id: store.createDraft() }), [kind, store])\n // EC-616 (multi-session): an edit of a STORED record writes only what the editor changed\n // against the record they opened (`base`, from the edit shell) — the EC-577 rule keeps every\n // other column, so another writer's change made meanwhile survives this save. A record the\n // editor is creating (an unsaved draft) is written whole.\n const save = useCallback(\n (doc: CmsDocument, base?: CmsDocument) => {\n const stored = store.records.some((record) => (record as { id: string }).id === doc.id)\n return store.save(lensDocumentToRecordWrite(kind, doc, stored ? base : undefined))\n },\n [kind, store],\n )\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig: RAIL_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n detailPath: paths.detailPath,\n listPath: paths.listPath,\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n\nexport interface RecordRailModuleProps<K extends AnyRecordKind> {\n kind: K\n projectId: string\n /** `null` → the list; an id → that record's detail. */\n recordId: string | null\n schema: CollectionDef\n paths: RecordRailPaths\n capabilities?: CollectionViewCapabilities\n /** The rail's label, for its loading / failure states. */\n label: string\n /** Optional copy for the loading / failure states (default: the generic catalog keys). */\n messages?: { loading?: () => string; loadFailed?: () => string }\n}\n\n/** The surface of a record rail: the portable list, or one record's detail. */\nexport function RecordRailModule<K extends AnyRecordKind>(props: RecordRailModuleProps<K>) {\n return (\n <RecordRailProvider kind={props.kind} projectId={props.projectId}>\n <RecordRailSurface {...props} />\n </RecordRailProvider>\n )\n}\n\nfunction RecordRailSurface<K extends AnyRecordKind>({\n kind,\n projectId,\n recordId,\n schema,\n paths,\n capabilities,\n label,\n messages,\n}: RecordRailModuleProps<K>) {\n const store = useRecordRail<K>()\n const source = useRecordCollectionViewSource(kind, projectId, schema, paths, capabilities)\n\n if (store.status !== 'ready') {\n return (\n <RailStoreStatus\n store={store}\n errorLabel={messages?.loadFailed?.() ?? translate('records.loadFailed', { label })}\n loadingLabel={messages?.loading?.() ?? translate('records.loading', { label })}\n errorTestId={`${kind.kind}-rail-error`}\n />\n )\n }\n // One record's detail opens in the portable edit shell (AD-19); the list otherwise.\n return <CollectionViewSurface source={source} projectId={projectId} documentId={recordId} />\n}\n"]}
|
package/dist/customization.d.ts
CHANGED
|
@@ -49,9 +49,9 @@ export type { StudioConfig, StudioCanvasTheme, StudioPreviewsConfig, CustomField
|
|
|
49
49
|
*/
|
|
50
50
|
export type { GeneratedPreviewLookup } from './components/builder/blocks-model.js';
|
|
51
51
|
/**
|
|
52
|
-
* Reusable elements (EC-467): the
|
|
53
|
-
*
|
|
54
|
-
* Absence means
|
|
52
|
+
* Reusable elements (EC-467): the one-line declaration of which component is the
|
|
53
|
+
* project's reuse embed — `defineStudio({ reuse: { component: 'project.Reusable' } })`.
|
|
54
|
+
* Absence means OFF (EC-728: declared, never inferred). Reusable elements are
|
|
55
55
|
* singular per project: one embed component, one reusable collection.
|
|
56
56
|
*/
|
|
57
57
|
export type { StudioReuseConfig } from './components/builder/reuse.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"customization.js","sourceRoot":"","sources":["../src/customization.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAuBnE;;;;;;GAMG;AACH,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAK1E,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAO5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,oFAAoF;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AACpE;;;;;;;GAOG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/customization` — the public customization API for a consuming\n * repo's `studio.config.tsx` (EC-236/237/238/239).\n *\n * Everything a repo customizes about the builder UI is **live studio code passed by\n * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom\n * field renderers, and (in time) branding. This config is studio-only: it is handed\n * to `createStudio(config, studioConfig)` at the mount and never enters the shared\n * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle\n * in one import surface: direct values, no registries, no string ids — the lone\n * string seam is `fieldRenderers`' control key, which the delivery-shared schema\n * references via `form.control`.\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 * // router.tsx (the mount)\n * import { createStudio } from '@elytracms/studio/customization'\n * import config from '../../elytra.config'\n * import studioConfig from './studio.config'\n * createStudio(config, studioConfig)\n * ```\n */\nexport { defineStudio } from './components/app-views/studio-config'\nexport type {\n StudioConfig,\n StudioCanvasTheme,\n StudioPreviewsConfig,\n CustomFieldRenderer,\n CustomFieldRendererProps,\n} from './components/app-views/studio-config'\n/**\n * The generated-preview seam (EC-439, filled by EC-440's `elytra previews`):\n * `defineStudio({ previews: { generated } })` supplies a pure\n * `componentId + fixtureName → URL` lookup for build-time fixture preview\n * images. Curated `manifest.previewImage` wins over it; without either, tiles\n * render an icon+name placeholder.\n */\nexport type { GeneratedPreviewLookup } from './components/builder/blocks-model'\n/**\n * Reusable elements (EC-467): the
|
|
1
|
+
{"version":3,"file":"customization.js","sourceRoot":"","sources":["../src/customization.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAuBnE;;;;;;GAMG;AACH,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAK1E,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAO5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,oFAAoF;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AACpE;;;;;;;GAOG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/customization` — the public customization API for a consuming\n * repo's `studio.config.tsx` (EC-236/237/238/239).\n *\n * Everything a repo customizes about the builder UI is **live studio code passed by\n * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom\n * field renderers, and (in time) branding. This config is studio-only: it is handed\n * to `createStudio(config, studioConfig)` at the mount and never enters the shared\n * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle\n * in one import surface: direct values, no registries, no string ids — the lone\n * string seam is `fieldRenderers`' control key, which the delivery-shared schema\n * references via `form.control`.\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 * // router.tsx (the mount)\n * import { createStudio } from '@elytracms/studio/customization'\n * import config from '../../elytra.config'\n * import studioConfig from './studio.config'\n * createStudio(config, studioConfig)\n * ```\n */\nexport { defineStudio } from './components/app-views/studio-config'\nexport type {\n StudioConfig,\n StudioCanvasTheme,\n StudioPreviewsConfig,\n CustomFieldRenderer,\n CustomFieldRendererProps,\n} from './components/app-views/studio-config'\n/**\n * The generated-preview seam (EC-439, filled by EC-440's `elytra previews`):\n * `defineStudio({ previews: { generated } })` supplies a pure\n * `componentId + fixtureName → URL` lookup for build-time fixture preview\n * images. Curated `manifest.previewImage` wins over it; without either, tiles\n * render an icon+name placeholder.\n */\nexport type { GeneratedPreviewLookup } from './components/builder/blocks-model'\n/**\n * Reusable elements (EC-467): the one-line declaration of which component is the\n * project's reuse embed — `defineStudio({ reuse: { component: 'project.Reusable' } })`.\n * Absence means OFF (EC-728: declared, never inferred). Reusable elements are\n * singular per project: one embed component, one reusable collection.\n */\nexport type { StudioReuseConfig } from './components/builder/reuse'\n/**\n * The EC-440 previews-manifest adapter: `elytra previews` writes\n * `studio/elytra-previews.json` + images under `public/elytra-previews/`; the\n * consumer wires the seam in two lines —\n * `import previewsManifest from './elytra-previews.json'` and\n * `previews: { generated: generatedPreviewsFromManifest(previewsManifest) }`.\n */\nexport { generatedPreviewsFromManifest } from './previews/manifest-format'\nexport type {\n GeneratedPreviewsManifest,\n GeneratedPreviewsManifestEntry,\n} from './previews/manifest-format'\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CmsViewConfig,\n BuiltinViewConfig,\n CustomViewConfig,\n} from './components/app-views/default-modules'\nexport type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module'\nexport { createStudio } from './lib/workspace'\n/** A ready-made example renderer; a consuming repo may use it or author its own. */\nexport { colorFieldRenderer } from './components/cms/field-renderer'\n/**\n * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({\n * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator\n * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.\n * Prefer `TranslationCatalog` (EC-398) when authoring: it is typed against the EN key\n * set, so unknown/stale keys fail typecheck — engine-shipped locales\n * (`@elytracms/studio/locales/de`) and spread-overrides of them use it.\n */\nexport { msg } from './lib/i18n'\nexport type {\n MessageCatalog,\n MessageKey,\n SourceMessage,\n StudioI18nConfig,\n TranslationCatalog,\n} from './lib/i18n'\n"]}
|