@elytracms/studio 0.0.100 → 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/redirect-migration.test.ts +1 -1
- package/convex/redirects.ts +2 -2
- package/convex/shared-capture.convex.test.ts +165 -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/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 +23 -7
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +18 -3
- package/dist/lib/i18n/messages.en.js +29 -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 +47 -0
- package/dist/lib/operations/cms-persistence.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"]}
|
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"]}
|
|
@@ -22,7 +22,17 @@
|
|
|
22
22
|
* stays enforced by the injected `validateRichTextEmbeds`;
|
|
23
23
|
* - a composition's STRUCTURE is never written (ratified 4c): a `blocks` field is not
|
|
24
24
|
* in the target shape at all (the catalog says why);
|
|
25
|
-
* -
|
|
25
|
+
* - writer-locked positions are untouchable (EC-716, owner ruling "Yes, honour
|
|
26
|
+
* readOnly"): `ai.exclude` and `form.readOnly`, at every depth — removed from the shape,
|
|
27
|
+
* and a value the model writes there anyway is DROPPED (the position keeps its initial
|
|
28
|
+
* value) and reported as a warning, like an id outside the leash;
|
|
29
|
+
* - a create starts from the INITIAL values a manual create gets (EC-716): core's
|
|
30
|
+
* `seedDefaults` — values and localized — so a field's default, a read-only one
|
|
31
|
+
* included, is on the AI's draft as it is on the editor's;
|
|
32
|
+
* - the slug role is DERIVED from the title with core's `autoDeriveSlug` (EC-715), as the
|
|
33
|
+
* slug control and translate derive it — never authored by the model;
|
|
34
|
+
* - what the draft leaves EMPTY is said (EC-715): every writable position without a value
|
|
35
|
+
* and every dropped id is on the result (`unfilled`), for the proposal and the model.
|
|
26
36
|
*
|
|
27
37
|
* Every value crosses from its writer form to its stored form through the ONE
|
|
28
38
|
* converter (`writer-form.ts`, EC-593) — the fold edit, translate and the read form
|
|
@@ -31,10 +41,10 @@
|
|
|
31
41
|
* The engine depends only on the injected `AiProvider`, so the FakeProvider runs the
|
|
32
42
|
* whole loop without a key. No data access, no service credentials here.
|
|
33
43
|
*/
|
|
34
|
-
import { cmsIssue, isFieldLocalized, resolveLabel, validateDocument, writeField, } from '@elytracms/core/cms-core';
|
|
44
|
+
import { autoDeriveSlug, cmsIssue, isFieldLocalized, isWriterLocked, resolveLabel, seedDefaults, seedObjectItem, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
|
|
35
45
|
import { assembleKnowledgeBlock } from './knowledge.js';
|
|
36
46
|
import { aiProvenance } from './provenance.js';
|
|
37
|
-
import { buildTargetShape } from './shape.js';
|
|
47
|
+
import { buildTargetShape, derivedSlugField } from './shape.js';
|
|
38
48
|
import { fromWriterFieldValue, heldReferencesOf } from './writer-form.js';
|
|
39
49
|
/** Default repair attempts after the first generation. */
|
|
40
50
|
export const DEFAULT_MAX_REPAIR_ATTEMPTS = 2;
|
|
@@ -56,15 +66,12 @@ function isPlainRecord(value) {
|
|
|
56
66
|
function asRecord(value) {
|
|
57
67
|
return isPlainRecord(value) ? value : {};
|
|
58
68
|
}
|
|
59
|
-
/** Compose the user-facing prompt: instruction + knowledge +
|
|
69
|
+
/** Compose the user-facing prompt: instruction + knowledge + prior issues. */
|
|
60
70
|
function buildPrompt(input, knowledge, priorIssues) {
|
|
61
71
|
const parts = [`Instruction:\n${input.instruction}`];
|
|
62
72
|
const knowledgeBlock = knowledge ? assembleKnowledgeBlock(knowledge) : '';
|
|
63
73
|
if (knowledgeBlock)
|
|
64
74
|
parts.push(knowledgeBlock);
|
|
65
|
-
if (input.existing && Object.keys(input.existing).length > 0) {
|
|
66
|
-
parts.push(`Existing values (reference — follow the instruction, fill gaps):\n${JSON.stringify(input.existing)}`);
|
|
67
|
-
}
|
|
68
75
|
if (priorIssues.length > 0) {
|
|
69
76
|
parts.push([
|
|
70
77
|
'The previous attempt was invalid. Fix these problems and return a corrected object:',
|
|
@@ -73,6 +80,35 @@ function buildPrompt(input, knowledge, priorIssues) {
|
|
|
73
80
|
}
|
|
74
81
|
return parts.join('\n\n');
|
|
75
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The values a NEW document starts with (EC-716) — THE seeding rule a manual create reads
|
|
85
|
+
* (`seedDefaults`, written in the project's default locale). Without a locale config there
|
|
86
|
+
* is no bucket a localized default could live in, so only the shared fields seed (the same
|
|
87
|
+
* per-field rule, `seedObjectItem`); a localized write then fails loudly below anyway.
|
|
88
|
+
*/
|
|
89
|
+
function initialValues(collection, localeConfig) {
|
|
90
|
+
if (localeConfig)
|
|
91
|
+
return seedDefaults(collection, localeConfig);
|
|
92
|
+
return { values: seedObjectItem(collection.fields.filter((field) => !isFieldLocalized(collection, field.name))) };
|
|
93
|
+
}
|
|
94
|
+
/** Whether a stored value is no value at all: absent, `null`, `''`, or an empty list. */
|
|
95
|
+
function isEmptyValue(value) {
|
|
96
|
+
return value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The writable top-level positions a draft left without a value (EC-715), in field order —
|
|
100
|
+
* a writer-locked field is no writer's to fill and a composition is the canvas's, so neither
|
|
101
|
+
* counts — followed by every id the leash dropped (at any depth).
|
|
102
|
+
*/
|
|
103
|
+
function unfilledPositions(collection, shape, valueOf, dropped) {
|
|
104
|
+
const offered = (shape.schema.properties ?? {});
|
|
105
|
+
const empty = collection.fields.flatMap((field) => {
|
|
106
|
+
if (isWriterLocked(field) || field.type === 'blocks' || !isEmptyValue(valueOf(field.name)))
|
|
107
|
+
return [];
|
|
108
|
+
return [{ field: field.name, at: [], reason: offered[field.name] === undefined ? 'no-candidates' : 'empty' }];
|
|
109
|
+
});
|
|
110
|
+
return [...empty, ...dropped];
|
|
111
|
+
}
|
|
76
112
|
/**
|
|
77
113
|
* Run the schema-driven authoring loop end to end. Returns a valid, leash-compliant
|
|
78
114
|
* patch with provenance, or — after exhausting the repair budget — the blocking
|
|
@@ -86,7 +122,6 @@ export async function applyContentWithShape(input) {
|
|
|
86
122
|
...(input.leash ? { leash: input.leash } : {}),
|
|
87
123
|
...(locale !== undefined ? { locale } : {}),
|
|
88
124
|
});
|
|
89
|
-
const excluded = new Set(shape.excluded);
|
|
90
125
|
// EC-497: THE bag, whole — the generated draft is judged by the rules the editor
|
|
91
126
|
// will judge it by, and no key can go missing by being forgotten at this call
|
|
92
127
|
// site. EC-580 removed the ONE deviation this used to state: a synthetic
|
|
@@ -96,12 +131,25 @@ export async function applyContentWithShape(input) {
|
|
|
96
131
|
// RULES D15). The project's config is the only default locale: a required
|
|
97
132
|
// localized field needs its default-locale value, here exactly as at publish.
|
|
98
133
|
const validateOptions = input.validateOptions ?? {};
|
|
134
|
+
// EC-580: every value is written through core's `writeField`, in the create's
|
|
135
|
+
// locale — or, when none is named, the project's DEFAULT locale (the only
|
|
136
|
+
// default there is). A localized field used to land on the shared `values` bag
|
|
137
|
+
// without a locale (the EC-390 plain shape, MODE F5b.bkt).
|
|
138
|
+
const writeLocale = locale ?? validateOptions.localeConfig?.default;
|
|
139
|
+
// EC-716: the draft starts from the initial values a manual create gets.
|
|
140
|
+
const initial = initialValues(collection, validateOptions.localeConfig);
|
|
141
|
+
/** The value a field's write replaces: its initial value in the bucket being written. */
|
|
142
|
+
const initialOf = (name) => isFieldLocalized(collection, name)
|
|
143
|
+
? writeLocale === undefined
|
|
144
|
+
? undefined
|
|
145
|
+
: initial.localized?.[writeLocale]?.[name]
|
|
146
|
+
: initial.values[name];
|
|
99
147
|
// EC-593: the ONE converter's context — the validator's own prop lookup, the
|
|
100
|
-
// conversation's leash, and the ids the
|
|
148
|
+
// conversation's leash, and the ids the initial values already reference.
|
|
101
149
|
const ctx = {
|
|
102
150
|
...(validateOptions.lookupManifest ? { lookupManifest: validateOptions.lookupManifest } : {}),
|
|
103
151
|
...(input.leash ? { leash: input.leash } : {}),
|
|
104
|
-
held: heldReferencesOf({ collection: collection.id, id: documentId,
|
|
152
|
+
held: heldReferencesOf({ collection: collection.id, id: documentId, ...initial }),
|
|
105
153
|
};
|
|
106
154
|
const totalAttempts = (input.maxRepairAttempts ?? DEFAULT_MAX_REPAIR_ATTEMPTS) + 1;
|
|
107
155
|
let priorIssues = [];
|
|
@@ -118,47 +166,52 @@ export async function applyContentWithShape(input) {
|
|
|
118
166
|
collection.id,
|
|
119
167
|
});
|
|
120
168
|
const object = asRecord(result.object);
|
|
121
|
-
// EC-580: every value is written through core's `writeField`, in the create's
|
|
122
|
-
// locale — or, when none is named, the project's DEFAULT locale (the only
|
|
123
|
-
// default there is). A localized field used to land on the shared `values` bag
|
|
124
|
-
// without a locale (the EC-390 plain shape, MODE F5b.bkt).
|
|
125
|
-
const writeLocale = locale ?? validateOptions.localeConfig?.default;
|
|
126
169
|
let doc = {
|
|
127
170
|
collection: collection.id,
|
|
128
171
|
id: documentId,
|
|
129
|
-
values: { ...
|
|
172
|
+
values: { ...initial.values },
|
|
173
|
+
...(initial.localized !== undefined ? { localized: { ...initial.localized } } : {}),
|
|
130
174
|
};
|
|
131
175
|
// EC-379: md→envelope conversion problems join the repair loop as explicit
|
|
132
176
|
// issues (the model gets them back verbatim) — never a silently-dropped body.
|
|
133
177
|
const conversionIssues = [];
|
|
134
|
-
// EC-381/EC-593: an id outside the leash is DROPPED (empty beats wrong)
|
|
135
|
-
//
|
|
136
|
-
|
|
178
|
+
// EC-381/EC-593: an id outside the leash is DROPPED (empty beats wrong); EC-716: so is
|
|
179
|
+
// a write to a locked position (it keeps its initial value). Each drop is said, as a
|
|
180
|
+
// warning on the proposal the editor reviews.
|
|
181
|
+
const dropWarnings = [];
|
|
182
|
+
// EC-715: the leash drops again, as positions — the proposal lists them as unfilled.
|
|
183
|
+
const droppedIds = [];
|
|
137
184
|
for (const field of collection.fields) {
|
|
138
|
-
|
|
185
|
+
// EC-715: the slug role is the engine's to derive (below), never the model's.
|
|
186
|
+
if (shape.derived.includes(field.name))
|
|
139
187
|
continue;
|
|
140
188
|
const raw = object[field.name];
|
|
141
189
|
if (raw === undefined || raw === null)
|
|
142
190
|
continue;
|
|
143
191
|
const fieldPath = ['documents', `${collection.id}:${documentId}`, 'values', field.name];
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
192
|
+
const current = initialOf(field.name);
|
|
193
|
+
// EC-593: writer form → stored form at every depth, the leash, the carry (EC-570 #3:
|
|
194
|
+
// from the initial value, through the one rule) and — EC-716 — the writer lock, at
|
|
195
|
+
// every position, the field's own included.
|
|
196
|
+
const converted = fromWriterFieldValue(field, raw, current, ctx, writeLocale);
|
|
147
197
|
for (const problem of converted.problems) {
|
|
198
|
+
const dropped = problem.leash === true || problem.locked === true;
|
|
148
199
|
const issue = cmsIssue({
|
|
149
200
|
code: problem.code,
|
|
150
201
|
message: problem.message,
|
|
151
202
|
path: [...fieldPath, ...problem.at],
|
|
152
203
|
collectionId: collection.id,
|
|
153
204
|
documentId,
|
|
154
|
-
...(
|
|
205
|
+
...(dropped ? { severity: 'warning' } : {}),
|
|
155
206
|
});
|
|
156
|
-
if (
|
|
157
|
-
|
|
207
|
+
if (dropped)
|
|
208
|
+
dropWarnings.push(issue);
|
|
158
209
|
else
|
|
159
210
|
conversionIssues.push(issue);
|
|
211
|
+
if (problem.leash)
|
|
212
|
+
droppedIds.push({ field: field.name, at: problem.at, reason: 'dropped' });
|
|
160
213
|
}
|
|
161
|
-
if (converted.value === undefined)
|
|
214
|
+
if (converted.value === undefined || converted.value === current)
|
|
162
215
|
continue;
|
|
163
216
|
if (isFieldLocalized(collection, field.name) && writeLocale === undefined) {
|
|
164
217
|
// A wiring gap, made loud: no locale named AND no project locale config to
|
|
@@ -174,6 +227,23 @@ export async function applyContentWithShape(input) {
|
|
|
174
227
|
}
|
|
175
228
|
doc = writeField(collection, doc, field.name, writeLocale, converted.value);
|
|
176
229
|
}
|
|
230
|
+
/** A field's value in the bucket this create writes. */
|
|
231
|
+
const valueOf = (name) => isFieldLocalized(collection, name)
|
|
232
|
+
? writeLocale === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: doc.localized?.[writeLocale]?.[name]
|
|
235
|
+
: doc.values[name];
|
|
236
|
+
// EC-715: the slug from its source in the written locale's displayed view — THE
|
|
237
|
+
// derivation (`autoDeriveSlug`) — unless the initial values already hold one.
|
|
238
|
+
const slugField = derivedSlugField(collection);
|
|
239
|
+
if (slugField && isEmptyValue(valueOf(slugField.name)) && !(isFieldLocalized(collection, slugField.name) && writeLocale === undefined)) {
|
|
240
|
+
const view = writeLocale !== undefined && validateOptions.localeConfig
|
|
241
|
+
? valuesInLocale(collection, doc, writeLocale, validateOptions.localeConfig)
|
|
242
|
+
: { ...doc.values, ...(writeLocale !== undefined ? doc.localized?.[writeLocale] : {}) };
|
|
243
|
+
const slug = autoDeriveSlug(slugField.slug, collection, view);
|
|
244
|
+
if (slug !== '')
|
|
245
|
+
doc = writeField(collection, doc, slugField.name, writeLocale, slug);
|
|
246
|
+
}
|
|
177
247
|
const issues = validateDocument(registry, doc, validateOptions);
|
|
178
248
|
const errors = [...conversionIssues, ...issues.filter((i) => i.severity === 'error')];
|
|
179
249
|
if (errors.length === 0) {
|
|
@@ -188,7 +258,8 @@ export async function applyContentWithShape(input) {
|
|
|
188
258
|
patch,
|
|
189
259
|
provenance,
|
|
190
260
|
attempts: attempt,
|
|
191
|
-
issues: [...
|
|
261
|
+
issues: [...dropWarnings, ...issues.filter((i) => i.severity !== 'error')],
|
|
262
|
+
unfilled: unfilledPositions(collection, shape, valueOf, droppedIds),
|
|
192
263
|
};
|
|
193
264
|
}
|
|
194
265
|
lastErrors = errors;
|