@elytracms/studio 0.0.59 → 0.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai/proposal-card.js +14 -23
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +34 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +65 -0
- package/dist/components/builder/canvas-chrome.js +125 -0
- package/dist/components/builder/canvas-chrome.js.map +1 -0
- package/dist/components/builder/canvas.js +13 -91
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +15 -0
- package/dist/components/cms/prop-fields.js +38 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +8 -0
- package/dist/components/cms/rich-text/editor.js +38 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +16 -1
- package/dist/components/cms/rich-text/toolbar.js +15 -4
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/project-session.d.ts +31 -1
- package/dist/components/project-session.js +56 -22
- package/dist/components/project-session.js.map +1 -1
- package/dist/customization.d.ts +1 -1
- package/dist/customization.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +5 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +8 -0
- package/dist/lib/workspace/index.js +10 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/ui/expandable-surface.d.ts +48 -0
- package/dist/ui/expandable-surface.js +72 -0
- package/dist/ui/expandable-surface.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/layer-stack.d.ts +36 -0
- package/dist/ui/layer-stack.js +42 -0
- package/dist/ui/layer-stack.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { createPortal } from 'react-dom';
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
4
3
|
import { ArrowUpRight, Check, Maximize2, Sparkles, X } from 'lucide-react';
|
|
5
4
|
import { diffDocuments } from '@elytracms/core/content';
|
|
6
5
|
import { resolveLabel } from '@elytracms/core/cms-core';
|
|
7
|
-
import { Badge, Button,
|
|
6
|
+
import { Badge, Button, ExpandableSurface, Row, Spacer, Stack, Text } from '@elytracms/studio/ui';
|
|
8
7
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
9
8
|
import { proposalDocuments } from '@elytracms/studio/lib/ai';
|
|
10
9
|
import { useCms } from '../cms/cms-store.js';
|
|
@@ -83,27 +82,19 @@ export function ProposalCardView({ proposal, collection, source, onApply, onDisc
|
|
|
83
82
|
* REVIEW surface, not a form, so it stays lightly dismissible (backdrop click /
|
|
84
83
|
* Escape / close button) rather than a trapping modal — the forms-never-modal
|
|
85
84
|
* spirit, applied. Presentation only: Apply/Discard call the same handlers the
|
|
86
|
-
* card does; it changes no proposal data.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
85
|
+
* card does; it changes no proposal data. Since EC-392 it renders through the
|
|
86
|
+
* shared {@link ExpandableSurface} primitive (portal-to-body, backdrop, header
|
|
87
|
+
* close, layer-stacked Escape) at the shipped 720px width, so it escapes the
|
|
88
|
+
* narrow AI panel; the card default keeps it closed, so the SSR smoke path
|
|
89
|
+
* never mounts the portal.
|
|
89
90
|
*/
|
|
90
91
|
function ProposalReviewSheet({ title, diff, collection, source, columnHeaders, status, onApply, onDiscard, onClose, }) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}, [onClose]);
|
|
99
|
-
if (typeof document === 'undefined')
|
|
100
|
-
return null;
|
|
101
|
-
return createPortal(_jsxs("div", { className: "fixed inset-0 z-50 flex", children: [_jsx("div", { className: "flex-1 bg-background/70 backdrop-blur-sm", "aria-hidden": true, onClick: onClose }), _jsxs("aside", { role: "dialog", "aria-label": title, className: "flex h-full w-[min(720px,92vw)] flex-col border-l border-border bg-card shadow-2xl", children: [_jsxs(Row, { gap: "2", align: "center", className: "h-12 shrink-0 border-b border-border px-4", children: [_jsx(Sparkles, { className: "size-4 shrink-0 text-primary" }), _jsx(Heading, { level: 2, size: "h4", className: "min-w-0 truncate", children: title }), _jsx(Spacer, {}), _jsx(Button, { size: "icon-sm", variant: "ghost", onClick: onClose, title: translate('ai.proposal.collapseReview'), "aria-label": translate('ai.proposal.collapseReview'), children: _jsx(X, {}) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-3xl", children: _jsx(CollectionViewProvider, { source: source, children: _jsx(DocumentDiffView, { diff: diff, collection: collection, columnHeaders: columnHeaders }) }) }) }), status === 'pending' ? (_jsxs(Row, { gap: "2", align: "center", className: "shrink-0 border-t border-border px-4 py-3", 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: () => {
|
|
102
|
-
onDiscard();
|
|
103
|
-
onClose();
|
|
104
|
-
}, children: [_jsx(X, {}), " ", translate('ai.proposal.discard')] }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
105
|
-
onApply();
|
|
106
|
-
onClose();
|
|
107
|
-
}, children: [_jsx(Check, {}), " ", translate('ai.proposal.apply')] })] })) : null] })] }), document.body);
|
|
92
|
+
return (_jsxs(ExpandableSurface, { title: title, closeLabel: translate('ai.proposal.collapseReview'), width: "min(720px, 92vw)", onDismiss: onClose, headerLeading: _jsx(Sparkles, { className: "size-4 shrink-0 text-primary" }), children: [_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto w-full max-w-3xl", children: _jsx(CollectionViewProvider, { source: source, children: _jsx(DocumentDiffView, { diff: diff, collection: collection, columnHeaders: columnHeaders }) }) }) }), status === 'pending' ? (_jsxs(Row, { gap: "2", align: "center", className: "shrink-0 border-t border-border px-4 py-3", 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: () => {
|
|
93
|
+
onDiscard();
|
|
94
|
+
onClose();
|
|
95
|
+
}, children: [_jsx(X, {}), " ", translate('ai.proposal.discard')] }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
96
|
+
onApply();
|
|
97
|
+
onClose();
|
|
98
|
+
}, children: [_jsx(Check, {}), " ", translate('ai.proposal.apply')] })] })) : null] }));
|
|
108
99
|
}
|
|
109
100
|
//# sourceMappingURL=proposal-card.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposal-card.js","sourceRoot":"","sources":["../../../src/components/ai/proposal-card.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useState } from 'react'\nimport { createPortal } from 'react-dom'\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 { CollectionDef } from '@elytracms/core/cms-core'\nimport { Badge, Button, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { proposalDocuments } from '@elytracms/studio/lib/ai'\nimport type { ProposalOrigin, ProposalReview } from '@elytracms/studio/lib/ai'\nimport type { CollectionViewSource } from '@elytracms/studio/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'\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\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 <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 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. Rendered through a portal so it\n * escapes the narrow AI panel; the card default keeps it closed, so the SSR\n * smoke path never mounts the portal.\n */\nfunction ProposalReviewSheet({\n title,\n diff,\n collection,\n source,\n columnHeaders,\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 status: ProposalReview['status']\n onApply: () => void\n onDiscard: () => void\n onClose: () => void\n}) {\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose()\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [onClose])\n\n if (typeof document === 'undefined') return null\n\n return createPortal(\n <div className=\"fixed inset-0 z-50 flex\">\n <div className=\"flex-1 bg-background/70 backdrop-blur-sm\" aria-hidden onClick={onClose} />\n <aside\n role=\"dialog\"\n aria-label={title}\n className=\"flex h-full w-[min(720px,92vw)] flex-col border-l border-border bg-card shadow-2xl\"\n >\n <Row gap=\"2\" align=\"center\" className=\"h-12 shrink-0 border-b border-border px-4\">\n <Sparkles className=\"size-4 shrink-0 text-primary\" />\n <Heading level={2} size=\"h4\" className=\"min-w-0 truncate\">\n {title}\n </Heading>\n <Spacer />\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={onClose}\n title={translate('ai.proposal.collapseReview')}\n aria-label={translate('ai.proposal.collapseReview')}\n >\n <X />\n </Button>\n </Row>\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 </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 </aside>\n </div>,\n document.body,\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 { CollectionDef } from '@elytracms/core/cms-core'\nimport { Badge, Button, ExpandableSurface, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { proposalDocuments } from '@elytracms/studio/lib/ai'\nimport type { ProposalOrigin, ProposalReview } from '@elytracms/studio/lib/ai'\nimport type { CollectionViewSource } from '@elytracms/studio/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'\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\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 <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 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 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 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 </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"]}
|
|
@@ -59,6 +59,40 @@ export interface StudioConfig {
|
|
|
59
59
|
* output required, fail-loud).
|
|
60
60
|
*/
|
|
61
61
|
ai?: StudioAiConfig;
|
|
62
|
+
/**
|
|
63
|
+
* Canvas page-surface theming (EC-389): make the preview iframe wear the SITE's
|
|
64
|
+
* chrome. Direct values (no registries): body classes + raw CSS, injected LAST
|
|
65
|
+
* into the canvas iframe — after the studio defaults and an instantiated starter
|
|
66
|
+
* kit's `tokensCss` — so consumer values win the cascade. This themes the canvas
|
|
67
|
+
* PAGE surface only, never the studio chrome (branding/logo theming is a future,
|
|
68
|
+
* separate key).
|
|
69
|
+
*/
|
|
70
|
+
canvas?: StudioCanvasTheme;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The canvas page-surface theme (EC-389). Both knobs target the preview iframe
|
|
74
|
+
* document — the isolated page the builder renders into — so a site whose theme
|
|
75
|
+
* styles `body` (background, color, font) previews exactly as it delivers.
|
|
76
|
+
*/
|
|
77
|
+
export interface StudioCanvasTheme {
|
|
78
|
+
/**
|
|
79
|
+
* REPLACES the studio's default canvas body classes
|
|
80
|
+
* (`bg-background text-foreground font-sans`). The default's utility classes
|
|
81
|
+
* out-cascade a site theme's `@layer base body` rules (Tailwind layers lose to
|
|
82
|
+
* later layers/un-layered styles), which is exactly how a site's page background
|
|
83
|
+
* used to paint white in the canvas — pass `''` to drop the default entirely and
|
|
84
|
+
* let the site's own `body` rules (from the canvas stylesheet) take over, or
|
|
85
|
+
* pass your own classes. Omitted → the studio default stays.
|
|
86
|
+
*/
|
|
87
|
+
bodyClassName?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Raw CSS injected into the canvas iframe as the LAST style element — after the
|
|
90
|
+
* canvas stylesheet, the studio editing chrome, and a kit's `tokensCss` — so it
|
|
91
|
+
* wins the cascade over all of them (an un-layered `body { background: … }` or a
|
|
92
|
+
* `:root { --token: … }` override both work). The kit seam keeps working: a kit
|
|
93
|
+
* provides, this overrides.
|
|
94
|
+
*/
|
|
95
|
+
tokensCss?: string;
|
|
62
96
|
}
|
|
63
97
|
/** The props a custom field renderer receives — identical to the built-in control. */
|
|
64
98
|
export interface CustomFieldRendererProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AA6GA;;;;;;;;;;;;;;;;;;;;;;;;;;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 '@elytracms/studio/lib/i18n'\nimport type { StudioAiConfig } from '@elytracms/studio/lib/ai'\nimport type { SidebarNode } from './module'\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 and an instantiated starter\n * kit's `tokensCss` — so consumer values win 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\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, the studio editing chrome, and a kit's `tokensCss` — so it\n * wins the cascade over all of them (an un-layered `body { background: … }` or a\n * `:root { --token: … }` override both work). The kit seam keeps working: a kit\n * provides, this overrides.\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/studio-core').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"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { StudioCanvasTheme } from '../app-views/studio-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The canvas iframe's document chrome (EC-389) — the framework-free half of
|
|
4
|
+
* `BuilderCanvas`'s `wireUp` (house pattern, EC-330): everything injected into
|
|
5
|
+
* the preview document lives here, in ONE pinned cascade order, unit-testable
|
|
6
|
+
* without jsdom.
|
|
7
|
+
*/
|
|
8
|
+
/** Selection/hover outlines, scoped to the per-node editable wrappers. */
|
|
9
|
+
export declare const OUTLINE_CSS = "\n /* The canvas backdrop is deliberately always light/site-like (see the\n wireUp note in canvas.tsx) \u2014 force the native form-control/scrollbar theme\n to match, so a page with real inputs never shows dark native chrome\n floating on a light page. */\n html { color-scheme: light; }\n body { margin: 0; padding: 16px; }\n .ec-editable { outline-offset: -1px; cursor: default; }\n .ec-editable[data-hovered]:not([data-selected]) { outline: 1px dashed rgba(130,150,190,0.75); }\n .ec-editable[data-selected] { outline: 2px solid #5b8cff; }\n /* Inline editing (EC-158 text, EC-159 rich text): the rendered element\n itself becomes the editor \u2014 no overlay, no replacement input \u2014 so\n typography, alignment, and layout are pixel-identical between edit and\n render states. The wrapper keeps the selection outline; the host only\n gets a caret. pre-wrap applies to PLAIN text hosts only \u2014 rich-text\n markup must keep its rendered whitespace handling (no layout shift). */\n .ec-editable[data-ec-editing] { outline: 2px solid #5b8cff; }\n .ec-editable [data-ec-edit] { outline: none; cursor: text; }\n .ec-editable [data-ec-edit='text'] { white-space: pre-wrap; }\n /* Preserved atoms during a rich-text session (custom blocks, unknown nodes,\n images, embeds): visible and rendered, but not text-editable. */\n .ec-editable [data-ec-edit='rich'] [data-ec-rt-preserve] { user-select: none; }\n /* Floating mark toolbar (EC-159), portaled into the iframe. */\n .ec-rt-toolbar { color-scheme: dark; }\n .ec-rt-toolbar-row { display: flex; align-items: center; gap: 2px; padding: 3px;\n border: 1px solid rgba(130,150,190,0.4); border-radius: 8px;\n background: rgb(24,26,32); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }\n .ec-rt-toolbar-btn { min-width: 26px; height: 24px; padding: 0 7px; border: 0; border-radius: 5px;\n background: transparent; color: rgb(225,228,236); font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;\n cursor: pointer; }\n .ec-rt-toolbar-btn:hover { background: rgba(130,150,190,0.25); }\n .ec-rt-toolbar-btn[data-active] { background: #5b8cff; color: white; }\n .ec-rt-toolbar-input { width: 200px; height: 24px; padding: 0 7px; border-radius: 5px;\n border: 1px solid rgba(130,150,190,0.4); background: rgb(16,18,22); color: rgb(225,228,236);\n font: 12px/1 ui-sans-serif, system-ui, sans-serif; outline: none; }\n /* Locale-fallback chips (EC-176): a node whose bound content is currently\n served via locale fallback wears a small corner chip naming the locale it\n actually came from \u2014 the canvas never silently mixes locales. */\n .ec-editable[data-ec-locale-fallback] { position: relative; }\n .ec-editable[data-ec-locale-fallback]::after {\n content: attr(data-ec-locale-fallback);\n position: absolute; top: 2px; right: 2px; z-index: 5; pointer-events: none;\n padding: 0 4px; border-radius: 4px;\n font: 600 9px/1.5 ui-sans-serif, system-ui, sans-serif;\n letter-spacing: 0.05em; text-transform: uppercase;\n color: #fbbf24; background: rgba(180,83,9,0.25); border: 1px solid rgba(245,158,11,0.45);\n }\n /* Drag & drop states (EC-160/EC-162): eligibility comes from the shared pure\n rules (lib/dnd/drop-rules); these classes only *show* the resolution.\n Rejection is explicit and visible during the drag \u2014 never a silent failed\n drop. Before/after bars follow the parent's flow axis (data-ec-drop-axis);\n left/right are the wrap-into-columns gesture (violet, paired highlight). */\n .ec-editable[data-ec-drop='into'] { outline: 2px solid #34d399; outline-offset: -2px; background-color: rgba(52, 211, 153, 0.08); }\n .ec-editable[data-ec-drop='before'] { box-shadow: 0 -3px 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='after'] { box-shadow: 0 3px 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='before'][data-ec-drop-axis='x'] { box-shadow: -3px 0 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='after'][data-ec-drop-axis='x'] { box-shadow: 3px 0 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='left'] { box-shadow: -3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }\n .ec-editable[data-ec-drop='right'] { box-shadow: 3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }\n .ec-editable[data-ec-drop='blocked'] { outline: 2px solid #ef4444; outline-offset: -2px; background-color: rgba(239, 68, 68, 0.08); cursor: not-allowed; }\n";
|
|
10
|
+
/**
|
|
11
|
+
* The studio's DEFAULT canvas body classes — a replaceable default (EC-389
|
|
12
|
+
* decision): a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES
|
|
13
|
+
* it wholesale (including `''` to drop it), because these utility classes
|
|
14
|
+
* out-cascade a site theme's `@layer base body` rules — keeping them additive
|
|
15
|
+
* would make the site's own page background unreachable (the NK-004 bug).
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_CANVAS_BODY_CLASS = "bg-background text-foreground font-sans";
|
|
18
|
+
/** Everything injected into the canvas preview document. */
|
|
19
|
+
export interface CanvasChromeInputs {
|
|
20
|
+
/** The consuming app's compiled canvas stylesheet URL; `null` → no link (tests/SSR). */
|
|
21
|
+
stylesheetUrl: string | null;
|
|
22
|
+
/** An instantiated starter kit's brand token palette (EC-117), when the project has one. */
|
|
23
|
+
kitTokensCss?: string;
|
|
24
|
+
/** The consumer's canvas theme from `defineStudio({ canvas })` (EC-389). */
|
|
25
|
+
theme?: StudioCanvasTheme;
|
|
26
|
+
}
|
|
27
|
+
/** The head element surface the chrome creates (link/style). */
|
|
28
|
+
export interface CanvasChromeElement {
|
|
29
|
+
id: string;
|
|
30
|
+
rel?: string;
|
|
31
|
+
href?: string;
|
|
32
|
+
textContent: string | null;
|
|
33
|
+
remove(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The document surface `applyCanvasChrome` needs (the `canvas-dnd` house
|
|
37
|
+
* pattern: a narrow structural type so the core is unit-testable with a
|
|
38
|
+
* hand-rolled fake — no jsdom). A real iframe `Document` satisfies it.
|
|
39
|
+
*/
|
|
40
|
+
export interface CanvasChromeDocument {
|
|
41
|
+
getElementById(id: string): CanvasChromeElement | null;
|
|
42
|
+
createElement(tag: string): CanvasChromeElement;
|
|
43
|
+
head: {
|
|
44
|
+
appendChild(node: object): void;
|
|
45
|
+
};
|
|
46
|
+
body: {
|
|
47
|
+
className: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Apply the canvas document chrome in the PINNED cascade order (EC-389):
|
|
52
|
+
*
|
|
53
|
+
* 1. `ec-app-css` — the canvas stylesheet `<link>` (site delivery CSS + studio CSS);
|
|
54
|
+
* 2. `ec-outline-css` — the studio's editing chrome (selection/DnD/toolbar styles);
|
|
55
|
+
* 3. `ec-kit-tokens` — an instantiated starter kit's `tokensCss` (kit provides…);
|
|
56
|
+
* 4. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (…consumer overrides).
|
|
57
|
+
*
|
|
58
|
+
* Later elements win the cascade at equal specificity, so studio defaults <
|
|
59
|
+
* kit tokens < consumer config — the customization-seam ordering. The body
|
|
60
|
+
* class is the consumer's `bodyClassName` when given (even `''`), else the
|
|
61
|
+
* studio default. Safe to run repeatedly (iframe onLoad + the already-loaded
|
|
62
|
+
* fast path): links/styles are deduped by id, and the two token styles are
|
|
63
|
+
* re-appended IN ORDER so a re-wire never flips who wins.
|
|
64
|
+
*/
|
|
65
|
+
export declare function applyCanvasChrome(d: CanvasChromeDocument, inputs: CanvasChromeInputs): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { PRESERVE_NODE_ATTR } from '@elytracms/core/rich-text';
|
|
2
|
+
/**
|
|
3
|
+
* The canvas iframe's document chrome (EC-389) — the framework-free half of
|
|
4
|
+
* `BuilderCanvas`'s `wireUp` (house pattern, EC-330): everything injected into
|
|
5
|
+
* the preview document lives here, in ONE pinned cascade order, unit-testable
|
|
6
|
+
* without jsdom.
|
|
7
|
+
*/
|
|
8
|
+
/** Selection/hover outlines, scoped to the per-node editable wrappers. */
|
|
9
|
+
export const OUTLINE_CSS = `
|
|
10
|
+
/* The canvas backdrop is deliberately always light/site-like (see the
|
|
11
|
+
wireUp note in canvas.tsx) — force the native form-control/scrollbar theme
|
|
12
|
+
to match, so a page with real inputs never shows dark native chrome
|
|
13
|
+
floating on a light page. */
|
|
14
|
+
html { color-scheme: light; }
|
|
15
|
+
body { margin: 0; padding: 16px; }
|
|
16
|
+
.ec-editable { outline-offset: -1px; cursor: default; }
|
|
17
|
+
.ec-editable[data-hovered]:not([data-selected]) { outline: 1px dashed rgba(130,150,190,0.75); }
|
|
18
|
+
.ec-editable[data-selected] { outline: 2px solid #5b8cff; }
|
|
19
|
+
/* Inline editing (EC-158 text, EC-159 rich text): the rendered element
|
|
20
|
+
itself becomes the editor — no overlay, no replacement input — so
|
|
21
|
+
typography, alignment, and layout are pixel-identical between edit and
|
|
22
|
+
render states. The wrapper keeps the selection outline; the host only
|
|
23
|
+
gets a caret. pre-wrap applies to PLAIN text hosts only — rich-text
|
|
24
|
+
markup must keep its rendered whitespace handling (no layout shift). */
|
|
25
|
+
.ec-editable[data-ec-editing] { outline: 2px solid #5b8cff; }
|
|
26
|
+
.ec-editable [data-ec-edit] { outline: none; cursor: text; }
|
|
27
|
+
.ec-editable [data-ec-edit='text'] { white-space: pre-wrap; }
|
|
28
|
+
/* Preserved atoms during a rich-text session (custom blocks, unknown nodes,
|
|
29
|
+
images, embeds): visible and rendered, but not text-editable. */
|
|
30
|
+
.ec-editable [data-ec-edit='rich'] [${PRESERVE_NODE_ATTR}] { user-select: none; }
|
|
31
|
+
/* Floating mark toolbar (EC-159), portaled into the iframe. */
|
|
32
|
+
.ec-rt-toolbar { color-scheme: dark; }
|
|
33
|
+
.ec-rt-toolbar-row { display: flex; align-items: center; gap: 2px; padding: 3px;
|
|
34
|
+
border: 1px solid rgba(130,150,190,0.4); border-radius: 8px;
|
|
35
|
+
background: rgb(24,26,32); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }
|
|
36
|
+
.ec-rt-toolbar-btn { min-width: 26px; height: 24px; padding: 0 7px; border: 0; border-radius: 5px;
|
|
37
|
+
background: transparent; color: rgb(225,228,236); font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;
|
|
38
|
+
cursor: pointer; }
|
|
39
|
+
.ec-rt-toolbar-btn:hover { background: rgba(130,150,190,0.25); }
|
|
40
|
+
.ec-rt-toolbar-btn[data-active] { background: #5b8cff; color: white; }
|
|
41
|
+
.ec-rt-toolbar-input { width: 200px; height: 24px; padding: 0 7px; border-radius: 5px;
|
|
42
|
+
border: 1px solid rgba(130,150,190,0.4); background: rgb(16,18,22); color: rgb(225,228,236);
|
|
43
|
+
font: 12px/1 ui-sans-serif, system-ui, sans-serif; outline: none; }
|
|
44
|
+
/* Locale-fallback chips (EC-176): a node whose bound content is currently
|
|
45
|
+
served via locale fallback wears a small corner chip naming the locale it
|
|
46
|
+
actually came from — the canvas never silently mixes locales. */
|
|
47
|
+
.ec-editable[data-ec-locale-fallback] { position: relative; }
|
|
48
|
+
.ec-editable[data-ec-locale-fallback]::after {
|
|
49
|
+
content: attr(data-ec-locale-fallback);
|
|
50
|
+
position: absolute; top: 2px; right: 2px; z-index: 5; pointer-events: none;
|
|
51
|
+
padding: 0 4px; border-radius: 4px;
|
|
52
|
+
font: 600 9px/1.5 ui-sans-serif, system-ui, sans-serif;
|
|
53
|
+
letter-spacing: 0.05em; text-transform: uppercase;
|
|
54
|
+
color: #fbbf24; background: rgba(180,83,9,0.25); border: 1px solid rgba(245,158,11,0.45);
|
|
55
|
+
}
|
|
56
|
+
/* Drag & drop states (EC-160/EC-162): eligibility comes from the shared pure
|
|
57
|
+
rules (lib/dnd/drop-rules); these classes only *show* the resolution.
|
|
58
|
+
Rejection is explicit and visible during the drag — never a silent failed
|
|
59
|
+
drop. Before/after bars follow the parent's flow axis (data-ec-drop-axis);
|
|
60
|
+
left/right are the wrap-into-columns gesture (violet, paired highlight). */
|
|
61
|
+
.ec-editable[data-ec-drop='into'] { outline: 2px solid #34d399; outline-offset: -2px; background-color: rgba(52, 211, 153, 0.08); }
|
|
62
|
+
.ec-editable[data-ec-drop='before'] { box-shadow: 0 -3px 0 0 #38bdf8; }
|
|
63
|
+
.ec-editable[data-ec-drop='after'] { box-shadow: 0 3px 0 0 #38bdf8; }
|
|
64
|
+
.ec-editable[data-ec-drop='before'][data-ec-drop-axis='x'] { box-shadow: -3px 0 0 0 #38bdf8; }
|
|
65
|
+
.ec-editable[data-ec-drop='after'][data-ec-drop-axis='x'] { box-shadow: 3px 0 0 0 #38bdf8; }
|
|
66
|
+
.ec-editable[data-ec-drop='left'] { box-shadow: -3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }
|
|
67
|
+
.ec-editable[data-ec-drop='right'] { box-shadow: 3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }
|
|
68
|
+
.ec-editable[data-ec-drop='blocked'] { outline: 2px solid #ef4444; outline-offset: -2px; background-color: rgba(239, 68, 68, 0.08); cursor: not-allowed; }
|
|
69
|
+
`;
|
|
70
|
+
/**
|
|
71
|
+
* The studio's DEFAULT canvas body classes — a replaceable default (EC-389
|
|
72
|
+
* decision): a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES
|
|
73
|
+
* it wholesale (including `''` to drop it), because these utility classes
|
|
74
|
+
* out-cascade a site theme's `@layer base body` rules — keeping them additive
|
|
75
|
+
* would make the site's own page background unreachable (the NK-004 bug).
|
|
76
|
+
*/
|
|
77
|
+
export const DEFAULT_CANVAS_BODY_CLASS = 'bg-background text-foreground font-sans';
|
|
78
|
+
/**
|
|
79
|
+
* Apply the canvas document chrome in the PINNED cascade order (EC-389):
|
|
80
|
+
*
|
|
81
|
+
* 1. `ec-app-css` — the canvas stylesheet `<link>` (site delivery CSS + studio CSS);
|
|
82
|
+
* 2. `ec-outline-css` — the studio's editing chrome (selection/DnD/toolbar styles);
|
|
83
|
+
* 3. `ec-kit-tokens` — an instantiated starter kit's `tokensCss` (kit provides…);
|
|
84
|
+
* 4. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (…consumer overrides).
|
|
85
|
+
*
|
|
86
|
+
* Later elements win the cascade at equal specificity, so studio defaults <
|
|
87
|
+
* kit tokens < consumer config — the customization-seam ordering. The body
|
|
88
|
+
* class is the consumer's `bodyClassName` when given (even `''`), else the
|
|
89
|
+
* studio default. Safe to run repeatedly (iframe onLoad + the already-loaded
|
|
90
|
+
* fast path): links/styles are deduped by id, and the two token styles are
|
|
91
|
+
* re-appended IN ORDER so a re-wire never flips who wins.
|
|
92
|
+
*/
|
|
93
|
+
export function applyCanvasChrome(d, inputs) {
|
|
94
|
+
if (inputs.stylesheetUrl && !d.getElementById('ec-app-css')) {
|
|
95
|
+
const link = d.createElement('link');
|
|
96
|
+
link.id = 'ec-app-css';
|
|
97
|
+
link.rel = 'stylesheet';
|
|
98
|
+
link.href = inputs.stylesheetUrl;
|
|
99
|
+
d.head.appendChild(link);
|
|
100
|
+
}
|
|
101
|
+
if (!d.getElementById('ec-outline-css')) {
|
|
102
|
+
const style = d.createElement('style');
|
|
103
|
+
style.id = 'ec-outline-css';
|
|
104
|
+
style.textContent = OUTLINE_CSS;
|
|
105
|
+
d.head.appendChild(style);
|
|
106
|
+
}
|
|
107
|
+
// Both token layers re-append (remove + add) on every wire, in sequence, so
|
|
108
|
+
// the consumer element always lands after the kit element.
|
|
109
|
+
appendCssLayer(d, 'ec-kit-tokens', inputs.kitTokensCss);
|
|
110
|
+
appendCssLayer(d, 'ec-canvas-theme', inputs.theme?.tokensCss);
|
|
111
|
+
d.body.className = inputs.theme?.bodyClassName ?? DEFAULT_CANVAS_BODY_CLASS;
|
|
112
|
+
}
|
|
113
|
+
/** Replace-append one identified `<style>` layer; absent CSS removes a stale element. */
|
|
114
|
+
function appendCssLayer(d, id, css) {
|
|
115
|
+
const existing = d.getElementById(id);
|
|
116
|
+
if (existing)
|
|
117
|
+
existing.remove();
|
|
118
|
+
if (css === undefined || css === '')
|
|
119
|
+
return;
|
|
120
|
+
const style = d.createElement('style');
|
|
121
|
+
style.id = id;
|
|
122
|
+
style.textContent = css;
|
|
123
|
+
d.head.appendChild(style);
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=canvas-chrome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-chrome.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-chrome.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAG9D;;;;;GAKG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;wCAqBa,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCzD,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,yCAAyC,CAAA;AAmClF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAuB,EAAE,MAA0B;IACnF,IAAI,MAAM,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,GAAG,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,CAAC,EAAE,GAAG,YAAY,CAAA;QACtB,IAAI,CAAC,GAAG,GAAG,YAAY,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,aAAa,CAAA;QAChC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IACD,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;QACtC,KAAK,CAAC,EAAE,GAAG,gBAAgB,CAAA;QAC3B,KAAK,CAAC,WAAW,GAAG,WAAW,CAAA;QAC/B,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;IAC3B,CAAC;IACD,4EAA4E;IAC5E,2DAA2D;IAC3D,cAAc,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;IACvD,cAAc,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAC7D,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,KAAK,EAAE,aAAa,IAAI,yBAAyB,CAAA;AAC7E,CAAC;AAED,yFAAyF;AACzF,SAAS,cAAc,CAAC,CAAuB,EAAE,EAAU,EAAE,GAAuB;IAClF,MAAM,QAAQ,GAAG,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC,CAAA;IACrC,IAAI,QAAQ;QAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;IAC/B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAM;IAC3C,MAAM,KAAK,GAAG,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;IACtC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAA;IACb,KAAK,CAAC,WAAW,GAAG,GAAG,CAAA;IACvB,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC","sourcesContent":["import { PRESERVE_NODE_ATTR } from '@elytracms/core/rich-text'\nimport type { StudioCanvasTheme } from '../app-views/studio-config'\n\n/**\n * The canvas iframe's document chrome (EC-389) — the framework-free half of\n * `BuilderCanvas`'s `wireUp` (house pattern, EC-330): everything injected into\n * the preview document lives here, in ONE pinned cascade order, unit-testable\n * without jsdom.\n */\n\n/** Selection/hover outlines, scoped to the per-node editable wrappers. */\nexport const OUTLINE_CSS = `\n /* The canvas backdrop is deliberately always light/site-like (see the\n wireUp note in canvas.tsx) — force the native form-control/scrollbar theme\n to match, so a page with real inputs never shows dark native chrome\n floating on a light page. */\n html { color-scheme: light; }\n body { margin: 0; padding: 16px; }\n .ec-editable { outline-offset: -1px; cursor: default; }\n .ec-editable[data-hovered]:not([data-selected]) { outline: 1px dashed rgba(130,150,190,0.75); }\n .ec-editable[data-selected] { outline: 2px solid #5b8cff; }\n /* Inline editing (EC-158 text, EC-159 rich text): the rendered element\n itself becomes the editor — no overlay, no replacement input — so\n typography, alignment, and layout are pixel-identical between edit and\n render states. The wrapper keeps the selection outline; the host only\n gets a caret. pre-wrap applies to PLAIN text hosts only — rich-text\n markup must keep its rendered whitespace handling (no layout shift). */\n .ec-editable[data-ec-editing] { outline: 2px solid #5b8cff; }\n .ec-editable [data-ec-edit] { outline: none; cursor: text; }\n .ec-editable [data-ec-edit='text'] { white-space: pre-wrap; }\n /* Preserved atoms during a rich-text session (custom blocks, unknown nodes,\n images, embeds): visible and rendered, but not text-editable. */\n .ec-editable [data-ec-edit='rich'] [${PRESERVE_NODE_ATTR}] { user-select: none; }\n /* Floating mark toolbar (EC-159), portaled into the iframe. */\n .ec-rt-toolbar { color-scheme: dark; }\n .ec-rt-toolbar-row { display: flex; align-items: center; gap: 2px; padding: 3px;\n border: 1px solid rgba(130,150,190,0.4); border-radius: 8px;\n background: rgb(24,26,32); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }\n .ec-rt-toolbar-btn { min-width: 26px; height: 24px; padding: 0 7px; border: 0; border-radius: 5px;\n background: transparent; color: rgb(225,228,236); font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;\n cursor: pointer; }\n .ec-rt-toolbar-btn:hover { background: rgba(130,150,190,0.25); }\n .ec-rt-toolbar-btn[data-active] { background: #5b8cff; color: white; }\n .ec-rt-toolbar-input { width: 200px; height: 24px; padding: 0 7px; border-radius: 5px;\n border: 1px solid rgba(130,150,190,0.4); background: rgb(16,18,22); color: rgb(225,228,236);\n font: 12px/1 ui-sans-serif, system-ui, sans-serif; outline: none; }\n /* Locale-fallback chips (EC-176): a node whose bound content is currently\n served via locale fallback wears a small corner chip naming the locale it\n actually came from — the canvas never silently mixes locales. */\n .ec-editable[data-ec-locale-fallback] { position: relative; }\n .ec-editable[data-ec-locale-fallback]::after {\n content: attr(data-ec-locale-fallback);\n position: absolute; top: 2px; right: 2px; z-index: 5; pointer-events: none;\n padding: 0 4px; border-radius: 4px;\n font: 600 9px/1.5 ui-sans-serif, system-ui, sans-serif;\n letter-spacing: 0.05em; text-transform: uppercase;\n color: #fbbf24; background: rgba(180,83,9,0.25); border: 1px solid rgba(245,158,11,0.45);\n }\n /* Drag & drop states (EC-160/EC-162): eligibility comes from the shared pure\n rules (lib/dnd/drop-rules); these classes only *show* the resolution.\n Rejection is explicit and visible during the drag — never a silent failed\n drop. Before/after bars follow the parent's flow axis (data-ec-drop-axis);\n left/right are the wrap-into-columns gesture (violet, paired highlight). */\n .ec-editable[data-ec-drop='into'] { outline: 2px solid #34d399; outline-offset: -2px; background-color: rgba(52, 211, 153, 0.08); }\n .ec-editable[data-ec-drop='before'] { box-shadow: 0 -3px 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='after'] { box-shadow: 0 3px 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='before'][data-ec-drop-axis='x'] { box-shadow: -3px 0 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='after'][data-ec-drop-axis='x'] { box-shadow: 3px 0 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='left'] { box-shadow: -3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }\n .ec-editable[data-ec-drop='right'] { box-shadow: 3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }\n .ec-editable[data-ec-drop='blocked'] { outline: 2px solid #ef4444; outline-offset: -2px; background-color: rgba(239, 68, 68, 0.08); cursor: not-allowed; }\n`\n\n/**\n * The studio's DEFAULT canvas body classes — a replaceable default (EC-389\n * decision): a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES\n * it wholesale (including `''` to drop it), because these utility classes\n * out-cascade a site theme's `@layer base body` rules — keeping them additive\n * would make the site's own page background unreachable (the NK-004 bug).\n */\nexport const DEFAULT_CANVAS_BODY_CLASS = 'bg-background text-foreground font-sans'\n\n/** Everything injected into the canvas preview document. */\nexport interface CanvasChromeInputs {\n /** The consuming app's compiled canvas stylesheet URL; `null` → no link (tests/SSR). */\n stylesheetUrl: string | null\n /** An instantiated starter kit's brand token palette (EC-117), when the project has one. */\n kitTokensCss?: string\n /** The consumer's canvas theme from `defineStudio({ canvas })` (EC-389). */\n theme?: StudioCanvasTheme\n}\n\n/** The head element surface the chrome creates (link/style). */\nexport interface CanvasChromeElement {\n id: string\n rel?: string\n href?: string\n textContent: string | null\n remove(): void\n}\n\n/**\n * The document surface `applyCanvasChrome` needs (the `canvas-dnd` house\n * pattern: a narrow structural type so the core is unit-testable with a\n * hand-rolled fake — no jsdom). A real iframe `Document` satisfies it.\n */\nexport interface CanvasChromeDocument {\n getElementById(id: string): CanvasChromeElement | null\n createElement(tag: string): CanvasChromeElement\n // `object` (not CanvasChromeElement): the real DOM `appendChild<T extends Node>`\n // is only bivariance-compatible with a parameter every Node satisfies.\n head: { appendChild(node: object): void }\n body: { className: string }\n}\n\n/**\n * Apply the canvas document chrome in the PINNED cascade order (EC-389):\n *\n * 1. `ec-app-css` — the canvas stylesheet `<link>` (site delivery CSS + studio CSS);\n * 2. `ec-outline-css` — the studio's editing chrome (selection/DnD/toolbar styles);\n * 3. `ec-kit-tokens` — an instantiated starter kit's `tokensCss` (kit provides…);\n * 4. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (…consumer overrides).\n *\n * Later elements win the cascade at equal specificity, so studio defaults <\n * kit tokens < consumer config — the customization-seam ordering. The body\n * class is the consumer's `bodyClassName` when given (even `''`), else the\n * studio default. Safe to run repeatedly (iframe onLoad + the already-loaded\n * fast path): links/styles are deduped by id, and the two token styles are\n * re-appended IN ORDER so a re-wire never flips who wins.\n */\nexport function applyCanvasChrome(d: CanvasChromeDocument, inputs: CanvasChromeInputs): void {\n if (inputs.stylesheetUrl && !d.getElementById('ec-app-css')) {\n const link = d.createElement('link')\n link.id = 'ec-app-css'\n link.rel = 'stylesheet'\n link.href = inputs.stylesheetUrl\n d.head.appendChild(link)\n }\n if (!d.getElementById('ec-outline-css')) {\n const style = d.createElement('style')\n style.id = 'ec-outline-css'\n style.textContent = OUTLINE_CSS\n d.head.appendChild(style)\n }\n // Both token layers re-append (remove + add) on every wire, in sequence, so\n // the consumer element always lands after the kit element.\n appendCssLayer(d, 'ec-kit-tokens', inputs.kitTokensCss)\n appendCssLayer(d, 'ec-canvas-theme', inputs.theme?.tokensCss)\n d.body.className = inputs.theme?.bodyClassName ?? DEFAULT_CANVAS_BODY_CLASS\n}\n\n/** Replace-append one identified `<style>` layer; absent CSS removes a stale element. */\nfunction appendCssLayer(d: CanvasChromeDocument, id: string, css: string | undefined): void {\n const existing = d.getElementById(id)\n if (existing) existing.remove()\n if (css === undefined || css === '') return\n const style = d.createElement('style')\n style.id = id\n style.textContent = css\n d.head.appendChild(style)\n}\n"]}
|
|
@@ -3,10 +3,11 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { renderPageComposition } from '@elytracms/core/runtime-renderer';
|
|
5
5
|
import { useParams } from '@elytracms/studio/router';
|
|
6
|
-
import { PRESERVE_NODE_ATTR } from '@elytracms/core/rich-text';
|
|
7
6
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
8
7
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
9
8
|
import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
|
|
9
|
+
import { resolveDeploymentCanvasTheme } from '@elytracms/studio/lib/workspace';
|
|
10
|
+
import { applyCanvasChrome } from './canvas-chrome.js';
|
|
10
11
|
import { getCanvasStylesheetUrl } from './canvas-stylesheet.js';
|
|
11
12
|
import { useBuilder } from './builder-store.js';
|
|
12
13
|
import { useCanvasViewports, viewportWidth } from './canvas-breakpoints.js';
|
|
@@ -17,68 +18,6 @@ import { useCanvasRenderContext } from './canvas-render-context.js';
|
|
|
17
18
|
import { ClipboardNoticeOverlay, attachClipboardListeners, useStudioClipboard, } from './clipboard-actions.js';
|
|
18
19
|
import { RichTextToolbar } from './rich-text-toolbar.js';
|
|
19
20
|
import { SpacingHandles, hasSpacingHandles } from './spacing-handles.js';
|
|
20
|
-
/** Selection/hover outlines, scoped to the per-node editable wrappers. */
|
|
21
|
-
const OUTLINE_CSS = `
|
|
22
|
-
/* The canvas backdrop is deliberately always light/site-like (see the
|
|
23
|
-
wireUp note below) — force the native form-control/scrollbar theme to
|
|
24
|
-
match, so a page with real inputs never shows dark native chrome floating
|
|
25
|
-
on a light page. */
|
|
26
|
-
html { color-scheme: light; }
|
|
27
|
-
body { margin: 0; padding: 16px; }
|
|
28
|
-
.ec-editable { outline-offset: -1px; cursor: default; }
|
|
29
|
-
.ec-editable[data-hovered]:not([data-selected]) { outline: 1px dashed rgba(130,150,190,0.75); }
|
|
30
|
-
.ec-editable[data-selected] { outline: 2px solid #5b8cff; }
|
|
31
|
-
/* Inline editing (EC-158 text, EC-159 rich text): the rendered element
|
|
32
|
-
itself becomes the editor — no overlay, no replacement input — so
|
|
33
|
-
typography, alignment, and layout are pixel-identical between edit and
|
|
34
|
-
render states. The wrapper keeps the selection outline; the host only
|
|
35
|
-
gets a caret. pre-wrap applies to PLAIN text hosts only — rich-text
|
|
36
|
-
markup must keep its rendered whitespace handling (no layout shift). */
|
|
37
|
-
.ec-editable[data-ec-editing] { outline: 2px solid #5b8cff; }
|
|
38
|
-
.ec-editable [data-ec-edit] { outline: none; cursor: text; }
|
|
39
|
-
.ec-editable [data-ec-edit='text'] { white-space: pre-wrap; }
|
|
40
|
-
/* Preserved atoms during a rich-text session (custom blocks, unknown nodes,
|
|
41
|
-
images, embeds): visible and rendered, but not text-editable. */
|
|
42
|
-
.ec-editable [data-ec-edit='rich'] [${PRESERVE_NODE_ATTR}] { user-select: none; }
|
|
43
|
-
/* Floating mark toolbar (EC-159), portaled into the iframe. */
|
|
44
|
-
.ec-rt-toolbar { color-scheme: dark; }
|
|
45
|
-
.ec-rt-toolbar-row { display: flex; align-items: center; gap: 2px; padding: 3px;
|
|
46
|
-
border: 1px solid rgba(130,150,190,0.4); border-radius: 8px;
|
|
47
|
-
background: rgb(24,26,32); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }
|
|
48
|
-
.ec-rt-toolbar-btn { min-width: 26px; height: 24px; padding: 0 7px; border: 0; border-radius: 5px;
|
|
49
|
-
background: transparent; color: rgb(225,228,236); font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;
|
|
50
|
-
cursor: pointer; }
|
|
51
|
-
.ec-rt-toolbar-btn:hover { background: rgba(130,150,190,0.25); }
|
|
52
|
-
.ec-rt-toolbar-btn[data-active] { background: #5b8cff; color: white; }
|
|
53
|
-
.ec-rt-toolbar-input { width: 200px; height: 24px; padding: 0 7px; border-radius: 5px;
|
|
54
|
-
border: 1px solid rgba(130,150,190,0.4); background: rgb(16,18,22); color: rgb(225,228,236);
|
|
55
|
-
font: 12px/1 ui-sans-serif, system-ui, sans-serif; outline: none; }
|
|
56
|
-
/* Locale-fallback chips (EC-176): a node whose bound content is currently
|
|
57
|
-
served via locale fallback wears a small corner chip naming the locale it
|
|
58
|
-
actually came from — the canvas never silently mixes locales. */
|
|
59
|
-
.ec-editable[data-ec-locale-fallback] { position: relative; }
|
|
60
|
-
.ec-editable[data-ec-locale-fallback]::after {
|
|
61
|
-
content: attr(data-ec-locale-fallback);
|
|
62
|
-
position: absolute; top: 2px; right: 2px; z-index: 5; pointer-events: none;
|
|
63
|
-
padding: 0 4px; border-radius: 4px;
|
|
64
|
-
font: 600 9px/1.5 ui-sans-serif, system-ui, sans-serif;
|
|
65
|
-
letter-spacing: 0.05em; text-transform: uppercase;
|
|
66
|
-
color: #fbbf24; background: rgba(180,83,9,0.25); border: 1px solid rgba(245,158,11,0.45);
|
|
67
|
-
}
|
|
68
|
-
/* Drag & drop states (EC-160/EC-162): eligibility comes from the shared pure
|
|
69
|
-
rules (lib/dnd/drop-rules); these classes only *show* the resolution.
|
|
70
|
-
Rejection is explicit and visible during the drag — never a silent failed
|
|
71
|
-
drop. Before/after bars follow the parent's flow axis (data-ec-drop-axis);
|
|
72
|
-
left/right are the wrap-into-columns gesture (violet, paired highlight). */
|
|
73
|
-
.ec-editable[data-ec-drop='into'] { outline: 2px solid #34d399; outline-offset: -2px; background-color: rgba(52, 211, 153, 0.08); }
|
|
74
|
-
.ec-editable[data-ec-drop='before'] { box-shadow: 0 -3px 0 0 #38bdf8; }
|
|
75
|
-
.ec-editable[data-ec-drop='after'] { box-shadow: 0 3px 0 0 #38bdf8; }
|
|
76
|
-
.ec-editable[data-ec-drop='before'][data-ec-drop-axis='x'] { box-shadow: -3px 0 0 0 #38bdf8; }
|
|
77
|
-
.ec-editable[data-ec-drop='after'][data-ec-drop-axis='x'] { box-shadow: 3px 0 0 0 #38bdf8; }
|
|
78
|
-
.ec-editable[data-ec-drop='left'] { box-shadow: -3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }
|
|
79
|
-
.ec-editable[data-ec-drop='right'] { box-shadow: 3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }
|
|
80
|
-
.ec-editable[data-ec-drop='blocked'] { outline: 2px solid #ef4444; outline-offset: -2px; background-color: rgba(239, 68, 68, 0.08); cursor: not-allowed; }
|
|
81
|
-
`;
|
|
82
21
|
/**
|
|
83
22
|
* Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders
|
|
84
23
|
* into a same-origin iframe via a React portal — true style isolation + true
|
|
@@ -120,34 +59,17 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
|
|
|
120
59
|
// dark-themed and the iframe root inherits that class). The injected
|
|
121
60
|
// canvas stylesheet's `:root` (light) tokens supply that backdrop for free
|
|
122
61
|
// once `.dark` is never added here (mirrors the components-preview fix,
|
|
123
|
-
// `component-preview.tsx`). The editing chrome
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
if (!d.getElementById('ec-outline-css')) {
|
|
136
|
-
const style = d.createElement('style');
|
|
137
|
-
style.id = 'ec-outline-css';
|
|
138
|
-
style.textContent = OUTLINE_CSS;
|
|
139
|
-
d.head.appendChild(style);
|
|
140
|
-
}
|
|
141
|
-
if (kitTokensCss) {
|
|
142
|
-
const existing = d.getElementById('ec-kit-tokens');
|
|
143
|
-
if (existing)
|
|
144
|
-
existing.remove();
|
|
145
|
-
const style = d.createElement('style');
|
|
146
|
-
style.id = 'ec-kit-tokens';
|
|
147
|
-
style.textContent = kitTokensCss;
|
|
148
|
-
d.head.appendChild(style);
|
|
149
|
-
}
|
|
150
|
-
d.body.className = 'bg-background text-foreground font-sans';
|
|
62
|
+
// `component-preview.tsx`). The editing chrome (selection/hover outlines,
|
|
63
|
+
// drop indicators, the rich-text toolbar, locale-fallback chips) uses
|
|
64
|
+
// hardcoded colors rather than these tokens, so it stays visible against a
|
|
65
|
+
// light page regardless. The full injection — stylesheet link, editing
|
|
66
|
+
// chrome, kit tokens, and the consumer's `defineStudio({ canvas })` theme
|
|
67
|
+
// (EC-389, injected LAST so it wins) — lives in `applyCanvasChrome`.
|
|
68
|
+
applyCanvasChrome(d, {
|
|
69
|
+
stylesheetUrl: getCanvasStylesheetUrl(),
|
|
70
|
+
kitTokensCss,
|
|
71
|
+
theme: resolveDeploymentCanvasTheme(),
|
|
72
|
+
});
|
|
151
73
|
setDoc(d);
|
|
152
74
|
}, [kitTokensCss]);
|
|
153
75
|
// Handle the already-loaded case (srcDoc may load before onLoad attaches).
|