@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
|
@@ -11,6 +11,7 @@ import { useCmsCollectionViewSource } from '../collection-view/cms-collection-vi
|
|
|
11
11
|
import { CollectionViewProvider } from '../collection-view/collection-view-context.js';
|
|
12
12
|
import { DocumentDiffView } from '../cms/document-diff-view.js';
|
|
13
13
|
import { ProposalIssues } from './proposal-issues.js';
|
|
14
|
+
import { ProposalUnfilled } from './proposal-unfilled.js';
|
|
14
15
|
import { TranslateSkipped } from './translate-skipped.js';
|
|
15
16
|
/**
|
|
16
17
|
* The AI proposal review (EC-199, extended by EC-332/EC-378): a valid,
|
|
@@ -102,7 +103,7 @@ export function ProposalCardView({ proposal, collection, source, onApply, onDisc
|
|
|
102
103
|
// roomy full-height sheet is offered for a large proposal (never a modal form).
|
|
103
104
|
const [reviewOpen, setReviewOpen] = useState(false);
|
|
104
105
|
const reviewable = diff !== null && source !== null;
|
|
105
|
-
return (_jsxs(Stack, { gap: "2", className: "overflow-hidden rounded-xl border border-border bg-card", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/60 bg-muted/30 px-3 py-2", children: [_jsx(Sparkles, { className: "size-4 shrink-0 text-primary" }), _jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: captions.title }), _jsx(Spacer, {}), applied ? _jsx(Badge, { variant: "success", children: translate('ai.proposal.applied') }) : null, rejected ? _jsx(Badge, { variant: "outline", children: translate('ai.proposal.discarded') }) : null, reviewable ? (_jsx(Button, { size: "icon-sm", variant: "ghost", onClick: () => setReviewOpen(true), title: translate('ai.proposal.expandReview'), "aria-label": translate('ai.proposal.expandReview'), children: _jsx(Maximize2, {}) })) : null] }), _jsx("div", { className: "px-3", children: reviewable ? (_jsx(CollectionViewProvider, { source: source, children: _jsx(DocumentDiffView, { diff: diff, collection: collection, columnHeaders: columnHeaders, layout: "stacked" }) })) : (_jsx(Text, { size: "xs", tone: "muted", className: "py-2", children: translate('ai.proposal.unavailable') })) }), _jsx(ProposalIssues, { issues: proposal.issues, className: "px-3" }), proposal.skipped && proposal.skipped.length > 0 ? (_jsx("div", { className: "px-3", children: _jsx(TranslateSkipped, { skipped: proposal.skipped, collection: collection }) })) : null, _jsx(Row, { gap: "2", align: "center", className: "border-t border-border/60 px-3 py-2", children: proposal.status === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", tone: "muted", className: "min-w-0 flex-1", children: translate('ai.proposal.reviewHint') }), _jsxs(Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: [_jsx(X, {}), " ", translate('ai.proposal.discard')] }), _jsxs(Button, { size: "sm", onClick: onApply, children: [_jsx(Check, {}), " ", translate('ai.proposal.apply')] })] })) : applied ? (_jsxs(_Fragment, { children: [_jsx(Spacer, {}), _jsxs(Button, { size: "sm", variant: "outline", onClick: onOpen, children: [_jsx(ArrowUpRight, {}), " ", translate('ai.proposal.open')] })] })) : (_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.proposal.discardedNote') })) }), reviewable && reviewOpen ? (_jsx(ProposalReviewSheet, { title: captions.title, diff: diff, collection: collection, source: source, columnHeaders: columnHeaders, issues: proposal.issues, status: proposal.status, onApply: onApply, onDiscard: onDiscard, onClose: () => setReviewOpen(false) })) : null] }));
|
|
106
|
+
return (_jsxs(Stack, { gap: "2", className: "overflow-hidden rounded-xl border border-border bg-card", children: [_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border/60 bg-muted/30 px-3 py-2", children: [_jsx(Sparkles, { className: "size-4 shrink-0 text-primary" }), _jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 truncate", children: captions.title }), _jsx(Spacer, {}), applied ? _jsx(Badge, { variant: "success", children: translate('ai.proposal.applied') }) : null, rejected ? _jsx(Badge, { variant: "outline", children: translate('ai.proposal.discarded') }) : null, reviewable ? (_jsx(Button, { size: "icon-sm", variant: "ghost", onClick: () => setReviewOpen(true), title: translate('ai.proposal.expandReview'), "aria-label": translate('ai.proposal.expandReview'), children: _jsx(Maximize2, {}) })) : null] }), _jsx("div", { className: "px-3", children: reviewable ? (_jsx(CollectionViewProvider, { source: source, children: _jsx(DocumentDiffView, { diff: diff, collection: collection, columnHeaders: columnHeaders, layout: "stacked" }) })) : (_jsx(Text, { size: "xs", tone: "muted", className: "py-2", children: translate('ai.proposal.unavailable') })) }), _jsx(ProposalIssues, { issues: proposal.issues, className: "px-3" }), proposal.skipped && proposal.skipped.length > 0 ? (_jsx("div", { className: "px-3", children: _jsx(TranslateSkipped, { skipped: proposal.skipped, collection: collection }) })) : null, proposal.unfilled && proposal.unfilled.length > 0 ? (_jsx("div", { className: "px-3", children: _jsx(ProposalUnfilled, { unfilled: proposal.unfilled, collection: collection }) })) : null, _jsx(Row, { gap: "2", align: "center", className: "border-t border-border/60 px-3 py-2", children: proposal.status === 'pending' ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", tone: "muted", className: "min-w-0 flex-1", children: translate('ai.proposal.reviewHint') }), _jsxs(Button, { size: "sm", variant: "ghost", onClick: onDiscard, children: [_jsx(X, {}), " ", translate('ai.proposal.discard')] }), _jsxs(Button, { size: "sm", onClick: onApply, children: [_jsx(Check, {}), " ", translate('ai.proposal.apply')] })] })) : applied ? (_jsxs(_Fragment, { children: [_jsx(Spacer, {}), _jsxs(Button, { size: "sm", variant: "outline", onClick: onOpen, children: [_jsx(ArrowUpRight, {}), " ", translate('ai.proposal.open')] })] })) : (_jsx(Text, { size: "xs", tone: "muted", children: translate('ai.proposal.discardedNote') })) }), reviewable && reviewOpen ? (_jsx(ProposalReviewSheet, { title: captions.title, diff: diff, collection: collection, source: source, columnHeaders: columnHeaders, issues: proposal.issues, status: proposal.status, onApply: onApply, onDiscard: onDiscard, onClose: () => setReviewOpen(false) })) : null] }));
|
|
106
107
|
}
|
|
107
108
|
/**
|
|
108
109
|
* The expandable review surface (EC-382): the proposal's diff promoted into a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposal-card.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-card.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAEvD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEvD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACjG,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,+CAA+C,CAAA;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB,CAC9B,MAAsB,EACtB,eAAuB;IAEvB,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;QAC3B,OAAO;YACL,KAAK,EAAE,SAAS,CAAC,4BAA4B,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;YAC/E,OAAO,EAAE,SAAS,CAAC,yBAAyB,CAAC;YAC7C,MAAM,EAAE,SAAS,CAAC,yBAAyB,CAAC;YAC5C,KAAK,EAAE,SAAS,CAAC,wBAAwB,CAAC;SAC3C,CAAA;IACH,CAAC;IACD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO;YACL,KAAK,EAAE,SAAS,CAAC,yBAAyB,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;YAC5E,OAAO,EAAE,SAAS,CAAC,yBAAyB,CAAC;YAC7C,MAAM,EAAE,SAAS,CAAC,sBAAsB,CAAC;YACzC,KAAK,EAAE,SAAS,CAAC,qBAAqB,CAAC;SACxC,CAAA;IACH,CAAC;IACD,OAAO;QACL,KAAK,EAAE,SAAS,CAAC,mBAAmB,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;QACtE,OAAO,EAAE,SAAS,CAAC,yBAAyB,CAAC;QAC7C,MAAM,EAAE,SAAS,CAAC,wBAAwB,CAAC;QAC3C,KAAK,EAAE,SAAS,CAAC,uBAAuB,CAAC;KAC1C,CAAA;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAC3B,QAAQ,EACR,OAAO,EACP,SAAS,EACT,MAAM,GAMP;IACC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;IACzD,MAAM,MAAM,GAAG,0BAA0B,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;IAC9D,OAAO,CACL,KAAC,gBAAgB,IACf,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,GACd,CACH,CAAA;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,gBAAgB,CAAC,EAC/B,QAAQ,EACR,UAAU,EACV,MAAM,EACN,OAAO,EACP,SAAS,EACT,MAAM,GAQP;IACC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE;QACxB,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAA;QAC5B,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,iBAAiB,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;QAC5E,OAAO,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;IACjD,CAAC,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IAEjD,MAAM,eAAe,GACnB,CAAC,UAAU,EAAE,IAAI,EAAE,aAAa,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAChF,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChE,QAAQ,CAAC,UAAU,CAAA;IAErB,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IACnE,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAA;IAC7C,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,KAAK,UAAU,CAAA;IAC/C,MAAM,aAAa,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAA;IAEnG,4EAA4E;IAC5E,gFAAgF;IAChF,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnD,MAAM,UAAU,GAAG,IAAI,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,CAAA;IAEnD,OAAO,CACL,MAAC,KAAK,IACJ,GAAG,EAAC,GAAG,EACP,SAAS,EAAC,yDAAyD,aAEnE,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,iDAAiD,aACrF,KAAC,QAAQ,IAAC,SAAS,EAAC,8BAA8B,GAAG,EACrD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,QAAQ,EAAC,SAAS,EAAC,kBAAkB,YACzD,QAAQ,CAAC,KAAK,GACV,EACP,KAAC,MAAM,KAAG,EACT,OAAO,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAE,SAAS,CAAC,qBAAqB,CAAC,GAAS,CAAC,CAAC,CAAC,IAAI,EACpF,QAAQ,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAE,SAAS,CAAC,uBAAuB,CAAC,GAAS,CAAC,CAAC,CAAC,IAAI,EACvF,UAAU,CAAC,CAAC,CAAC,CACZ,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,EACf,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAClC,KAAK,EAAE,SAAS,CAAC,0BAA0B,CAAC,gBAChC,SAAS,CAAC,0BAA0B,CAAC,YAEjD,KAAC,SAAS,KAAG,GACN,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,EAEN,cAAK,SAAS,EAAC,MAAM,YAClB,UAAU,CAAC,CAAC,CAAC,CACZ,KAAC,sBAAsB,IAAC,MAAM,EAAE,MAAO,YACrC,KAAC,gBAAgB,IACf,IAAI,EAAE,IAAK,EACX,UAAU,EAAE,UAAW,EACvB,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAC,SAAS,GAChB,GACqB,CAC1B,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,MAAM,YAC1C,SAAS,CAAC,yBAAyB,CAAC,GAChC,CACR,GACG,EAGN,KAAC,cAAc,IAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAC,MAAM,GAAG,EAE3D,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACjD,cAAK,SAAS,EAAC,MAAM,YACnB,KAAC,gBAAgB,IAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,GAAI,GACnE,CACP,CAAC,CAAC,CAAC,IAAI,EAER,KAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,qCAAqC,YACxE,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAC/B,8BACE,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,gBAAgB,YACpD,SAAS,CAAC,wBAAwB,CAAC,GAC/B,EACP,MAAC,MAAM,IAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAE,SAAS,aAClD,KAAC,CAAC,KAAG,OAAE,SAAS,CAAC,qBAAqB,CAAC,IAChC,EACT,MAAC,MAAM,IAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,OAAO,aAChC,KAAC,KAAK,KAAG,OAAE,SAAS,CAAC,mBAAmB,CAAC,IAClC,IACR,CACJ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CACZ,8BACE,KAAC,MAAM,KAAG,EACV,MAAC,MAAM,IAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,OAAO,EAAE,MAAM,aACjD,KAAC,YAAY,KAAG,OAAE,SAAS,CAAC,kBAAkB,CAAC,IACxC,IACR,CACJ,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,2BAA2B,CAAC,GAClC,CACR,GACG,EAEL,UAAU,IAAI,UAAU,CAAC,CAAC,CAAC,CAC1B,KAAC,mBAAmB,IAClB,KAAK,EAAE,QAAQ,CAAC,KAAK,EACrB,IAAI,EAAE,IAAK,EACX,UAAU,EAAE,UAAW,EACvB,MAAM,EAAE,MAAO,EACf,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,QAAQ,CAAC,MAAM,EACvB,MAAM,EAAE,QAAQ,CAAC,MAAM,EACvB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,GACnC,CACH,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,mBAAmB,CAAC,EAC3B,KAAK,EACL,IAAI,EACJ,UAAU,EACV,MAAM,EACN,aAAa,EACb,MAAM,EACN,MAAM,EACN,OAAO,EACP,SAAS,EACT,OAAO,GAaR;IACC,OAAO,CACL,MAAC,iBAAiB,IAChB,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,SAAS,CAAC,4BAA4B,CAAC,EACnD,KAAK,EAAC,kBAAkB,EACxB,SAAS,EAAE,OAAO,EAClB,aAAa,EAAE,KAAC,QAAQ,IAAC,SAAS,EAAC,8BAA8B,GAAG,aAEpE,cAAK,SAAS,EAAC,2CAA2C,YACxD,eAAK,SAAS,EAAC,0BAA0B,aACvC,KAAC,sBAAsB,IAAC,MAAM,EAAE,MAAM,YACpC,KAAC,gBAAgB,IAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,GAAI,GAC/D,EACzB,KAAC,cAAc,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAC,MAAM,GAAG,IAC/C,GACF,EAEL,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CACtB,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,2CAA2C,aAC/E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,gBAAgB,YACpD,SAAS,CAAC,wBAAwB,CAAC,GAC/B,EACP,MAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,OAAO,EACf,OAAO,EAAE,GAAG,EAAE;4BACZ,SAAS,EAAE,CAAA;4BACX,OAAO,EAAE,CAAA;wBACX,CAAC,aAED,KAAC,CAAC,KAAG,OAAE,SAAS,CAAC,qBAAqB,CAAC,IAChC,EACT,MAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,GAAG,EAAE;4BACZ,OAAO,EAAE,CAAA;4BACT,OAAO,EAAE,CAAA;wBACX,CAAC,aAED,KAAC,KAAK,KAAG,OAAE,SAAS,CAAC,mBAAmB,CAAC,IAClC,IACL,CACP,CAAC,CAAC,CAAC,IAAI,IACU,CACrB,CAAA;AACH,CAAC","sourcesContent":["import { useMemo, useState } from 'react'\nimport { ArrowUpRight, Check, Maximize2, Sparkles, X } from 'lucide-react'\nimport { diffDocuments } from '@elytracms/core/content'\nimport type { DocumentDiff } from '@elytracms/core/content'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue, CollectionDef } from '@elytracms/core/cms-core'\nimport { Badge, Button, ExpandableSurface, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { proposalDocuments } from '../../lib/ai'\nimport type { ProposalOrigin, ProposalReview } from '../../lib/ai'\nimport type { CollectionViewSource } from '../../lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { DocumentDiffView } from '../cms/document-diff-view'\nimport { ProposalIssues } from './proposal-issues'\nimport { TranslateSkipped } from './translate-skipped'\n\n/**\n * The AI proposal review (EC-199, extended by EC-332/EC-378): a valid,\n * leash-compliant proposal the editor reviews as a DIFF (the shared EC-317\n * {@link DocumentDiffView} — never a second renderer) before applying it as a\n * draft or discarding it. Create proposals diff `empty-before` vs\n * `proposed-after`; translate/update proposals diff the EXISTING document\n * (old → new). Apply/discard are handled by the panel (the operations write +\n * provenance snapshot happen there); this card is the surface.\n *\n * {@link ProposalCard} is the CMS-wired shell; {@link ProposalCardView} is the\n * presentational card over an injected collection + view source (node-smoked\n * with `renderToStaticMarkup`, house pattern).\n *\n * ## EC-496 — the proposal's own findings\n *\n * The diff answers *what changes*; {@link ProposalIssues} answers *what the engine\n * flagged about it*, immediately above the Apply/Discard row so it is read while the\n * decision is being made. Two calls made here, both deliberate:\n *\n * **An error on the card does NOT block apply.** Grounded before deciding\n * (`file:line`, read 2026-08-01): every producer already filters this list to\n * non-error findings (`lib/ai/apply-content.ts:259`, `lib/ai/update-fields.ts:442`,\n * `lib/ai/tools.ts:764`), so a card-blocking gate would be dead code today. More\n * importantly the real gate already exists one layer up and is strictly better\n * placed: EC-473 made apply RE-VALIDATE against the session as it is NOW\n * (`ai-chat-panel.tsx:371`) and refuse with `ai.error.applyInvalid` when the write\n * would INTRODUCE an error. A build-time snapshot cannot see an edit the editor made\n * since, and gating on pre-existing brokenness is exactly what EC-473 argued makes an\n * assistant unusable on real content. So this surface REPORTS truthfully — errors\n * render as errors and sort first ({@link orderedProposalIssues}) — and the gate\n * stays where it can see the truth.\n *\n * **`attempts` is not rendered** (the EC-496 scope-2 owner call). The repair-loop\n * count is provenance about the MODEL, not about the content: a proposal repaired on\n * the second attempt passed the identical validation gate as a first-shot one, so the\n * number changes nothing an editor can act on — and spending sidebar space plus a new\n * catalog string on it would trade a real signal (the findings) for a trivia line. The\n * field stays on `ChatProposal` (it is deterministic, cheap, and the natural input for\n * a future AI-diagnostics surface); it simply has no place on the editor's card.\n */\n\n/** The card's origin-dependent captions (title + diff column headers). */\nexport function proposalCaptions(\n origin: ProposalOrigin,\n collectionLabel: string,\n): { title: string; unified: string; before: string; after: string } {\n if (origin === 'translate') {\n return {\n title: translate('ai.translate.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.translate.diffBefore'),\n after: translate('ai.translate.diffAfter'),\n }\n }\n if (origin === 'update') {\n return {\n title: translate('ai.update.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.update.diffBefore'),\n after: translate('ai.update.diffAfter'),\n }\n }\n return {\n title: translate('ai.proposal.title', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.proposal.diffBefore'),\n after: translate('ai.proposal.diffAfter'),\n }\n}\n\nexport function ProposalCard({\n proposal,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const cms = useCms()\n const collection = cms.getCollection(proposal.collection)\n const source = useCmsCollectionViewSource(proposal.collection)\n return (\n <ProposalCardView\n proposal={proposal}\n collection={collection}\n source={source}\n onApply={onApply}\n onDiscard={onDiscard}\n onOpen={onOpen}\n />\n )\n}\n\n/** The presentational proposal card over an injected collection + view source. */\nexport function ProposalCardView({\n proposal,\n collection,\n source,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n collection: CollectionDef | undefined\n source: CollectionViewSource | null\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const diff = useMemo(() => {\n if (!collection) return null\n const { before, after } = proposalDocuments(proposal.patch, proposal.before)\n return diffDocuments(collection, before, after)\n }, [collection, proposal.patch, proposal.before])\n\n const collectionLabel =\n (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??\n (collection?.form?.label && resolveLabel(collection.form.label)) ??\n proposal.collection\n\n const captions = proposalCaptions(proposal.origin, collectionLabel)\n const applied = proposal.status === 'applied'\n const rejected = proposal.status === 'rejected'\n const columnHeaders = { unified: captions.unified, before: captions.before, after: captions.after }\n\n // EC-382 — the expandable review surface: the sidebar card stays stacked; a\n // roomy full-height sheet is offered for a large proposal (never a modal form).\n const [reviewOpen, setReviewOpen] = useState(false)\n const reviewable = diff !== null && source !== null\n\n return (\n <Stack\n gap=\"2\"\n className=\"overflow-hidden rounded-xl border border-border bg-card\"\n >\n <Row gap=\"2\" align=\"center\" className=\"border-b border-border/60 bg-muted/30 px-3 py-2\">\n <Sparkles className=\"size-4 shrink-0 text-primary\" />\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {captions.title}\n </Text>\n <Spacer />\n {applied ? <Badge variant=\"success\">{translate('ai.proposal.applied')}</Badge> : null}\n {rejected ? <Badge variant=\"outline\">{translate('ai.proposal.discarded')}</Badge> : null}\n {reviewable ? (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setReviewOpen(true)}\n title={translate('ai.proposal.expandReview')}\n aria-label={translate('ai.proposal.expandReview')}\n >\n <Maximize2 />\n </Button>\n ) : null}\n </Row>\n\n <div className=\"px-3\">\n {reviewable ? (\n <CollectionViewProvider source={source!}>\n <DocumentDiffView\n diff={diff!}\n collection={collection!}\n columnHeaders={columnHeaders}\n layout=\"stacked\"\n />\n </CollectionViewProvider>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"py-2\">\n {translate('ai.proposal.unavailable')}\n </Text>\n )}\n </div>\n\n {/* EC-496 — the engine's surviving findings, last thing before the actions. */}\n <ProposalIssues issues={proposal.issues} className=\"px-3\" />\n {/* EC-593 — what a translation could not translate, listed (never skipped silently). */}\n {proposal.skipped && proposal.skipped.length > 0 ? (\n <div className=\"px-3\">\n <TranslateSkipped skipped={proposal.skipped} collection={collection} />\n </div>\n ) : null}\n\n <Row gap=\"2\" align=\"center\" className=\"border-t border-border/60 px-3 py-2\">\n {proposal.status === 'pending' ? (\n <>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDiscard}>\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button size=\"sm\" onClick={onApply}>\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </>\n ) : applied ? (\n <>\n <Spacer />\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen}>\n <ArrowUpRight /> {translate('ai.proposal.open')}\n </Button>\n </>\n ) : (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.proposal.discardedNote')}\n </Text>\n )}\n </Row>\n\n {reviewable && reviewOpen ? (\n <ProposalReviewSheet\n title={captions.title}\n diff={diff!}\n collection={collection!}\n source={source!}\n columnHeaders={columnHeaders}\n issues={proposal.issues}\n status={proposal.status}\n onApply={onApply}\n onDiscard={onDiscard}\n onClose={() => setReviewOpen(false)}\n />\n ) : null}\n </Stack>\n )\n}\n\n/**\n * The expandable review surface (EC-382): the proposal's diff promoted into a\n * roomy, full-height right-anchored sheet where the wide `grid` layout is\n * comfortable — for a large proposal the 320px sidebar can only cramp. It is a\n * REVIEW surface, not a form, so it stays lightly dismissible (backdrop click /\n * Escape / close button) rather than a trapping modal — the forms-never-modal\n * spirit, applied. Presentation only: Apply/Discard call the same handlers the\n * card does; it changes no proposal data. Since EC-392 it renders through the\n * shared {@link ExpandableSurface} primitive (portal-to-body, backdrop, header\n * close, layer-stacked Escape) at the shipped 720px width, so it escapes the\n * narrow AI panel; the card default keeps it closed, so the SSR smoke path\n * never mounts the portal.\n */\nfunction ProposalReviewSheet({\n title,\n diff,\n collection,\n source,\n columnHeaders,\n issues,\n status,\n onApply,\n onDiscard,\n onClose,\n}: {\n title: string\n diff: DocumentDiff\n collection: CollectionDef\n source: CollectionViewSource\n columnHeaders: { unified: string; before: string; after: string }\n /** EC-496 — the same findings the card shows; the roomy surface must not hide them. */\n issues: readonly CmsValidationIssue[]\n status: ProposalReview['status']\n onApply: () => void\n onDiscard: () => void\n onClose: () => void\n}) {\n return (\n <ExpandableSurface\n title={title}\n closeLabel={translate('ai.proposal.collapseReview')}\n width=\"min(720px, 92vw)\"\n onDismiss={onClose}\n headerLeading={<Sparkles className=\"size-4 shrink-0 text-primary\" />}\n >\n <div className=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-3xl\">\n <CollectionViewProvider source={source}>\n <DocumentDiffView diff={diff} collection={collection} columnHeaders={columnHeaders} />\n </CollectionViewProvider>\n <ProposalIssues issues={issues} className=\"mt-4\" />\n </div>\n </div>\n\n {status === 'pending' ? (\n <Row gap=\"2\" align=\"center\" className=\"shrink-0 border-t border-border px-4 py-3\">\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n onDiscard()\n onClose()\n }}\n >\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button\n size=\"sm\"\n onClick={() => {\n onApply()\n onClose()\n }}\n >\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </Row>\n ) : null}\n </ExpandableSurface>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"proposal-card.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-card.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { ArrowUpRight, Check, Maximize2, Sparkles, X } from 'lucide-react'\nimport { diffDocuments } from '@elytracms/core/content'\nimport type { DocumentDiff } from '@elytracms/core/content'\nimport { resolveLabel } from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue, CollectionDef } from '@elytracms/core/cms-core'\nimport { Badge, Button, ExpandableSurface, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { proposalDocuments } from '../../lib/ai'\nimport type { ProposalOrigin, ProposalReview } from '../../lib/ai'\nimport type { CollectionViewSource } from '../../lib/collection-view/collection-view-source'\nimport { useCms } from '../cms/cms-store'\nimport { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { DocumentDiffView } from '../cms/document-diff-view'\nimport { ProposalIssues } from './proposal-issues'\nimport { ProposalUnfilled } from './proposal-unfilled'\nimport { TranslateSkipped } from './translate-skipped'\n\n/**\n * The AI proposal review (EC-199, extended by EC-332/EC-378): a valid,\n * leash-compliant proposal the editor reviews as a DIFF (the shared EC-317\n * {@link DocumentDiffView} — never a second renderer) before applying it as a\n * draft or discarding it. Create proposals diff `empty-before` vs\n * `proposed-after`; translate/update proposals diff the EXISTING document\n * (old → new). Apply/discard are handled by the panel (the operations write +\n * provenance snapshot happen there); this card is the surface.\n *\n * {@link ProposalCard} is the CMS-wired shell; {@link ProposalCardView} is the\n * presentational card over an injected collection + view source (node-smoked\n * with `renderToStaticMarkup`, house pattern).\n *\n * ## EC-496 — the proposal's own findings\n *\n * The diff answers *what changes*; {@link ProposalIssues} answers *what the engine\n * flagged about it*, immediately above the Apply/Discard row so it is read while the\n * decision is being made. Two calls made here, both deliberate:\n *\n * **An error on the card does NOT block apply.** Grounded before deciding\n * (`file:line`, read 2026-08-01): every producer already filters this list to\n * non-error findings (`lib/ai/apply-content.ts:259`, `lib/ai/update-fields.ts:442`,\n * `lib/ai/tools.ts:764`), so a card-blocking gate would be dead code today. More\n * importantly the real gate already exists one layer up and is strictly better\n * placed: EC-473 made apply RE-VALIDATE against the session as it is NOW\n * (`ai-chat-panel.tsx:371`) and refuse with `ai.error.applyInvalid` when the write\n * would INTRODUCE an error. A build-time snapshot cannot see an edit the editor made\n * since, and gating on pre-existing brokenness is exactly what EC-473 argued makes an\n * assistant unusable on real content. So this surface REPORTS truthfully — errors\n * render as errors and sort first ({@link orderedProposalIssues}) — and the gate\n * stays where it can see the truth.\n *\n * **`attempts` is not rendered** (the EC-496 scope-2 owner call). The repair-loop\n * count is provenance about the MODEL, not about the content: a proposal repaired on\n * the second attempt passed the identical validation gate as a first-shot one, so the\n * number changes nothing an editor can act on — and spending sidebar space plus a new\n * catalog string on it would trade a real signal (the findings) for a trivia line. The\n * field stays on `ChatProposal` (it is deterministic, cheap, and the natural input for\n * a future AI-diagnostics surface); it simply has no place on the editor's card.\n */\n\n/** The card's origin-dependent captions (title + diff column headers). */\nexport function proposalCaptions(\n origin: ProposalOrigin,\n collectionLabel: string,\n): { title: string; unified: string; before: string; after: string } {\n if (origin === 'translate') {\n return {\n title: translate('ai.translate.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.translate.diffBefore'),\n after: translate('ai.translate.diffAfter'),\n }\n }\n if (origin === 'update') {\n return {\n title: translate('ai.update.proposalTitle', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.update.diffBefore'),\n after: translate('ai.update.diffAfter'),\n }\n }\n return {\n title: translate('ai.proposal.title', { collection: collectionLabel }),\n unified: translate('ai.proposal.diffUnified'),\n before: translate('ai.proposal.diffBefore'),\n after: translate('ai.proposal.diffAfter'),\n }\n}\n\nexport function ProposalCard({\n proposal,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const cms = useCms()\n const collection = cms.getCollection(proposal.collection)\n const source = useCmsCollectionViewSource(proposal.collection)\n return (\n <ProposalCardView\n proposal={proposal}\n collection={collection}\n source={source}\n onApply={onApply}\n onDiscard={onDiscard}\n onOpen={onOpen}\n />\n )\n}\n\n/** The presentational proposal card over an injected collection + view source. */\nexport function ProposalCardView({\n proposal,\n collection,\n source,\n onApply,\n onDiscard,\n onOpen,\n}: {\n proposal: ProposalReview\n collection: CollectionDef | undefined\n source: CollectionViewSource | null\n onApply: () => void\n onDiscard: () => void\n onOpen: () => void\n}) {\n const diff = useMemo(() => {\n if (!collection) return null\n const { before, after } = proposalDocuments(proposal.patch, proposal.before)\n return diffDocuments(collection, before, after)\n }, [collection, proposal.patch, proposal.before])\n\n const collectionLabel =\n (collection?.form?.labelSingular && resolveLabel(collection.form.labelSingular)) ??\n (collection?.form?.label && resolveLabel(collection.form.label)) ??\n proposal.collection\n\n const captions = proposalCaptions(proposal.origin, collectionLabel)\n const applied = proposal.status === 'applied'\n const rejected = proposal.status === 'rejected'\n const columnHeaders = { unified: captions.unified, before: captions.before, after: captions.after }\n\n // EC-382 — the expandable review surface: the sidebar card stays stacked; a\n // roomy full-height sheet is offered for a large proposal (never a modal form).\n const [reviewOpen, setReviewOpen] = useState(false)\n const reviewable = diff !== null && source !== null\n\n return (\n <Stack\n gap=\"2\"\n className=\"overflow-hidden rounded-xl border border-border bg-card\"\n >\n <Row gap=\"2\" align=\"center\" className=\"border-b border-border/60 bg-muted/30 px-3 py-2\">\n <Sparkles className=\"size-4 shrink-0 text-primary\" />\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {captions.title}\n </Text>\n <Spacer />\n {applied ? <Badge variant=\"success\">{translate('ai.proposal.applied')}</Badge> : null}\n {rejected ? <Badge variant=\"outline\">{translate('ai.proposal.discarded')}</Badge> : null}\n {reviewable ? (\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setReviewOpen(true)}\n title={translate('ai.proposal.expandReview')}\n aria-label={translate('ai.proposal.expandReview')}\n >\n <Maximize2 />\n </Button>\n ) : null}\n </Row>\n\n <div className=\"px-3\">\n {reviewable ? (\n <CollectionViewProvider source={source!}>\n <DocumentDiffView\n diff={diff!}\n collection={collection!}\n columnHeaders={columnHeaders}\n layout=\"stacked\"\n />\n </CollectionViewProvider>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"py-2\">\n {translate('ai.proposal.unavailable')}\n </Text>\n )}\n </div>\n\n {/* EC-496 — the engine's surviving findings, last thing before the actions. */}\n <ProposalIssues issues={proposal.issues} className=\"px-3\" />\n {/* EC-593 — what a translation could not translate, listed (never skipped silently). */}\n {proposal.skipped && proposal.skipped.length > 0 ? (\n <div className=\"px-3\">\n <TranslateSkipped skipped={proposal.skipped} collection={collection} />\n </div>\n ) : null}\n {/* EC-715 — what a create left empty, listed (an unfilled image is said, never just missing). */}\n {proposal.unfilled && proposal.unfilled.length > 0 ? (\n <div className=\"px-3\">\n <ProposalUnfilled unfilled={proposal.unfilled} collection={collection} />\n </div>\n ) : null}\n\n <Row gap=\"2\" align=\"center\" className=\"border-t border-border/60 px-3 py-2\">\n {proposal.status === 'pending' ? (\n <>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDiscard}>\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button size=\"sm\" onClick={onApply}>\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </>\n ) : applied ? (\n <>\n <Spacer />\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen}>\n <ArrowUpRight /> {translate('ai.proposal.open')}\n </Button>\n </>\n ) : (\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.proposal.discardedNote')}\n </Text>\n )}\n </Row>\n\n {reviewable && reviewOpen ? (\n <ProposalReviewSheet\n title={captions.title}\n diff={diff!}\n collection={collection!}\n source={source!}\n columnHeaders={columnHeaders}\n issues={proposal.issues}\n status={proposal.status}\n onApply={onApply}\n onDiscard={onDiscard}\n onClose={() => setReviewOpen(false)}\n />\n ) : null}\n </Stack>\n )\n}\n\n/**\n * The expandable review surface (EC-382): the proposal's diff promoted into a\n * roomy, full-height right-anchored sheet where the wide `grid` layout is\n * comfortable — for a large proposal the 320px sidebar can only cramp. It is a\n * REVIEW surface, not a form, so it stays lightly dismissible (backdrop click /\n * Escape / close button) rather than a trapping modal — the forms-never-modal\n * spirit, applied. Presentation only: Apply/Discard call the same handlers the\n * card does; it changes no proposal data. Since EC-392 it renders through the\n * shared {@link ExpandableSurface} primitive (portal-to-body, backdrop, header\n * close, layer-stacked Escape) at the shipped 720px width, so it escapes the\n * narrow AI panel; the card default keeps it closed, so the SSR smoke path\n * never mounts the portal.\n */\nfunction ProposalReviewSheet({\n title,\n diff,\n collection,\n source,\n columnHeaders,\n issues,\n status,\n onApply,\n onDiscard,\n onClose,\n}: {\n title: string\n diff: DocumentDiff\n collection: CollectionDef\n source: CollectionViewSource\n columnHeaders: { unified: string; before: string; after: string }\n /** EC-496 — the same findings the card shows; the roomy surface must not hide them. */\n issues: readonly CmsValidationIssue[]\n status: ProposalReview['status']\n onApply: () => void\n onDiscard: () => void\n onClose: () => void\n}) {\n return (\n <ExpandableSurface\n title={title}\n closeLabel={translate('ai.proposal.collapseReview')}\n width=\"min(720px, 92vw)\"\n onDismiss={onClose}\n headerLeading={<Sparkles className=\"size-4 shrink-0 text-primary\" />}\n >\n <div className=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n <div className=\"mx-auto w-full max-w-3xl\">\n <CollectionViewProvider source={source}>\n <DocumentDiffView diff={diff} collection={collection} columnHeaders={columnHeaders} />\n </CollectionViewProvider>\n <ProposalIssues issues={issues} className=\"mt-4\" />\n </div>\n </div>\n\n {status === 'pending' ? (\n <Row gap=\"2\" align=\"center\" className=\"shrink-0 border-t border-border px-4 py-3\">\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1\">\n {translate('ai.proposal.reviewHint')}\n </Text>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n onDiscard()\n onClose()\n }}\n >\n <X /> {translate('ai.proposal.discard')}\n </Button>\n <Button\n size=\"sm\"\n onClick={() => {\n onApply()\n onClose()\n }}\n >\n <Check /> {translate('ai.proposal.apply')}\n </Button>\n </Row>\n ) : null}\n </ExpandableSurface>\n )\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { UnfilledPosition } from '../../lib/ai/index.js';
|
|
3
|
+
/** One unfilled position as the editor reads it: where (field label › position) and why. */
|
|
4
|
+
export interface UnfilledLine {
|
|
5
|
+
where: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
/** The where-label: the field's label, then the position below it (an item counted from 1). */
|
|
9
|
+
export declare function unfilledLines(unfilled: readonly UnfilledPosition[], collection: CollectionDef | undefined): UnfilledLine[];
|
|
10
|
+
/** The list; renders nothing when the draft filled every writable position. */
|
|
11
|
+
export declare function ProposalUnfilled({ unfilled, collection, }: {
|
|
12
|
+
unfilled: readonly UnfilledPosition[];
|
|
13
|
+
collection: CollectionDef | undefined;
|
|
14
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveLabel } from '@elytracms/core/cms-core';
|
|
3
|
+
import { CircleDashed } from 'lucide-react';
|
|
4
|
+
import { Row, Stack, Text } from '@elytracms/studio/ui';
|
|
5
|
+
import { translate } from '../../lib/i18n/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* EC-715 — what an AI create left EMPTY, shown on its proposal card: a writable field the
|
|
8
|
+
* draft gave no value (a date, an image the library had nothing for), and an id the leash
|
|
9
|
+
* removed. Before, a create that asked for an image left the field empty and said nothing.
|
|
10
|
+
*/
|
|
11
|
+
const REASON_KEY = {
|
|
12
|
+
empty: 'ai.create.unfilled.empty',
|
|
13
|
+
'no-candidates': 'ai.create.unfilled.noCandidates',
|
|
14
|
+
dropped: 'ai.create.unfilled.dropped',
|
|
15
|
+
};
|
|
16
|
+
/** The where-label: the field's label, then the position below it (an item counted from 1). */
|
|
17
|
+
export function unfilledLines(unfilled, collection) {
|
|
18
|
+
return unfilled.map((position) => {
|
|
19
|
+
const field = collection?.fields.find((f) => f.name === position.field);
|
|
20
|
+
const label = resolveLabel(field?.form?.label) ?? position.field;
|
|
21
|
+
const segments = position.at
|
|
22
|
+
.filter((segment) => segment !== 'content' && segment !== 'props')
|
|
23
|
+
.map((segment) => (typeof segment === 'number' ? `#${segment + 1}` : segment));
|
|
24
|
+
return { where: [label, ...segments].join(' › '), reason: translate(REASON_KEY[position.reason]) };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** The list; renders nothing when the draft filled every writable position. */
|
|
28
|
+
export function ProposalUnfilled({ unfilled, collection, }) {
|
|
29
|
+
if (unfilled.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
return (_jsxs(Stack, { gap: "1", "data-testid": "proposal-unfilled", children: [_jsxs(Row, { gap: "1", align: "center", children: [_jsx(CircleDashed, { className: "size-3.5 text-muted-foreground" }), _jsx(Text, { size: "xs", weight: "medium", children: translate('ai.create.unfilledTitle', { count: unfilled.length }) })] }), unfilledLines(unfilled, collection).map((line, index) => (_jsxs(Text, { size: "xs", tone: "muted", "data-proposal-unfilled": true, children: [line.where, " \u2014 ", line.reason] }, index)))] }));
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=proposal-unfilled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposal-unfilled.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-unfilled.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEvD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAG1C;;;;GAIG;AAEH,MAAM,UAAU,GAA+C;IAC7D,KAAK,EAAE,0BAA0B;IACjC,eAAe,EAAE,iCAAiC;IAClD,OAAO,EAAE,4BAA4B;CACtC,CAAA;AAQD,+FAA+F;AAC/F,MAAM,UAAU,aAAa,CAC3B,QAAqC,EACrC,UAAqC;IAErC,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QAC/B,MAAM,KAAK,GAAG,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvE,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAA;QAChE,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE;aACzB,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,OAAO,CAAC;aACjE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;QAChF,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAA;IACpG,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,EAC/B,QAAQ,EACR,UAAU,GAIX;IACC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACtC,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,iBAAa,mBAAmB,aAC5C,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aACzB,KAAC,YAAY,IAAC,SAAS,EAAC,gCAAgC,GAAG,EAC3D,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,QAAQ,YAC5B,SAAS,CAAC,yBAAyB,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAC5D,IACH,EACL,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CACxD,MAAC,IAAI,IAAa,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,6CACrC,IAAI,CAAC,KAAK,cAAK,IAAI,CAAC,MAAM,KADlB,KAAK,CAET,CACR,CAAC,IACI,CACT,CAAA;AACH,CAAC","sourcesContent":["import { resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { CircleDashed } from 'lucide-react'\nimport { Row, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport type { UnfilledPosition } from '../../lib/ai'\n\n/**\n * EC-715 — what an AI create left EMPTY, shown on its proposal card: a writable field the\n * draft gave no value (a date, an image the library had nothing for), and an id the leash\n * removed. Before, a create that asked for an image left the field empty and said nothing.\n */\n\nconst REASON_KEY: Record<UnfilledPosition['reason'], string> = {\n empty: 'ai.create.unfilled.empty',\n 'no-candidates': 'ai.create.unfilled.noCandidates',\n dropped: 'ai.create.unfilled.dropped',\n}\n\n/** One unfilled position as the editor reads it: where (field label › position) and why. */\nexport interface UnfilledLine {\n where: string\n reason: string\n}\n\n/** The where-label: the field's label, then the position below it (an item counted from 1). */\nexport function unfilledLines(\n unfilled: readonly UnfilledPosition[],\n collection: CollectionDef | undefined,\n): UnfilledLine[] {\n return unfilled.map((position) => {\n const field = collection?.fields.find((f) => f.name === position.field)\n const label = resolveLabel(field?.form?.label) ?? position.field\n const segments = position.at\n .filter((segment) => segment !== 'content' && segment !== 'props')\n .map((segment) => (typeof segment === 'number' ? `#${segment + 1}` : segment))\n return { where: [label, ...segments].join(' › '), reason: translate(REASON_KEY[position.reason]) }\n })\n}\n\n/** The list; renders nothing when the draft filled every writable position. */\nexport function ProposalUnfilled({\n unfilled,\n collection,\n}: {\n unfilled: readonly UnfilledPosition[]\n collection: CollectionDef | undefined\n}) {\n if (unfilled.length === 0) return null\n return (\n <Stack gap=\"1\" data-testid=\"proposal-unfilled\">\n <Row gap=\"1\" align=\"center\">\n <CircleDashed className=\"size-3.5 text-muted-foreground\" />\n <Text size=\"xs\" weight=\"medium\">\n {translate('ai.create.unfilledTitle', { count: unfilled.length })}\n </Text>\n </Row>\n {unfilledLines(unfilled, collection).map((line, index) => (\n <Text key={index} size=\"xs\" tone=\"muted\" data-proposal-unfilled>\n {line.where} — {line.reason}\n </Text>\n ))}\n </Stack>\n )\n}\n"]}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Suspense, lazy, useMemo, useState } from 'react';
|
|
3
3
|
import { Languages } from 'lucide-react';
|
|
4
|
-
import { isFieldLocalized } from '@elytracms/core/cms-core';
|
|
5
|
-
import { Button } from '@elytracms/studio/ui';
|
|
4
|
+
import { isFieldLocalized, isWriterLocked } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Button, Dialog, DialogContent, DialogTitle } from '@elytracms/studio/ui';
|
|
6
6
|
import { translate } from '../../lib/i18n/index.js';
|
|
7
7
|
import { resolveDeploymentAi } from '../../lib/workspace/index.js';
|
|
8
|
+
import { LazySurfaceBoundary } from '../load-failure.js';
|
|
8
9
|
/**
|
|
9
10
|
* The "Translate document" header action (EC-332), gate side.
|
|
10
11
|
*
|
|
11
12
|
* Rendered ALWAYS in the detail-editor header but returns null unless the studio is
|
|
12
|
-
* `ai`-configured (EC-331 presence-enable), the collection
|
|
13
|
-
*
|
|
13
|
+
* `ai`-configured (EC-331 presence-enable), the collection has a real localized field a
|
|
14
|
+
* writer may fill (EC-716: not `ai.exclude`, not `form.readOnly`), and there is a target
|
|
15
|
+
* locale to fill. Like the AI panel toggle, the
|
|
14
16
|
* gate reads only the config object + pure core helpers — it never statically imports
|
|
15
17
|
* the AI engine (`@elytracms/studio/lib/ai`) or the AI SDK. The heavy dialog (which runs
|
|
16
18
|
* the translation engine + operations write) loads ONLY when the editor opens it, via a
|
|
@@ -29,10 +31,10 @@ export function TranslateDocumentAction(props) {
|
|
|
29
31
|
const base = localeConfig.default;
|
|
30
32
|
if (!localeConfig.locales.some((l) => l !== base))
|
|
31
33
|
return false;
|
|
32
|
-
return collection.fields.some((f) => isFieldLocalized(collection, f.name));
|
|
34
|
+
return collection.fields.some((f) => isFieldLocalized(collection, f.name) && !isWriterLocked(f));
|
|
33
35
|
}, [collection, localeConfig, localesEnabled, projectId]);
|
|
34
36
|
if (!eligible)
|
|
35
37
|
return null;
|
|
36
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setOpen(true), "data-testid": "doc-translate", title: translate('ai.translate.tooltip'), children: [_jsx(Languages, {}), " ", translate('ai.translate.action')] }), open ? (_jsx(Suspense, { fallback: null, children: _jsx(TranslateDocumentDialog, { ...props, open: true, onOpenChange: setOpen }) })) : null] }));
|
|
38
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setOpen(true), "data-testid": "doc-translate", title: translate('ai.translate.tooltip'), children: [_jsx(Languages, {}), " ", translate('ai.translate.action')] }), open ? (_jsx(LazySurfaceBoundary, { frame: (notice) => (_jsx(Dialog, { open: true, onOpenChange: setOpen, children: _jsxs(DialogContent, { "aria-describedby": undefined, children: [_jsx(DialogTitle, { children: translate('ai.translate.action') }), notice] }) })), children: _jsx(Suspense, { fallback: null, children: _jsx(TranslateDocumentDialog, { ...props, open: true, onOpenChange: setOpen }) }) })) : null] }));
|
|
37
39
|
}
|
|
38
40
|
//# sourceMappingURL=translate-document-action.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translate-document-action.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-action.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"translate-document-action.js","sourceRoot":"","sources":["../../../src/components/ai/translate-document-action.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAE3E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,uBAAuB,GAAG,IAAI,CAAC,GAAG,EAAE,CACxC,MAAM,CAAC,6BAA6B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAC5F,CAAA;AA2BD,MAAM,UAAU,uBAAuB,CAAC,KAAmC;IACzE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;IAErE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE;QAC5B,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAA;QAChC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,KAAK,KAAK;YAAE,OAAO,KAAK,CAAA;QAC7C,IAAI,CAAC,cAAc,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QAC5D,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAA;QACjC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QAC/D,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;IAClG,CAAC,EAAE,CAAC,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,CAAC,CAAC,CAAA;IAEzD,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAE1B,OAAO,CACL,8BACE,MAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,SAAS,EACjB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,iBAChB,eAAe,EAC3B,KAAK,EAAE,SAAS,CAAC,sBAAsB,CAAC,aAExC,KAAC,SAAS,KAAG,OAAE,SAAS,CAAC,qBAAqB,CAAC,IACxC,EACR,IAAI,CAAC,CAAC,CAAC,CAGN,KAAC,mBAAmB,IAClB,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CACjB,KAAC,MAAM,IAAC,IAAI,QAAC,YAAY,EAAE,OAAO,YAChC,MAAC,aAAa,wBAAmB,SAAS,aACxC,KAAC,WAAW,cAAE,SAAS,CAAC,qBAAqB,CAAC,GAAe,EAC5D,MAAM,IACO,GACT,CACV,YAED,KAAC,QAAQ,IAAC,QAAQ,EAAE,IAAI,YACtB,KAAC,uBAAuB,OAAK,KAAK,EAAE,IAAI,QAAC,YAAY,EAAE,OAAO,GAAI,GACzD,GACS,CACvB,CAAC,CAAC,CAAC,IAAI,IACP,CACJ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy, useMemo, useState } from 'react'\nimport { Languages } from 'lucide-react'\nimport { isFieldLocalized, isWriterLocked } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentRef, LocaleConfig } from '@elytracms/core/cms-core'\nimport { Button, Dialog, DialogContent, DialogTitle } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { resolveDeploymentAi } from '../../lib/workspace'\nimport { LazySurfaceBoundary } from '../load-failure'\n\n/**\n * The \"Translate document\" header action (EC-332), gate side.\n *\n * Rendered ALWAYS in the detail-editor header but returns null unless the studio is\n * `ai`-configured (EC-331 presence-enable), the collection has a real localized field a\n * writer may fill (EC-716: not `ai.exclude`, not `form.readOnly`), and there is a target\n * locale to fill. Like the AI panel toggle, the\n * gate reads only the config object + pure core helpers — it never statically imports\n * the AI engine (`@elytracms/studio/lib/ai`) or the AI SDK. The heavy dialog (which runs\n * the translation engine + operations write) loads ONLY when the editor opens it, via a\n * dynamic `import()`, so an unconfigured studio pulls none of it onto the critical path.\n */\nconst TranslateDocumentDialog = lazy(() =>\n import('./translate-document-dialog').then((m) => ({ default: m.TranslateDocumentDialog })),\n)\n\nexport interface TranslateDocumentActionProps {\n projectId: string | undefined\n collection: CollectionDef\n docRef: DocumentRef\n /** The live editor draft — the translation source + the target for the fills. */\n document: CmsDocument\n /**\n * EC-647 — the revision the draft is at (`DocumentDraft.revision`): the translation is\n * authored on {@link document}, so its fills are written AT this revision — a bucket a\n * colleague changed since is refused on that field, never overwritten.\n */\n revision: number | undefined\n localeConfig: LocaleConfig\n /** Whether the collection view exposes per-locale editing (`source.capabilities.locales`). */\n localesEnabled: boolean\n /**\n * EC-595 A2 — the FORCED FLUSH before the AI reads the draft (the ruling: \"a forced\n * flush before … an AI proposal\"): the editor commits its uncommitted fields and the\n * persister lands them, so the translation's patches are made on the committed draft.\n * The translated buckets reach the open editor through the session (it rebases), so\n * there is no draft to hand back.\n */\n flushDraft: () => Promise<void>\n}\n\nexport function TranslateDocumentAction(props: TranslateDocumentActionProps) {\n const [open, setOpen] = useState(false)\n const { collection, localeConfig, localesEnabled, projectId } = props\n\n const eligible = useMemo(() => {\n const ai = resolveDeploymentAi()\n if (!ai || ai.enabled === false) return false\n if (!localesEnabled || projectId === undefined) return false\n const base = localeConfig.default\n if (!localeConfig.locales.some((l) => l !== base)) return false\n return collection.fields.some((f) => isFieldLocalized(collection, f.name) && !isWriterLocked(f))\n }, [collection, localeConfig, localesEnabled, projectId])\n\n if (!eligible) return null\n\n return (\n <>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => setOpen(true)}\n data-testid=\"doc-translate\"\n title={translate('ai.translate.tooltip')}\n >\n <Languages /> {translate('ai.translate.action')}\n </Button>\n {open ? (\n // EC-717: a dialog chunk that fails to load shows its notice where the dialog\n // opens, closable like it; never the whole studio.\n <LazySurfaceBoundary\n frame={(notice) => (\n <Dialog open onOpenChange={setOpen}>\n <DialogContent aria-describedby={undefined}>\n <DialogTitle>{translate('ai.translate.action')}</DialogTitle>\n {notice}\n </DialogContent>\n </Dialog>\n )}\n >\n <Suspense fallback={null}>\n <TranslateDocumentDialog {...props} open onOpenChange={setOpen} />\n </Suspense>\n </LazySurfaceBoundary>\n ) : null}\n </>\n )\n}\n"]}
|
|
@@ -84,16 +84,16 @@ export interface StudioConfig {
|
|
|
84
84
|
previews?: StudioPreviewsConfig;
|
|
85
85
|
/**
|
|
86
86
|
* Reusable elements (EC-467, AD-b): WHICH component is this project's
|
|
87
|
-
* reusable-element embed
|
|
88
|
-
* `reuse: { component: 'project.Reusable' }`.
|
|
87
|
+
* reusable-element embed — `reuse: { component: 'project.Reusable' }`.
|
|
89
88
|
*
|
|
90
|
-
* **ABSENCE MEANS
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* whose target
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
89
|
+
* **ABSENCE MEANS OFF (EC-728, owner ruling "Declare, never infer").** Like the
|
|
90
|
+
* `ai?` precedent, omitting this block means the project has no reusable
|
|
91
|
+
* elements in the studio: no *Make reusable* / *Detach to local copy* gestures,
|
|
92
|
+
* and no canvas placeholder for an embed whose target is a draft or missing (the
|
|
93
|
+
* embed then renders empty in the canvas). The studio never guesses an embed from
|
|
94
|
+
* a component's shape — that guess crowned an ordinary link card and moved a
|
|
95
|
+
* block's content off the page. Transclusion itself (an embed rendering its
|
|
96
|
+
* target's blocks) does not depend on this declaration.
|
|
97
97
|
*
|
|
98
98
|
* **Reusable elements are singular per project (AD-a):** exactly one embed
|
|
99
99
|
* component and exactly one reusable collection. This is one value, never a
|
|
@@ -102,10 +102,10 @@ export interface StudioConfig {
|
|
|
102
102
|
* collection → that collection's `blocks` field), which is why the declaration
|
|
103
103
|
* stays a single identifier.
|
|
104
104
|
*
|
|
105
|
-
* Studio-only, like every key here —
|
|
106
|
-
* gestures
|
|
105
|
+
* Studio-only, like every key here — what it drives is studio-side (the canvas
|
|
106
|
+
* gestures and placeholder), so it has zero delivery-wire surface and never
|
|
107
107
|
* reaches the `frontend/` bundle. A declaration that cannot resolve is a visible
|
|
108
|
-
* validation issue, never a silent fallback
|
|
108
|
+
* validation issue in the header Issues popover, never a silent fallback.
|
|
109
109
|
*/
|
|
110
110
|
reuse?: StudioReuseConfig;
|
|
111
111
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AAuKA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport type { StudioI18nConfig } from '../../lib/i18n'\nimport type { StudioAiConfig } from '../../lib/ai'\nimport type { GeneratedPreviewLookup } from '../builder/blocks-model'\nimport type { StudioReuseConfig } from '../builder/reuse'\nimport type { SidebarNode } from './module'\nimport type { DocumentPane } from '../inspector/document-panes'\n\n/**\n * EC-236 — the studio customization config: the single, studio-only home for\n * everything a consuming repo customizes about the builder UI.\n *\n * This is the \"separate studio-only config solution\": it lives in the consumer's\n * `studio.config.tsx`, is `defineStudio({...})`-typed, and is passed to\n * `createStudio(config, studioConfig)` at the mount — NEVER mixed into the shared\n * `elytra.config.ts`, so the live React it carries (views, icons, field renderers,\n * future branding) never reaches the `frontend/` delivery bundle.\n *\n * The customization PRINCIPLE it embodies: customization is **code passed\n * directly**, not data referenced by a string key. Every field here holds live\n * values/components by reference — `sidebar` is built from `nav.*` view objects,\n * `fieldRenderers` maps a control name to a component. The only string indirection\n * in the whole surface is `fieldRenderers`' key, and ONLY because its other end\n * (`form.control` on a field) lives in the delivery-shared serializable schema and\n * cannot import studio code. This config grows by adding direct-value keys\n * (branding/logo/panels/theme), never by adding registries.\n */\nexport interface StudioConfig {\n /**\n * The section rail (EC-237) as an ordered list of `nav.*` nodes — views, groups,\n * and separators. Omitted → the studio derives the default rail from its built-in\n * views. A rail node carries its view object directly; there are no string ids to\n * keep in sync.\n */\n sidebar?: SidebarNode[]\n /**\n * Custom field renderers (EC-238) keyed by CONTROL name (`form.control`), each a\n * React component replacing the built-in document-editor control. A field opts in\n * per-field via `form: { control: '<name>' }` — so a renderer targets exactly the\n * fields that ask for it, not every field of a datatype. This is the one\n * boundary-crossing seam: the field names its control by string in the shared\n * schema, this map binds that name to a studio-only component.\n */\n fieldRenderers?: Record<string, CustomFieldRenderer>\n /**\n * Studio-UI localization (EC-241): the default locale + imported translation\n * catalogs. English is the built-in source; a consumer adds e.g.\n * `i18n: { locales: { de: deMessages } }` and the studio chrome renders in the\n * browser's preferred locale (no persisted selector in v1). Studio-only — never\n * reaches the delivery bundle. Schema labels localize separately, via a\n * `LocalizedString` on the field/collection def.\n */\n i18n?: StudioI18nConfig\n /**\n * AI platform (EC-331): provider, project knowledge, and soft limits. The block's\n * PRESENCE enables the AI feature; its ABSENCE means the feature is fully absent —\n * no AI UI and no AI dependencies on the critical path (the runtime lives behind a\n * lazy boundary in `@elytracms/studio/lib/ai`, only reached when configured).\n * `enabled: false` is a temporary kill-switch. Studio-only live code (holds the\n * provider adapter + knowledge functions) — never reaches the delivery bundle.\n * Validated at load by `resolveAiConfig` (capability gate: tool calling + structured\n * output required, fail-loud).\n */\n ai?: StudioAiConfig\n /**\n * Canvas page-surface theming (EC-389): make the preview iframe wear the SITE's\n * chrome. Direct values (no registries): body classes + raw CSS, injected LAST\n * into the canvas iframe — after the studio defaults — so consumer values win\n * the cascade. This themes the canvas\n * PAGE surface only, never the studio chrome (branding/logo theming is a future,\n * separate key).\n */\n canvas?: StudioCanvasTheme\n /**\n * Insert-menu preview artifacts (EC-439, the seam EC-440 fills): where the\n * studio's blocks panel finds GENERATED preview images for component\n * fixtures. Studio-only (from `defineStudio`, never the delivery bundle) and\n * fully optional — absent, tiles fall back from a curated\n * `manifest.previewImage` straight to the icon+name placeholder. The EC-440\n * CLI (`elytra previews`) renders fixtures headlessly against the consumer\n * theme and writes a previews manifest; the consumer adapts that manifest\n * into `previews.generated` here.\n */\n previews?: StudioPreviewsConfig\n /**\n * Reusable elements (EC-467, AD-b): WHICH component is this project's\n * reusable-element embed. One line, and only when needed —\n * `reuse: { component: 'project.Reusable' }`.\n *\n * **ABSENCE MEANS INFER, not off.** Unlike the `ai?` precedent, omitting this\n * block does not disable anything: the studio derives the embed from config as\n * it always has (a slot-less component with one cardinality-one `relation` prop\n * whose target collection holds a `blocks` field), so the zero-config promise is\n * untouched and no existing consumer needs to change anything. Declare it only\n * when the inference cannot decide — the studio then says so, by name, in the\n * rail's issue list rather than silently switching the reuse gestures off.\n *\n * **Reusable elements are singular per project (AD-a):** exactly one embed\n * component and exactly one reusable collection. This is one value, never a\n * list. The collection and the composition field DERIVE from the declared\n * component (its sole single-document relation prop → that prop's target\n * collection → that collection's `blocks` field), which is why the declaration\n * stays a single identifier.\n *\n * Studio-only, like every key here — the whole feature is studio-side (canvas\n * gestures + publish integrity), so it has zero delivery-wire surface and never\n * reaches the `frontend/` bundle. A declaration that cannot resolve is a visible\n * validation issue, never a silent fallback to guessing.\n */\n reuse?: StudioReuseConfig\n /**\n * Panes contributed to the document editor's inspector (EC-618 — AD-19's edit-shell\n * slot), beside the frame's own Details / Versions / Issues: direct values, built with\n * `documentPane({ id, label, render })` from `@elytracms/studio/addon` or exported by an\n * addon (`analyticsDocumentPane`). CMS documents only; a pane's `collections` narrows it.\n * Studio-only, like every key here.\n */\n documentPanes?: DocumentPane[]\n}\n\n/**\n * The generated-preview seam (EC-439 AD-2). `generated` is a PURE lookup —\n * component id + fixture name → image URL (`undefined` = no artifact). A\n * fixture-less component's single implicit variant is looked up under the\n * fixture name `'Default'`. Curated `manifest.previewImage` always wins over\n * this lookup; the lookup wins over the placeholder.\n */\nexport interface StudioPreviewsConfig {\n generated?: GeneratedPreviewLookup\n}\n\n/**\n * The canvas page-surface theme (EC-389). Both knobs target the preview iframe\n * document — the isolated page the builder renders into — so a site whose theme\n * styles `body` (background, color, font) previews exactly as it delivers.\n */\nexport interface StudioCanvasTheme {\n /**\n * REPLACES the studio's default canvas body classes\n * (`bg-background text-foreground font-sans`). The default's utility classes\n * out-cascade a site theme's `@layer base body` rules (Tailwind layers lose to\n * later layers/un-layered styles), which is exactly how a site's page background\n * used to paint white in the canvas — pass `''` to drop the default entirely and\n * let the site's own `body` rules (from the canvas stylesheet) take over, or\n * pass your own classes. Omitted → the studio default stays.\n */\n bodyClassName?: string\n /**\n * Raw CSS injected into the canvas iframe as the LAST style element — after the\n * canvas stylesheet and the studio editing chrome — so it wins the cascade over\n * both (an un-layered `body { background: … }` or a `:root { --token: … }`\n * override both work).\n */\n tokensCss?: string\n}\n\n/** The props a custom field renderer receives — identical to the built-in control. */\nexport interface CustomFieldRendererProps {\n field: FieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}\nexport type CustomFieldRenderer = (props: CustomFieldRendererProps) => ReactNode\n\n/**\n * Typed identity helper for `studio.config.tsx` — keeps the config declarative\n * (like {@link import('@elytracms/core/config').defineElytraConfig}). No\n * validation here; the studio resolves + validates the sidebar and field renderers\n * through the EC-236 seam at mount, surfacing every issue visibly (never throws).\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n * ```\n */\nexport function defineStudio(config: StudioConfig): StudioConfig {\n return config\n}\n"]}
|
|
1
|
+
{"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AAuKA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import type { ReactNode } from 'react'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport type { StudioI18nConfig } from '../../lib/i18n'\nimport type { StudioAiConfig } from '../../lib/ai'\nimport type { GeneratedPreviewLookup } from '../builder/blocks-model'\nimport type { StudioReuseConfig } from '../builder/reuse'\nimport type { SidebarNode } from './module'\nimport type { DocumentPane } from '../inspector/document-panes'\n\n/**\n * EC-236 — the studio customization config: the single, studio-only home for\n * everything a consuming repo customizes about the builder UI.\n *\n * This is the \"separate studio-only config solution\": it lives in the consumer's\n * `studio.config.tsx`, is `defineStudio({...})`-typed, and is passed to\n * `createStudio(config, studioConfig)` at the mount — NEVER mixed into the shared\n * `elytra.config.ts`, so the live React it carries (views, icons, field renderers,\n * future branding) never reaches the `frontend/` delivery bundle.\n *\n * The customization PRINCIPLE it embodies: customization is **code passed\n * directly**, not data referenced by a string key. Every field here holds live\n * values/components by reference — `sidebar` is built from `nav.*` view objects,\n * `fieldRenderers` maps a control name to a component. The only string indirection\n * in the whole surface is `fieldRenderers`' key, and ONLY because its other end\n * (`form.control` on a field) lives in the delivery-shared serializable schema and\n * cannot import studio code. This config grows by adding direct-value keys\n * (branding/logo/panels/theme), never by adding registries.\n */\nexport interface StudioConfig {\n /**\n * The section rail (EC-237) as an ordered list of `nav.*` nodes — views, groups,\n * and separators. Omitted → the studio derives the default rail from its built-in\n * views. A rail node carries its view object directly; there are no string ids to\n * keep in sync.\n */\n sidebar?: SidebarNode[]\n /**\n * Custom field renderers (EC-238) keyed by CONTROL name (`form.control`), each a\n * React component replacing the built-in document-editor control. A field opts in\n * per-field via `form: { control: '<name>' }` — so a renderer targets exactly the\n * fields that ask for it, not every field of a datatype. This is the one\n * boundary-crossing seam: the field names its control by string in the shared\n * schema, this map binds that name to a studio-only component.\n */\n fieldRenderers?: Record<string, CustomFieldRenderer>\n /**\n * Studio-UI localization (EC-241): the default locale + imported translation\n * catalogs. English is the built-in source; a consumer adds e.g.\n * `i18n: { locales: { de: deMessages } }` and the studio chrome renders in the\n * browser's preferred locale (no persisted selector in v1). Studio-only — never\n * reaches the delivery bundle. Schema labels localize separately, via a\n * `LocalizedString` on the field/collection def.\n */\n i18n?: StudioI18nConfig\n /**\n * AI platform (EC-331): provider, project knowledge, and soft limits. The block's\n * PRESENCE enables the AI feature; its ABSENCE means the feature is fully absent —\n * no AI UI and no AI dependencies on the critical path (the runtime lives behind a\n * lazy boundary in `@elytracms/studio/lib/ai`, only reached when configured).\n * `enabled: false` is a temporary kill-switch. Studio-only live code (holds the\n * provider adapter + knowledge functions) — never reaches the delivery bundle.\n * Validated at load by `resolveAiConfig` (capability gate: tool calling + structured\n * output required, fail-loud).\n */\n ai?: StudioAiConfig\n /**\n * Canvas page-surface theming (EC-389): make the preview iframe wear the SITE's\n * chrome. Direct values (no registries): body classes + raw CSS, injected LAST\n * into the canvas iframe — after the studio defaults — so consumer values win\n * the cascade. This themes the canvas\n * PAGE surface only, never the studio chrome (branding/logo theming is a future,\n * separate key).\n */\n canvas?: StudioCanvasTheme\n /**\n * Insert-menu preview artifacts (EC-439, the seam EC-440 fills): where the\n * studio's blocks panel finds GENERATED preview images for component\n * fixtures. Studio-only (from `defineStudio`, never the delivery bundle) and\n * fully optional — absent, tiles fall back from a curated\n * `manifest.previewImage` straight to the icon+name placeholder. The EC-440\n * CLI (`elytra previews`) renders fixtures headlessly against the consumer\n * theme and writes a previews manifest; the consumer adapts that manifest\n * into `previews.generated` here.\n */\n previews?: StudioPreviewsConfig\n /**\n * Reusable elements (EC-467, AD-b): WHICH component is this project's\n * reusable-element embed — `reuse: { component: 'project.Reusable' }`.\n *\n * **ABSENCE MEANS OFF (EC-728, owner ruling \"Declare, never infer\").** Like the\n * `ai?` precedent, omitting this block means the project has no reusable\n * elements in the studio: no *Make reusable* / *Detach to local copy* gestures,\n * and no canvas placeholder for an embed whose target is a draft or missing (the\n * embed then renders empty in the canvas). The studio never guesses an embed from\n * a component's shape — that guess crowned an ordinary link card and moved a\n * block's content off the page. Transclusion itself (an embed rendering its\n * target's blocks) does not depend on this declaration.\n *\n * **Reusable elements are singular per project (AD-a):** exactly one embed\n * component and exactly one reusable collection. This is one value, never a\n * list. The collection and the composition field DERIVE from the declared\n * component (its sole single-document relation prop → that prop's target\n * collection → that collection's `blocks` field), which is why the declaration\n * stays a single identifier.\n *\n * Studio-only, like every key here — what it drives is studio-side (the canvas\n * gestures and placeholder), so it has zero delivery-wire surface and never\n * reaches the `frontend/` bundle. A declaration that cannot resolve is a visible\n * validation issue in the header Issues popover, never a silent fallback.\n */\n reuse?: StudioReuseConfig\n /**\n * Panes contributed to the document editor's inspector (EC-618 — AD-19's edit-shell\n * slot), beside the frame's own Details / Versions / Issues: direct values, built with\n * `documentPane({ id, label, render })` from `@elytracms/studio/addon` or exported by an\n * addon (`analyticsDocumentPane`). CMS documents only; a pane's `collections` narrows it.\n * Studio-only, like every key here.\n */\n documentPanes?: DocumentPane[]\n}\n\n/**\n * The generated-preview seam (EC-439 AD-2). `generated` is a PURE lookup —\n * component id + fixture name → image URL (`undefined` = no artifact). A\n * fixture-less component's single implicit variant is looked up under the\n * fixture name `'Default'`. Curated `manifest.previewImage` always wins over\n * this lookup; the lookup wins over the placeholder.\n */\nexport interface StudioPreviewsConfig {\n generated?: GeneratedPreviewLookup\n}\n\n/**\n * The canvas page-surface theme (EC-389). Both knobs target the preview iframe\n * document — the isolated page the builder renders into — so a site whose theme\n * styles `body` (background, color, font) previews exactly as it delivers.\n */\nexport interface StudioCanvasTheme {\n /**\n * REPLACES the studio's default canvas body classes\n * (`bg-background text-foreground font-sans`). The default's utility classes\n * out-cascade a site theme's `@layer base body` rules (Tailwind layers lose to\n * later layers/un-layered styles), which is exactly how a site's page background\n * used to paint white in the canvas — pass `''` to drop the default entirely and\n * let the site's own `body` rules (from the canvas stylesheet) take over, or\n * pass your own classes. Omitted → the studio default stays.\n */\n bodyClassName?: string\n /**\n * Raw CSS injected into the canvas iframe as the LAST style element — after the\n * canvas stylesheet and the studio editing chrome — so it wins the cascade over\n * both (an un-layered `body { background: … }` or a `:root { --token: … }`\n * override both work).\n */\n tokensCss?: string\n}\n\n/** The props a custom field renderer receives — identical to the built-in control. */\nexport interface CustomFieldRendererProps {\n field: FieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n id: string\n}\nexport type CustomFieldRenderer = (props: CustomFieldRendererProps) => ReactNode\n\n/**\n * Typed identity helper for `studio.config.tsx` — keeps the config declarative\n * (like {@link import('@elytracms/core/config').defineElytraConfig}). No\n * validation here; the studio resolves + validates the sidebar and field renderers\n * through the EC-236 seam at mount, surfacing every issue visibly (never throws).\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n * ```\n */\nexport function defineStudio(config: StudioConfig): StudioConfig {\n return config\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { AllowList, ComponentLookup, ComponentNode, InsertTarget, LayoutContainerIds, PlacementScope, PlacementVerdict, PropValue } from '@elytracms/core/project-graph';
|
|
3
3
|
import type { IdFactory } from '@elytracms/core/persistence';
|
|
4
|
+
import type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core';
|
|
4
5
|
import type { PreservedItem } from './canvas-runs.js';
|
|
5
6
|
/**
|
|
6
7
|
* A canvas preview-width key (EC-250): `'full'` (unconstrained) or a configured
|
|
@@ -61,13 +62,15 @@ export type BuilderAction = {
|
|
|
61
62
|
* EC-282 reuse gesture: replace a node with zero or more nodes at its
|
|
62
63
|
* position (clone-with-fresh-ids, like `insert`). Promote → 1 embed node;
|
|
63
64
|
* detach → the referenced element's inline blocks. One atomic structural
|
|
64
|
-
* edit, one undo step.
|
|
65
|
+
* edit, one undo step. EC-728: `effect` — what the step did OUTSIDE the canvas
|
|
66
|
+
* (promote created a reusable document), recorded on that one history entry.
|
|
65
67
|
*/
|
|
66
68
|
| {
|
|
67
69
|
type: 'replace';
|
|
68
70
|
id: string;
|
|
69
71
|
nodes: ComponentNode[];
|
|
70
72
|
select?: boolean;
|
|
73
|
+
effect?: CanvasHistoryEffect;
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
73
76
|
* EC-162 layout gesture: wrap the reference node and the dragged subject in
|
|
@@ -144,13 +147,44 @@ export interface PlacementRefusal {
|
|
|
144
147
|
action: PlacingAction['type'];
|
|
145
148
|
verdict: PlacementVerdict;
|
|
146
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* EC-728 — what a canvas step did OUTSIDE the canvas, so Undo can reverse it and Redo repeat
|
|
152
|
+
* it: *Make reusable* created a reusable document (the canvas half of the gesture — the block
|
|
153
|
+
* swapped for its embed — is the entry's snapshot like any edit's). The store only CARRIES it:
|
|
154
|
+
* the reuse provider performs the document side when an Undo or Redo crosses the step
|
|
155
|
+
* ({@link BuilderState.historyEffect}).
|
|
156
|
+
*/
|
|
157
|
+
export interface CanvasHistoryEffect {
|
|
158
|
+
readonly kind: 'reusable-created';
|
|
159
|
+
readonly ref: DocumentRef;
|
|
160
|
+
/** The name the editor gave it — what a notice names. */
|
|
161
|
+
readonly name: string;
|
|
162
|
+
/** The document as created — what Redo re-inserts (the same id, insert-if-absent). */
|
|
163
|
+
readonly document: CmsDocument;
|
|
164
|
+
}
|
|
147
165
|
/**
|
|
148
166
|
* One history entry (EC-549): the component tree AND the preserved typography — so a
|
|
149
167
|
* run's deletion or edit is undone exactly like a block's.
|
|
168
|
+
*
|
|
169
|
+
* EC-728 — `effect` belongs to the STEP between this entry and its neighbour toward the
|
|
170
|
+
* present: on an undo entry (`past`), the step forward from it; on a redo entry (`future`),
|
|
171
|
+
* the step forward to it. Undo and Redo move it with the step, so it is reversed and
|
|
172
|
+
* repeated exactly once per crossing.
|
|
150
173
|
*/
|
|
151
174
|
export interface CanvasSnapshot {
|
|
152
175
|
root: ComponentNode;
|
|
153
176
|
preserved: readonly PreservedItem[];
|
|
177
|
+
effect?: CanvasHistoryEffect;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* EC-728 — the last Undo or Redo that crossed a step with an effect outside the canvas:
|
|
181
|
+
* which way, and the effect. `seq` increases with every such crossing, so a consumer handles
|
|
182
|
+
* each exactly once.
|
|
183
|
+
*/
|
|
184
|
+
export interface HistoryEffectStep {
|
|
185
|
+
readonly seq: number;
|
|
186
|
+
readonly direction: 'undo' | 'redo';
|
|
187
|
+
readonly effect: CanvasHistoryEffect;
|
|
154
188
|
}
|
|
155
189
|
export interface BuilderState {
|
|
156
190
|
/**
|
|
@@ -204,6 +238,8 @@ export interface BuilderState {
|
|
|
204
238
|
* dispatched from one after it (`createCanvasSync`).
|
|
205
239
|
*/
|
|
206
240
|
rebased: number;
|
|
241
|
+
/** EC-728 — see {@link HistoryEffectStep}; `null` until an Undo or Redo crosses an effectful step. */
|
|
242
|
+
historyEffect: HistoryEffectStep | null;
|
|
207
243
|
}
|
|
208
244
|
type Action = BuilderAction;
|
|
209
245
|
/**
|
|
@@ -22,7 +22,7 @@ function commit(state, root, opts = {}) {
|
|
|
22
22
|
const normalized = normalizeContainers(root, state.layout);
|
|
23
23
|
const present = normalized;
|
|
24
24
|
const preserved = opts.preserved ?? state.preserved;
|
|
25
|
-
const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey;
|
|
25
|
+
const coalesce = opts.effect === undefined && opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey;
|
|
26
26
|
// Normalization can dissolve the node a caller wanted selected (e.g. the
|
|
27
27
|
// parent container of a removed child) — selection never points at a ghost.
|
|
28
28
|
const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId;
|
|
@@ -30,7 +30,7 @@ function commit(state, root, opts = {}) {
|
|
|
30
30
|
const desiredRun = opts.selectedRun !== undefined ? opts.selectedRun : state.selectedRun;
|
|
31
31
|
return {
|
|
32
32
|
...state,
|
|
33
|
-
past: coalesce ? state.past : [...state.past, snapshotOf(state)].slice(-HISTORY_LIMIT),
|
|
33
|
+
past: coalesce ? state.past : [...state.past, snapshotOf(state, opts.effect)].slice(-HISTORY_LIMIT),
|
|
34
34
|
present,
|
|
35
35
|
preserved,
|
|
36
36
|
future: [],
|
|
@@ -40,8 +40,14 @@ function commit(state, root, opts = {}) {
|
|
|
40
40
|
refusal: null,
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
function snapshotOf(state) {
|
|
44
|
-
return { root: state.present, preserved: state.preserved };
|
|
43
|
+
function snapshotOf(state, effect) {
|
|
44
|
+
return { root: state.present, preserved: state.preserved, ...(effect ? { effect } : {}) };
|
|
45
|
+
}
|
|
46
|
+
/** EC-728 — the crossing to report when an Undo/Redo moves across a step carrying `effect`. */
|
|
47
|
+
function crossing(state, direction, effect) {
|
|
48
|
+
if (!effect)
|
|
49
|
+
return state.historyEffect;
|
|
50
|
+
return { seq: (state.historyEffect?.seq ?? 0) + 1, direction, effect };
|
|
45
51
|
}
|
|
46
52
|
/** The run key if that run still exists in this state, else `null` (never a ghost selection). */
|
|
47
53
|
function liveRun(root, preserved, run) {
|
|
@@ -130,13 +136,20 @@ function transition(state, action, placement) {
|
|
|
130
136
|
const prev = state.past.at(-1);
|
|
131
137
|
if (!prev)
|
|
132
138
|
return state;
|
|
133
|
-
|
|
139
|
+
// EC-728: the step's effect moves with it — onto the redo entry that repeats it.
|
|
140
|
+
return {
|
|
141
|
+
...restore(state, prev, state.past.slice(0, -1), [snapshotOf(state, prev.effect), ...state.future]),
|
|
142
|
+
historyEffect: crossing(state, 'undo', prev.effect),
|
|
143
|
+
};
|
|
134
144
|
}
|
|
135
145
|
case 'redo': {
|
|
136
146
|
const next = state.future[0];
|
|
137
147
|
if (!next)
|
|
138
148
|
return state;
|
|
139
|
-
return
|
|
149
|
+
return {
|
|
150
|
+
...restore(state, next, [...state.past, snapshotOf(state, next.effect)], state.future.slice(1)),
|
|
151
|
+
historyEffect: crossing(state, 'redo', next.effect),
|
|
152
|
+
};
|
|
140
153
|
}
|
|
141
154
|
case 'rebase': {
|
|
142
155
|
// EC-630: undo never reverts the external change — every snapshot absorbs it. The
|
|
@@ -216,6 +229,7 @@ function transition(state, action, placement) {
|
|
|
216
229
|
return state; // root or absent id — no-op
|
|
217
230
|
return commit(state, next, {
|
|
218
231
|
selectedId: action.select === false ? state.selectedId : (cloned[0]?.id ?? state.selectedId),
|
|
232
|
+
...(action.effect ? { effect: action.effect } : {}),
|
|
219
233
|
});
|
|
220
234
|
}
|
|
221
235
|
case 'wrap': {
|
|
@@ -298,6 +312,7 @@ preserved = []) {
|
|
|
298
312
|
layout,
|
|
299
313
|
refusal: null,
|
|
300
314
|
rebased: 0,
|
|
315
|
+
historyEffect: null,
|
|
301
316
|
};
|
|
302
317
|
}
|
|
303
318
|
/**
|