@elytracms/studio 0.0.59 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/app-views/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 +46 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +5 -1
- package/dist/components/cms/rich-text/toolbar.js +4 -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/package.json +2 -2
|
@@ -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).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/components/builder/canvas.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { renderPageComposition } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { useParams } from '@elytracms/studio/router'\nimport { PRESERVE_NODE_ATTR } from '@elytracms/core/rich-text'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { getCanvasStylesheetUrl } from './canvas-stylesheet'\nimport { useBuilder } from './builder-store'\nimport { useCanvasViewports, viewportWidth } from './canvas-breakpoints'\nimport { nodeIdAt } from './canvas-dom'\nimport { useCanvasDnd } from './canvas-dnd'\nimport { useCanvasInlineEditing } from './canvas-inline-editing'\nimport { useCanvasRenderContext } from './canvas-render-context'\nimport {\n ClipboardNoticeOverlay,\n attachClipboardListeners,\n useStudioClipboard,\n} from './clipboard-actions'\nimport { RichTextToolbar } from './rich-text-toolbar'\nimport { SpacingHandles, hasSpacingHandles } from './spacing-handles'\n\n\n/** Selection/hover outlines, scoped to the per-node editable wrappers. */\nconst OUTLINE_CSS = `\n /* The canvas backdrop is deliberately always light/site-like (see the\n wireUp note below) — force the native form-control/scrollbar theme to\n match, so a page with real inputs never shows dark native chrome floating\n 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 * Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders\n * into a same-origin iframe via a React portal — true style isolation + true\n * responsive widths. Each node is wrapped with an editable marker so clicks\n * select and hover/selection draw outlines. Native listeners bridge the iframe\n * boundary (React's synthetic events don't cross frames); the two big gesture\n * state machines live in their own tested modules (EC-330): drag & drop in\n * `canvas-dnd`, inline text/rich-text editing in `canvas-inline-editing`, and\n * the render-context assembly in `canvas-render-context`.\n */\nexport function BuilderCanvas({\n perspective = 'draft',\n}: {\n /** Resolve references against this perspective (EC-300/AD-16). The editable Draft\n * view passes `'draft'` (a just-made reusable shows immediately); the read-only\n * Published view passes `'published'` (honest preview of what delivery serves). */\n perspective?: CanvasPerspective\n} = {}) {\n const builder = useBuilder()\n const { dispatch } = builder\n const studio = getStudioRegistry()\n\n // The edited composition root, composed for preview. EC-287 retired the project\n // graph and its published-graph preview; the canvas always renders the live,\n // editable draft (content publishing is per document).\n const activeRoot: ComponentNode = builder.present\n const iframeRef = useRef<HTMLIFrameElement>(null)\n const [doc, setDoc] = useState<Document | null>(null)\n\n // Graph/selection refs let the native iframe listeners read the *current*\n // state without re-subscribing on every edit.\n const presentRef = useRef(builder.present)\n presentRef.current = builder.present\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n\n // When the project was created from a starter kit, its brand token palette drives the canvas UI\n // (EC-117) — injected after app.css so the kit's overrides win in the cascade.\n const params = useParams({ strict: false }) as { projectId?: string }\n const kitTokensCss = params.projectId ? getInstantiatedKit(params.projectId)?.tokensCss : undefined\n\n const wireUp = useCallback(() => {\n const d = iframeRef.current?.contentDocument\n if (!d || !d.body) return\n // Deliberately no `.dark` here: this iframe renders the REAL page (plus\n // studio editing chrome portaled alongside it), and its backdrop must read\n // as a light, site-like page — never the studio chrome's current theme (a\n // black canvas is exactly what leaks through when the studio is\n // dark-themed and the iframe root inherits that class). The injected\n // canvas stylesheet's `:root` (light) tokens supply that backdrop for free\n // once `.dark` is never added here (mirrors the components-preview fix,\n // `component-preview.tsx`). The editing chrome below (selection/hover\n // outlines, drop indicators, the rich-text toolbar, locale-fallback chips)\n // uses hardcoded colors rather than these tokens, so it stays visible\n // against a light page regardless.\n const appCss = getCanvasStylesheetUrl()\n if (appCss && !d.getElementById('ec-app-css')) {\n const link = d.createElement('link')\n link.id = 'ec-app-css'\n link.rel = 'stylesheet'\n link.href = appCss\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 if (kitTokensCss) {\n const existing = d.getElementById('ec-kit-tokens')\n if (existing) existing.remove()\n const style = d.createElement('style')\n style.id = 'ec-kit-tokens'\n style.textContent = kitTokensCss\n d.head.appendChild(style)\n }\n d.body.className = 'bg-background text-foreground font-sans'\n setDoc(d)\n }, [kitTokensCss])\n\n // Handle the already-loaded case (srcDoc may load before onLoad attaches).\n useEffect(() => {\n if (iframeRef.current?.contentDocument?.readyState === 'complete') wireUp()\n }, [wireUp])\n\n // Inline editing (EC-158/EC-159): the dblclick-to-edit state machine, the\n // live session ref other listeners consult, the rich-text host (mark\n // toolbar), and the per-node remount epochs (decorate keys).\n const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio })\n const { editingRef, richEditHost, richEditEpochs } = inline\n\n // Native selection/hover listeners inside the iframe document.\n useEffect(() => {\n if (!doc) return\n const onClick = (e: Event) => {\n // Clicks inside the active inline editor are caret moves, not selection.\n if (editingRef.current?.host.contains(e.target as Node)) return\n const id = nodeIdAt(e.target)\n if (id) {\n e.preventDefault()\n dispatch({ type: 'select', id })\n }\n }\n const onOver = (e: Event) => dispatch({ type: 'hover', id: nodeIdAt(e.target) })\n const onLeave = () => dispatch({ type: 'hover', id: null })\n doc.addEventListener('click', onClick, true)\n doc.addEventListener('mouseover', onOver)\n doc.addEventListener('mouseleave', onLeave, true)\n return () => {\n doc.removeEventListener('click', onClick, true)\n doc.removeEventListener('mouseover', onOver)\n doc.removeEventListener('mouseleave', onLeave, true)\n }\n }, [doc, dispatch, editingRef])\n\n // Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.\n useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef })\n\n // Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page\n // tree and blocks panel use, wired to the *iframe* document (events never\n // cross frames). Shortcuts stay inert while an inline editor is live and in\n // form fields; the iframe's own navigator.clipboard does the async I/O\n // (Chromium scopes clipboard access to the focused document).\n const clipboard = useStudioClipboard()\n useEffect(() => {\n if (!doc) return\n return attachClipboardListeners(doc, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n isBlocked: () => editingRef.current !== null,\n asyncClipboard: doc.defaultView?.navigator.clipboard,\n getSelection: () => doc.getSelection(),\n })\n }, [doc, clipboard, editingRef])\n\n // The render context: the seven delivery-parity resolvers + the editable-\n // wrapper decorator (selection/hover, locale chips, remount epochs) — see\n // `canvas-render-context`.\n const { ctx, pageNodeIds } = useCanvasRenderContext({\n present: builder.present,\n selectedId: builder.selectedId,\n hoveredId: builder.hoveredId,\n studio,\n perspective,\n richEditEpochs,\n })\n\n // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children\n // gets draggable gap handles between them, portaled into the iframe.\n const spacingTarget =\n doc && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)\n ? builder.selectedNode\n : null\n\n // EC-250: the preview frame width derives from the config-sourced breakpoints\n // (Tailwind defaults when unconfigured), the same resolved set the toolbar\n // switcher offers.\n const { viewports } = useCanvasViewports()\n\n return (\n <div className=\"canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6\">\n {/* Copy/cut/paste outcomes (EC-175): non-blocking, with issue deep links. */}\n <ClipboardNoticeOverlay />\n <div style={{ width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }} className=\"flex\">\n <iframe\n ref={iframeRef}\n onLoad={wireUp}\n title={translate('builder.canvas.pagePreview')}\n className=\"min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md\"\n srcDoc=\"<!doctype html><html><head></head><body></body></html>\"\n />\n {doc\n ? createPortal(\n // Render the composition root directly (EC-287: no layout\n // indirection — a page renders its blocks).\n renderPageComposition(activeRoot, ctx),\n doc.body,\n )\n : null}\n {spacingTarget && doc ? (\n <SpacingHandles key={spacingTarget.id} doc={doc} node={spacingTarget} />\n ) : null}\n {/* Floating mark toolbar for the live rich-text session (EC-159). */}\n {richEditHost && doc ? <RichTextToolbar doc={doc} host={richEditHost} /> : null}\n </div>\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/components/builder/canvas.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { renderPageComposition } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { useParams } from '@elytracms/studio/router'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { resolveDeploymentCanvasTheme } from '@elytracms/studio/lib/workspace'\nimport { applyCanvasChrome } from './canvas-chrome'\nimport { getCanvasStylesheetUrl } from './canvas-stylesheet'\nimport { useBuilder } from './builder-store'\nimport { useCanvasViewports, viewportWidth } from './canvas-breakpoints'\nimport { nodeIdAt } from './canvas-dom'\nimport { useCanvasDnd } from './canvas-dnd'\nimport { useCanvasInlineEditing } from './canvas-inline-editing'\nimport { useCanvasRenderContext } from './canvas-render-context'\nimport {\n ClipboardNoticeOverlay,\n attachClipboardListeners,\n useStudioClipboard,\n} from './clipboard-actions'\nimport { RichTextToolbar } from './rich-text-toolbar'\nimport { SpacingHandles, hasSpacingHandles } from './spacing-handles'\n\n\n/**\n * Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders\n * into a same-origin iframe via a React portal — true style isolation + true\n * responsive widths. Each node is wrapped with an editable marker so clicks\n * select and hover/selection draw outlines. Native listeners bridge the iframe\n * boundary (React's synthetic events don't cross frames); the two big gesture\n * state machines live in their own tested modules (EC-330): drag & drop in\n * `canvas-dnd`, inline text/rich-text editing in `canvas-inline-editing`, and\n * the render-context assembly in `canvas-render-context`.\n */\nexport function BuilderCanvas({\n perspective = 'draft',\n}: {\n /** Resolve references against this perspective (EC-300/AD-16). The editable Draft\n * view passes `'draft'` (a just-made reusable shows immediately); the read-only\n * Published view passes `'published'` (honest preview of what delivery serves). */\n perspective?: CanvasPerspective\n} = {}) {\n const builder = useBuilder()\n const { dispatch } = builder\n const studio = getStudioRegistry()\n\n // The edited composition root, composed for preview. EC-287 retired the project\n // graph and its published-graph preview; the canvas always renders the live,\n // editable draft (content publishing is per document).\n const activeRoot: ComponentNode = builder.present\n const iframeRef = useRef<HTMLIFrameElement>(null)\n const [doc, setDoc] = useState<Document | null>(null)\n\n // Graph/selection refs let the native iframe listeners read the *current*\n // state without re-subscribing on every edit.\n const presentRef = useRef(builder.present)\n presentRef.current = builder.present\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n\n // When the project was created from a starter kit, its brand token palette drives the canvas UI\n // (EC-117) — injected after app.css so the kit's overrides win in the cascade.\n const params = useParams({ strict: false }) as { projectId?: string }\n const kitTokensCss = params.projectId ? getInstantiatedKit(params.projectId)?.tokensCss : undefined\n\n const wireUp = useCallback(() => {\n const d = iframeRef.current?.contentDocument\n if (!d || !d.body) return\n // Deliberately no `.dark` here: this iframe renders the REAL page (plus\n // studio editing chrome portaled alongside it), and its backdrop must read\n // as a light, site-like page — never the studio chrome's current theme (a\n // black canvas is exactly what leaks through when the studio is\n // dark-themed and the iframe root inherits that class). The injected\n // canvas stylesheet's `:root` (light) tokens supply that backdrop for free\n // once `.dark` is never added here (mirrors the components-preview fix,\n // `component-preview.tsx`). The editing chrome (selection/hover outlines,\n // drop indicators, the rich-text toolbar, locale-fallback chips) uses\n // hardcoded colors rather than these tokens, so it stays visible against a\n // light page regardless. The full injection — stylesheet link, editing\n // chrome, kit tokens, and the consumer's `defineStudio({ canvas })` theme\n // (EC-389, injected LAST so it wins) — lives in `applyCanvasChrome`.\n applyCanvasChrome(d, {\n stylesheetUrl: getCanvasStylesheetUrl(),\n kitTokensCss,\n theme: resolveDeploymentCanvasTheme(),\n })\n setDoc(d)\n }, [kitTokensCss])\n\n // Handle the already-loaded case (srcDoc may load before onLoad attaches).\n useEffect(() => {\n if (iframeRef.current?.contentDocument?.readyState === 'complete') wireUp()\n }, [wireUp])\n\n // Inline editing (EC-158/EC-159): the dblclick-to-edit state machine, the\n // live session ref other listeners consult, the rich-text host (mark\n // toolbar), and the per-node remount epochs (decorate keys).\n const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio })\n const { editingRef, richEditHost, richEditEpochs } = inline\n\n // Native selection/hover listeners inside the iframe document.\n useEffect(() => {\n if (!doc) return\n const onClick = (e: Event) => {\n // Clicks inside the active inline editor are caret moves, not selection.\n if (editingRef.current?.host.contains(e.target as Node)) return\n const id = nodeIdAt(e.target)\n if (id) {\n e.preventDefault()\n dispatch({ type: 'select', id })\n }\n }\n const onOver = (e: Event) => dispatch({ type: 'hover', id: nodeIdAt(e.target) })\n const onLeave = () => dispatch({ type: 'hover', id: null })\n doc.addEventListener('click', onClick, true)\n doc.addEventListener('mouseover', onOver)\n doc.addEventListener('mouseleave', onLeave, true)\n return () => {\n doc.removeEventListener('click', onClick, true)\n doc.removeEventListener('mouseover', onOver)\n doc.removeEventListener('mouseleave', onLeave, true)\n }\n }, [doc, dispatch, editingRef])\n\n // Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.\n useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef })\n\n // Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page\n // tree and blocks panel use, wired to the *iframe* document (events never\n // cross frames). Shortcuts stay inert while an inline editor is live and in\n // form fields; the iframe's own navigator.clipboard does the async I/O\n // (Chromium scopes clipboard access to the focused document).\n const clipboard = useStudioClipboard()\n useEffect(() => {\n if (!doc) return\n return attachClipboardListeners(doc, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n isBlocked: () => editingRef.current !== null,\n asyncClipboard: doc.defaultView?.navigator.clipboard,\n getSelection: () => doc.getSelection(),\n })\n }, [doc, clipboard, editingRef])\n\n // The render context: the seven delivery-parity resolvers + the editable-\n // wrapper decorator (selection/hover, locale chips, remount epochs) — see\n // `canvas-render-context`.\n const { ctx, pageNodeIds } = useCanvasRenderContext({\n present: builder.present,\n selectedId: builder.selectedId,\n hoveredId: builder.hoveredId,\n studio,\n perspective,\n richEditEpochs,\n })\n\n // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children\n // gets draggable gap handles between them, portaled into the iframe.\n const spacingTarget =\n doc && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)\n ? builder.selectedNode\n : null\n\n // EC-250: the preview frame width derives from the config-sourced breakpoints\n // (Tailwind defaults when unconfigured), the same resolved set the toolbar\n // switcher offers.\n const { viewports } = useCanvasViewports()\n\n return (\n <div className=\"canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6\">\n {/* Copy/cut/paste outcomes (EC-175): non-blocking, with issue deep links. */}\n <ClipboardNoticeOverlay />\n <div style={{ width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }} className=\"flex\">\n <iframe\n ref={iframeRef}\n onLoad={wireUp}\n title={translate('builder.canvas.pagePreview')}\n className=\"min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md\"\n srcDoc=\"<!doctype html><html><head></head><body></body></html>\"\n />\n {doc\n ? createPortal(\n // Render the composition root directly (EC-287: no layout\n // indirection — a page renders its blocks).\n renderPageComposition(activeRoot, ctx),\n doc.body,\n )\n : null}\n {spacingTarget && doc ? (\n <SpacingHandles key={spacingTarget.id} doc={doc} node={spacingTarget} />\n ) : null}\n {/* Floating mark toolbar for the live rich-text session (EC-159). */}\n {richEditHost && doc ? <RichTextToolbar doc={doc} host={richEditHost} /> : null}\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -21,6 +21,21 @@ import type { PropField } from '@elytracms/core/component-registry';
|
|
|
21
21
|
*/
|
|
22
22
|
export type ControlKind = 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'json' | 'color' | 'richtext' | 'asset' | 'object' | 'relation';
|
|
23
23
|
export declare function controlKind(spec: PropField): ControlKind;
|
|
24
|
+
/**
|
|
25
|
+
* EC-387: route a rich-text editor emission to the prop value callbacks.
|
|
26
|
+
*
|
|
27
|
+
* The shared {@link RichTextEditor} emits either an emptied-editor `undefined`, the
|
|
28
|
+
* exact stored envelope (an untouched/reverted edit), or a fresh `{ version, doc }`
|
|
29
|
+
* value — never a plain string. An `undefined` clears the prop (reverting to the
|
|
30
|
+
* manifest default, matching the asset/relation/object controls); anything else is a
|
|
31
|
+
* structured `JsonValue` envelope set verbatim. Extracted pure so the wiring — the
|
|
32
|
+
* exact bug the retired textarea stub had (it wrote a bare string over the envelope) —
|
|
33
|
+
* is regression-pinned without driving TipTap.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyRichTextPropChange(next: unknown, handlers: {
|
|
36
|
+
onSet: (value: JsonValue) => void;
|
|
37
|
+
onClear: () => void;
|
|
38
|
+
}): void;
|
|
24
39
|
export interface PropValueControlProps {
|
|
25
40
|
/** DOM id for the control's primary input (label `htmlFor` target). */
|
|
26
41
|
id: string;
|
|
@@ -6,6 +6,7 @@ import { Input, Row, Select, Stack, Text } from '@elytracms/studio/ui';
|
|
|
6
6
|
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
7
7
|
import { FieldControl } from './field-control.js';
|
|
8
8
|
import { FieldDensityProvider } from './field-density.js';
|
|
9
|
+
import { RichTextEditor } from './rich-text/index.js';
|
|
9
10
|
import { useOptionalCollectionView } from '../collection-view/collection-view-context.js';
|
|
10
11
|
export function controlKind(spec) {
|
|
11
12
|
switch (spec.type) {
|
|
@@ -42,6 +43,23 @@ export function controlKind(spec) {
|
|
|
42
43
|
return 'text';
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* EC-387: route a rich-text editor emission to the prop value callbacks.
|
|
48
|
+
*
|
|
49
|
+
* The shared {@link RichTextEditor} emits either an emptied-editor `undefined`, the
|
|
50
|
+
* exact stored envelope (an untouched/reverted edit), or a fresh `{ version, doc }`
|
|
51
|
+
* value — never a plain string. An `undefined` clears the prop (reverting to the
|
|
52
|
+
* manifest default, matching the asset/relation/object controls); anything else is a
|
|
53
|
+
* structured `JsonValue` envelope set verbatim. Extracted pure so the wiring — the
|
|
54
|
+
* exact bug the retired textarea stub had (it wrote a bare string over the envelope) —
|
|
55
|
+
* is regression-pinned without driving TipTap.
|
|
56
|
+
*/
|
|
57
|
+
export function applyRichTextPropChange(next, handlers) {
|
|
58
|
+
if (next === undefined)
|
|
59
|
+
handlers.onClear();
|
|
60
|
+
else
|
|
61
|
+
handlers.onSet(next);
|
|
62
|
+
}
|
|
45
63
|
/**
|
|
46
64
|
* Render + edit a single manifest prop value. Store-agnostic: emits through
|
|
47
65
|
* `onSet`/`onClear`. Shows a validation issue when the current value fails the
|
|
@@ -82,7 +100,26 @@ export function PropValueControl({ id, name, spec, value, onSet, onClear }) {
|
|
|
82
100
|
else if (kind === 'color') {
|
|
83
101
|
control = (_jsxs(Row, { gap: "2", children: [_jsx("input", { id: id, type: "color", value: typeof current === 'string' && /^#[0-9a-f]{6}$/i.test(current) ? current : '#000000', onChange: (e) => onSet(e.target.value), className: "h-8 w-10 shrink-0 rounded-md border border-input bg-background" }), _jsx(Input, { value: typeof current === 'string' ? current : '', onChange: (e) => onSet(e.target.value) })] }));
|
|
84
102
|
}
|
|
85
|
-
else if (kind === '
|
|
103
|
+
else if (kind === 'richtext') {
|
|
104
|
+
// EC-387: a richText PROP now uses the SAME real editor as the collection-field
|
|
105
|
+
// path (`field-control.tsx` `case 'richText'`) — one editing surface, one
|
|
106
|
+
// `{ version, doc }` value contract, never a second implementation. The retired
|
|
107
|
+
// textarea stub read `typeof current === 'string'`, so a stored envelope showed
|
|
108
|
+
// EMPTY and any edit clobbered the structured document with a bare string
|
|
109
|
+
// (silent data destruction). The editor reads the envelope (or a legacy string,
|
|
110
|
+
// or empty), always emits an envelope, and routes an emptied editor's `undefined`
|
|
111
|
+
// to a prop clear via {@link applyRichTextPropChange}.
|
|
112
|
+
//
|
|
113
|
+
// Like the asset/relation/object controls below, the editor needs the ambient
|
|
114
|
+
// workspace — its asset-image picker reads the REQUIRED collection-view context.
|
|
115
|
+
// The block property inspector (the EC-387 target) is inside the detail editor's
|
|
116
|
+
// `CollectionViewProvider`, so it gets the full editor; the component-registry
|
|
117
|
+
// preview surface (no provider, nothing to edit against) degrades to the same
|
|
118
|
+
// read-only value display the sibling controls use, instead of crashing with
|
|
119
|
+
// "useCollectionView must be used within a <CollectionViewProvider>".
|
|
120
|
+
control = hasWorkspaceContext ? (_jsx(RichTextEditor, { id: id, value: current, onChange: (next) => applyRichTextPropChange(next, { onSet, onClear }), allow: spec.type === 'richText' ? spec.allow : undefined })) : (_jsx(WorkspaceValueFallback, { value: current }));
|
|
121
|
+
}
|
|
122
|
+
else if (kind === 'textarea') {
|
|
86
123
|
control = (_jsx("textarea", { id: id, rows: 2, value: typeof current === 'string' ? current : '', onChange: (e) => onSet(e.target.value), className: "flex w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm text-foreground shadow-sm focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none" }));
|
|
87
124
|
}
|
|
88
125
|
else if (kind === 'asset' || kind === 'object' || kind === 'relation') {
|