@elytracms/studio 0.0.83 → 0.0.85
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/builder/blocks-model.js +5 -2
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.js +1 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +48 -0
- package/dist/components/builder/composition-field-editor.js +94 -22
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/inline-edit.js +3 -2
- package/dist/components/builder/inline-edit.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +4 -0
- package/dist/components/builder/node-factory.js +14 -9
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.d.ts +7 -16
- package/dist/components/cms/cms-composition-validators.js +101 -52
- package/dist/components/cms/cms-composition-validators.js.map +1 -1
- package/dist/components/cms/gate-validation.js +3 -1
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.d.ts +13 -7
- package/dist/components/cms/rich-text/component-embed.js +21 -13
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +1 -1
- package/dist/components/cms/rich-text/editor.js +2 -2
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +4 -0
- package/dist/components/cms/rich-text/extensions.js +6 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/insert.js +1 -1
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/rich-text/unknown-node.d.ts +66 -0
- package/dist/components/cms/rich-text/unknown-node.js +101 -0
- package/dist/components/cms/rich-text/unknown-node.js.map +1 -0
- package/dist/components/cms/rich-text/value.d.ts +82 -13
- package/dist/components/cms/rich-text/value.js +169 -38
- package/dist/components/cms/rich-text/value.js.map +1 -1
- package/dist/components/cms/rich-text-preview.js +1 -1
- package/dist/components/cms/rich-text-preview.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.js +3 -3
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +7 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +4 -0
- package/dist/lib/i18n/messages.en.js +7 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isCanvasEligible, manifestSlots, propValueValidator } from '@elytracms/core/component-registry';
|
|
2
2
|
import { localizedStringVariants } from '@elytracms/core/cms-core';
|
|
3
|
+
import { componentBlockNode } from '@elytracms/core/rich-text';
|
|
3
4
|
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
4
5
|
import { componentStatus } from '@elytracms/studio/lib/registry';
|
|
5
6
|
import { nodeFromProps } from '@elytracms/studio/lib/registry/preview';
|
|
@@ -65,7 +66,9 @@ export function fixtureSubtree(studio, manifest, fixture, baseId) {
|
|
|
65
66
|
issues.push(`Fixture "${fixture.name}" sets an invalid value for prop "${name}".`);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
// EC-547: fixture-built inserts are v2 block-sequence citizens like every
|
|
70
|
+
// other factory node — the root carries the `elytra/component` discriminator.
|
|
71
|
+
const node = componentBlockNode(nodeFromProps(manifest, fixtureProps, baseId));
|
|
69
72
|
if (fixture.slotChildren) {
|
|
70
73
|
// EC-386: blocks-prop regions are declared slots too (`manifestSlots`), so a
|
|
71
74
|
// fixture may seed a Section-style body without a false "undeclared slot" issue.
|
|
@@ -81,7 +84,7 @@ export function fixtureSubtree(studio, manifest, fixture, baseId) {
|
|
|
81
84
|
if (!childManifest) {
|
|
82
85
|
issues.push(`Fixture "${fixture.name}" references unknown component "${childId}" in slot "${slotName}".`);
|
|
83
86
|
// Keep the reference: the renderer shows a visible missing-component fallback.
|
|
84
|
-
return { id, componentId: childId };
|
|
87
|
+
return componentBlockNode({ id, componentId: childId });
|
|
85
88
|
}
|
|
86
89
|
return defaultNode(childManifest, id);
|
|
87
90
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks-model.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-model.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry'\nimport { isCanvasEligible, manifestSlots, propValueValidator } from '@elytracms/core/component-registry'\nimport { localizedStringVariants } from '@elytracms/core/cms-core'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport { componentStatus } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { nodeFromProps } from '@elytracms/studio/lib/registry/preview'\nimport { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory'\n\n/**\n * Blocks panel model (EC-164, reshaped by EC-439) — the pure, React-free\n * derivation behind the visual insert surface. Every canvas-eligible registry\n * manifest yields **one tile** carrying the component identity plus a\n * `variants` list (one per declared fixture, or a single implicit `Default`\n * variant built from the seeded manifest defaults). Tiles group by manifest\n * `category` and filter by a free-text query over name / description /\n * category / **fixture names** — no hand-maintained panel config. Broken\n * fixtures degrade to variants carrying explicit `issues`, never to a missing\n * entry or a crash.\n *\n * Previews are ARTIFACTS, never runtime renders (EC-439 AD-2). Per tile and\n * per variant, `previewUrl` resolves: curated `manifest.previewImage` (EC-409,\n * always wins) → the {@link GeneratedPreviewLookup} seam (EC-440's build-time\n * fixture renders) → `undefined` (the panel shows an icon+name placeholder).\n */\n\n/**\n * The generated-preview seam (EC-439 AD-2, filled by EC-440): a pure lookup\n * from component id + fixture name to an image URL. The EC-440 CLI renders\n * every canvas-eligible component's fixtures headlessly against the consumer\n * theme and writes a previews manifest; the consumer's `studio.config.tsx`\n * adapts that manifest into this function via `defineStudio({ previews:\n * { generated } })`. A fixture-less manifest's single implicit variant is\n * looked up under the name {@link DEFAULT_VARIANT}. Empty/undefined everywhere\n * until EC-440 lands — the panel then falls back to placeholders.\n */\nexport type GeneratedPreviewLookup = (componentId: string, fixtureName: string) => string | undefined\n\n/**\n * The implicit variant name of a fixture-less manifest (and the fixture name\n * a {@link GeneratedPreviewLookup} is queried with for it).\n */\nexport const DEFAULT_VARIANT = 'Default'\n\n/**\n * One insertable variant of a component tile: a declared manifest fixture, or\n * the single implicit `Default` for a fixture-less manifest. `variants[0]` is\n * the tile's default — what tile click / Enter / drag inserts.\n */\nexport interface BlockVariant {\n /** The fixture name, or {@link DEFAULT_VARIANT} for the implicit variant. */\n name: string\n /**\n * Validation problems for this variant (manifest-level statuses plus the\n * fixture's own issues). A non-empty list marks the variant as degraded —\n * it still lists and still inserts.\n */\n issues: string[]\n /**\n * Resolved preview image URL: curated `manifest.previewImage` (wins) else\n * the generated-preview lookup's URL for this fixture. `undefined` → the\n * panel renders the icon+name placeholder.\n */\n previewUrl?: string\n /** Build the subtree this variant inserts (ids are reassigned on insert). */\n build: () => ComponentNode\n}\n\n/** One component (or layout preset) in the blocks panel — ONE tile per component (EC-439 AD-1). */\nexport interface BlockTile {\n /** Stable identity: the component id, or `preset::<key>` for layout helpers. */\n key: string\n /** Component id of the node the tile inserts. */\n componentId: string\n title: string\n description?: string\n /**\n * The DEFAULT variant's validation problems — what clicking the tile\n * inserts. Per-variant issues live on {@link BlockTile.variants}.\n */\n issues: string[]\n /** The default variant's resolved preview URL (see {@link BlockVariant.previewUrl}). */\n previewUrl?: string\n /** All insertable variants; `variants[0]` is the default the tile inserts. */\n variants: BlockVariant[]\n /**\n * Fixture names the active query matched (stamped by {@link blockGroups}\n * when a query is present) — the panel highlights the variant affordance and\n * the matching flyout rows so \"found by fixture name\" is visible.\n */\n matchedVariants?: string[]\n /** Build the DEFAULT subtree to insert: the first declared fixture's, else the seeded manifest default. */\n build: () => ComponentNode\n}\n\n/** Tiles of one manifest `category`. */\nexport interface BlockGroup {\n category: string\n tiles: BlockTile[]\n}\n\nexport interface BlockFilter {\n /** Case-insensitive match over id, title, description, category, and fixture names. */\n query?: string\n /** When set, only these component ids may be inserted (slot.allow). */\n slotAllow?: string[]\n}\n\n/**\n * Intersect two component-id allow-lists. `undefined` means \"no constraint from\n * this source\", so the result is the other list (copied); two present lists yield\n * their set intersection, preserving the first list's order. This layers a\n * composition field's top-level `allow` vocabulary over a slot's positional\n * `allow` at the canvas root (EC-188): a component must satisfy BOTH leashes to be\n * offered. An empty array stays empty (\"allow nothing\"); callers wanting the\n * validator's \"empty = allow any\" must coalesce `[]` to `undefined` first (which\n * `resolveInsertContext` does via its `length > 0` guard, mirroring\n * `validateCompositionValue`).\n */\nexport function intersectAllow(\n slotAllow: readonly string[] | undefined,\n fieldAllow: readonly string[] | undefined,\n): string[] | undefined {\n if (!slotAllow) return fieldAllow ? [...fieldAllow] : undefined\n if (!fieldAllow) return [...slotAllow]\n const inField = new Set(fieldAllow)\n return slotAllow.filter((id) => inField.has(id))\n}\n\nconst UNCATEGORIZED = 'other'\n\n/** A node built from a manifest's defaults with an explicit id. */\nfunction defaultNode(manifest: ComponentManifest, id: string): ComponentNode {\n return { ...nodeFromManifest(manifest), id }\n}\n\n/** Structured issues for a component that cannot render (EC-055 statuses). */\nfunction manifestIssues(studio: StudioRegistry, manifest: ComponentManifest): string[] {\n const status = componentStatus(studio, manifest)\n if (status === 'incompatible') {\n return [`No implementation is registered for \"${manifest.id}\".`]\n }\n if (status === 'invalid') {\n return studio.registry.issues.filter((issue) => issue.componentId === manifest.id).map((issue) => issue.message)\n }\n return []\n}\n\n/**\n * Build the configured subtree a fixture describes: manifest defaults overlaid\n * with the fixture's props, plus one default-props child node per\n * `slotChildren` component id. Never throws — unknown components, undeclared\n * slots, and invalid prop values become entries in `issues` while the node is\n * still produced (the renderer shows its own visible fallbacks for them).\n */\nexport function fixtureSubtree(\n studio: StudioRegistry,\n manifest: ComponentManifest,\n fixture: ComponentFixture,\n baseId: string,\n): { node: ComponentNode; issues: string[] } {\n const issues: string[] = []\n const fixtureProps = fixture.props ?? {}\n\n for (const [name, value] of Object.entries(fixtureProps)) {\n const spec = manifest.props[name]\n if (!spec) {\n issues.push(`Fixture \"${fixture.name}\" sets unknown prop \"${name}\".`)\n continue\n }\n // EC-190: validate the fixture value with the prop's field-def validator.\n if (!(propValueValidator(manifest, name)?.(value) ?? true)) {\n issues.push(`Fixture \"${fixture.name}\" sets an invalid value for prop \"${name}\".`)\n }\n }\n\n const node = nodeFromProps(manifest, fixtureProps, baseId)\n\n if (fixture.slotChildren) {\n // EC-386: blocks-prop regions are declared slots too (`manifestSlots`), so a\n // fixture may seed a Section-style body without a false \"undeclared slot\" issue.\n const declared = new Set(manifestSlots(manifest).map((slot) => slot.name))\n const slots: NonNullable<ComponentNode['slots']> = {}\n for (const [slotName, childIds] of Object.entries(fixture.slotChildren)) {\n if (!declared.has(slotName)) {\n issues.push(`Fixture \"${fixture.name}\" fills undeclared slot \"${slotName}\".`)\n }\n slots[slotName] = childIds.map((childId, index) => {\n const id = `${baseId}-${slotName}-${index}`\n const childManifest = studio.registry.getManifest(childId)\n if (!childManifest) {\n issues.push(`Fixture \"${fixture.name}\" references unknown component \"${childId}\" in slot \"${slotName}\".`)\n // Keep the reference: the renderer shows a visible missing-component fallback.\n return { id, componentId: childId }\n }\n return defaultNode(childManifest, id)\n })\n }\n if (Object.keys(slots).length > 0) node.slots = slots\n }\n\n return { node, issues }\n}\n\n/**\n * The ONE tile for a manifest (EC-439 AD-1): component identity plus one\n * variant per declared fixture — or a single implicit `Default` variant using\n * the seeded manifest default (a bare layout container is seeded so it\n * survives EC-161 normalization instead of vanishing on commit, EC-383). The\n * tile's own `issues`/`previewUrl`/`build` mirror the default variant\n * (`variants[0]`), so tile click/Enter/drag inserts exactly what the first\n * fixture tile inserted before the reshape.\n */\nexport function tileForManifest(\n studio: StudioRegistry,\n manifest: ComponentManifest,\n generatedPreview?: GeneratedPreviewLookup,\n): BlockTile {\n const title = localizedLabel(manifest.title, manifest.id)\n const baseIssues = manifestIssues(studio, manifest)\n // EC-409: a curated preview image previews the COMPONENT, not one fixture —\n // when declared it wins on the tile and on every variant row (AD-2 tier 1).\n const curated = manifest.previewImage\n\n const variants: BlockVariant[] =\n manifest.fixtures && manifest.fixtures.length > 0\n ? manifest.fixtures.map((fixture, index) => {\n const { issues } = fixtureSubtree(studio, manifest, fixture, `probe-${manifest.id}-${index}`)\n return {\n name: fixture.name,\n issues: [...baseIssues, ...issues],\n ...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, fixture.name)),\n build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,\n }\n })\n : [\n {\n name: DEFAULT_VARIANT,\n issues: baseIssues,\n ...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, DEFAULT_VARIANT)),\n build: () => seededNodeFromManifest(manifest),\n },\n ]\n\n const defaultVariant = variants[0]!\n return {\n key: manifest.id,\n componentId: manifest.id,\n title,\n description: manifest.description,\n issues: defaultVariant.issues,\n ...previewUrlEntry(defaultVariant.previewUrl),\n variants,\n build: defaultVariant.build,\n }\n}\n\n/** Spreadable `{ previewUrl }` entry — omitted (not `undefined`-valued) when absent. */\nfunction previewUrlEntry(url: string | undefined): { previewUrl?: string } {\n return url === undefined ? {} : { previewUrl: url }\n}\n\n/**\n * A two-letter monogram from a tile title — the deliberate icon+name\n * placeholder glyph (EC-439 AD-2 tier 3) for tiles with neither a curated nor\n * a generated preview image: first letters of the first two words, else the\n * first two letters of a single word. Deterministic and pure.\n */\nexport function tileMonogram(title: string): string {\n const words = title.trim().split(/\\s+/).filter(Boolean)\n if (words.length === 0) return '?'\n const glyph = words.length === 1 ? words[0]!.slice(0, 2) : `${words[0]!.charAt(0)}${words[1]!.charAt(0)}`\n return glyph.toUpperCase()\n}\n\nfunction matchedFixtureNames(manifest: ComponentManifest, query: string): string[] {\n return (manifest.fixtures ?? [])\n .map((fixture) => fixture.name)\n .filter((name) => name.toLowerCase().includes(query))\n}\n\nfunction matches(manifest: ComponentManifest, query: string): boolean {\n const hay = [manifest.id, ...localizedStringVariants(manifest.title), manifest.description, manifest.category]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n // EC-439: fixture names are searchable too — a content-seeded fixture name\n // (\"Startseite\") must find its component even when no manifest field matches.\n return hay.includes(query) || matchedFixtureNames(manifest, query).length > 0\n}\n\n/**\n * Derive the panel's category groups from the registry: filter by slot allow\n * list and query, derive ONE tile per manifest, group by `category`, and sort\n * categories alphabetically (registry order within a group). Deterministic.\n * When a query is active, each tile is stamped with the fixture names it\n * matched (see {@link BlockTile.matchedVariants}).\n */\nexport function blockGroups(\n studio: StudioRegistry,\n filter: BlockFilter = {},\n generatedPreview?: GeneratedPreviewLookup,\n): BlockGroup[] {\n const query = filter.query?.trim().toLowerCase()\n const allow = filter.slotAllow\n\n const groups = new Map<string, BlockTile[]>()\n for (const manifest of studio.registry.list()) {\n // EC-191: the canvas is client-rendered, so only canvas-eligible\n // (client-renderable) components are offered for insertion; server-only / RSC\n // components are usable in dev frames, not placed on the canvas.\n if (!isCanvasEligible(manifest)) continue\n if (allow && !allow.includes(manifest.id)) continue\n if (query && !matches(manifest, query)) continue\n const tile = tileForManifest(studio, manifest, generatedPreview)\n if (query) {\n const matched = matchedFixtureNames(manifest, query)\n if (matched.length > 0) tile.matchedVariants = matched\n }\n const category = manifest.category ?? UNCATEGORIZED\n const tiles = groups.get(category) ?? []\n tiles.push(tile)\n groups.set(category, tiles)\n }\n\n return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, tiles]) => ({ category, tiles }))\n}\n\n/**\n * Whether ANY component could be inserted under a slot allow-list (EC-405) — the\n * cheap existence check behind the canvas insertion affordances: a \"+\" only shows\n * at a position where the blocks panel would offer at least one tile. Mirrors the\n * exact filter {@link blockGroups} applies (canvas-eligible AND, when an allow-list\n * is present, on that list), short-circuiting on the first match instead of\n * building tiles; falls back to the layout-helper presets (which are also\n * insertable there). `undefined`/absent `slotAllow` means \"allow any\" → true when\n * the registry has any canvas-eligible component.\n */\nexport function hasInsertableComponent(studio: StudioRegistry, slotAllow?: string[]): boolean {\n for (const manifest of studio.registry.list()) {\n if (!isCanvasEligible(manifest)) continue\n if (slotAllow && !slotAllow.includes(manifest.id)) continue\n return true\n }\n return presetTiles(slotAllow).length > 0\n}\n\n/**\n * Layout helper presets (EC-067) as panel tiles — already one tile per preset.\n * A preset has a single implicit variant and never a preview image, so its\n * tile renders the placeholder treatment.\n */\nexport function presetTiles(slotAllow?: string[]): BlockTile[] {\n return LAYOUT_PRESETS.filter((preset) => !slotAllow || slotAllow.includes(preset.build().componentId)).map(\n (preset) => ({\n key: `preset::${preset.key}`,\n componentId: preset.build().componentId,\n title: preset.label,\n description: preset.description,\n issues: [],\n variants: [{ name: DEFAULT_VARIANT, issues: [], build: () => preset.build() }],\n build: () => preset.build(),\n }),\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"blocks-model.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-model.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry'\nimport { isCanvasEligible, manifestSlots, propValueValidator } from '@elytracms/core/component-registry'\nimport { localizedStringVariants } from '@elytracms/core/cms-core'\nimport { componentBlockNode } from '@elytracms/core/rich-text'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport { componentStatus } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { nodeFromProps } from '@elytracms/studio/lib/registry/preview'\nimport { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory'\n\n/**\n * Blocks panel model (EC-164, reshaped by EC-439) — the pure, React-free\n * derivation behind the visual insert surface. Every canvas-eligible registry\n * manifest yields **one tile** carrying the component identity plus a\n * `variants` list (one per declared fixture, or a single implicit `Default`\n * variant built from the seeded manifest defaults). Tiles group by manifest\n * `category` and filter by a free-text query over name / description /\n * category / **fixture names** — no hand-maintained panel config. Broken\n * fixtures degrade to variants carrying explicit `issues`, never to a missing\n * entry or a crash.\n *\n * Previews are ARTIFACTS, never runtime renders (EC-439 AD-2). Per tile and\n * per variant, `previewUrl` resolves: curated `manifest.previewImage` (EC-409,\n * always wins) → the {@link GeneratedPreviewLookup} seam (EC-440's build-time\n * fixture renders) → `undefined` (the panel shows an icon+name placeholder).\n */\n\n/**\n * The generated-preview seam (EC-439 AD-2, filled by EC-440): a pure lookup\n * from component id + fixture name to an image URL. The EC-440 CLI renders\n * every canvas-eligible component's fixtures headlessly against the consumer\n * theme and writes a previews manifest; the consumer's `studio.config.tsx`\n * adapts that manifest into this function via `defineStudio({ previews:\n * { generated } })`. A fixture-less manifest's single implicit variant is\n * looked up under the name {@link DEFAULT_VARIANT}. Empty/undefined everywhere\n * until EC-440 lands — the panel then falls back to placeholders.\n */\nexport type GeneratedPreviewLookup = (componentId: string, fixtureName: string) => string | undefined\n\n/**\n * The implicit variant name of a fixture-less manifest (and the fixture name\n * a {@link GeneratedPreviewLookup} is queried with for it).\n */\nexport const DEFAULT_VARIANT = 'Default'\n\n/**\n * One insertable variant of a component tile: a declared manifest fixture, or\n * the single implicit `Default` for a fixture-less manifest. `variants[0]` is\n * the tile's default — what tile click / Enter / drag inserts.\n */\nexport interface BlockVariant {\n /** The fixture name, or {@link DEFAULT_VARIANT} for the implicit variant. */\n name: string\n /**\n * Validation problems for this variant (manifest-level statuses plus the\n * fixture's own issues). A non-empty list marks the variant as degraded —\n * it still lists and still inserts.\n */\n issues: string[]\n /**\n * Resolved preview image URL: curated `manifest.previewImage` (wins) else\n * the generated-preview lookup's URL for this fixture. `undefined` → the\n * panel renders the icon+name placeholder.\n */\n previewUrl?: string\n /** Build the subtree this variant inserts (ids are reassigned on insert). */\n build: () => ComponentNode\n}\n\n/** One component (or layout preset) in the blocks panel — ONE tile per component (EC-439 AD-1). */\nexport interface BlockTile {\n /** Stable identity: the component id, or `preset::<key>` for layout helpers. */\n key: string\n /** Component id of the node the tile inserts. */\n componentId: string\n title: string\n description?: string\n /**\n * The DEFAULT variant's validation problems — what clicking the tile\n * inserts. Per-variant issues live on {@link BlockTile.variants}.\n */\n issues: string[]\n /** The default variant's resolved preview URL (see {@link BlockVariant.previewUrl}). */\n previewUrl?: string\n /** All insertable variants; `variants[0]` is the default the tile inserts. */\n variants: BlockVariant[]\n /**\n * Fixture names the active query matched (stamped by {@link blockGroups}\n * when a query is present) — the panel highlights the variant affordance and\n * the matching flyout rows so \"found by fixture name\" is visible.\n */\n matchedVariants?: string[]\n /** Build the DEFAULT subtree to insert: the first declared fixture's, else the seeded manifest default. */\n build: () => ComponentNode\n}\n\n/** Tiles of one manifest `category`. */\nexport interface BlockGroup {\n category: string\n tiles: BlockTile[]\n}\n\nexport interface BlockFilter {\n /** Case-insensitive match over id, title, description, category, and fixture names. */\n query?: string\n /** When set, only these component ids may be inserted (slot.allow). */\n slotAllow?: string[]\n}\n\n/**\n * Intersect two component-id allow-lists. `undefined` means \"no constraint from\n * this source\", so the result is the other list (copied); two present lists yield\n * their set intersection, preserving the first list's order. This layers a\n * composition field's top-level `allow` vocabulary over a slot's positional\n * `allow` at the canvas root (EC-188): a component must satisfy BOTH leashes to be\n * offered. An empty array stays empty (\"allow nothing\"); callers wanting the\n * validator's \"empty = allow any\" must coalesce `[]` to `undefined` first (which\n * `resolveInsertContext` does via its `length > 0` guard, mirroring\n * `validateCompositionValue`).\n */\nexport function intersectAllow(\n slotAllow: readonly string[] | undefined,\n fieldAllow: readonly string[] | undefined,\n): string[] | undefined {\n if (!slotAllow) return fieldAllow ? [...fieldAllow] : undefined\n if (!fieldAllow) return [...slotAllow]\n const inField = new Set(fieldAllow)\n return slotAllow.filter((id) => inField.has(id))\n}\n\nconst UNCATEGORIZED = 'other'\n\n/** A node built from a manifest's defaults with an explicit id. */\nfunction defaultNode(manifest: ComponentManifest, id: string): ComponentNode {\n return { ...nodeFromManifest(manifest), id }\n}\n\n/** Structured issues for a component that cannot render (EC-055 statuses). */\nfunction manifestIssues(studio: StudioRegistry, manifest: ComponentManifest): string[] {\n const status = componentStatus(studio, manifest)\n if (status === 'incompatible') {\n return [`No implementation is registered for \"${manifest.id}\".`]\n }\n if (status === 'invalid') {\n return studio.registry.issues.filter((issue) => issue.componentId === manifest.id).map((issue) => issue.message)\n }\n return []\n}\n\n/**\n * Build the configured subtree a fixture describes: manifest defaults overlaid\n * with the fixture's props, plus one default-props child node per\n * `slotChildren` component id. Never throws — unknown components, undeclared\n * slots, and invalid prop values become entries in `issues` while the node is\n * still produced (the renderer shows its own visible fallbacks for them).\n */\nexport function fixtureSubtree(\n studio: StudioRegistry,\n manifest: ComponentManifest,\n fixture: ComponentFixture,\n baseId: string,\n): { node: ComponentNode; issues: string[] } {\n const issues: string[] = []\n const fixtureProps = fixture.props ?? {}\n\n for (const [name, value] of Object.entries(fixtureProps)) {\n const spec = manifest.props[name]\n if (!spec) {\n issues.push(`Fixture \"${fixture.name}\" sets unknown prop \"${name}\".`)\n continue\n }\n // EC-190: validate the fixture value with the prop's field-def validator.\n if (!(propValueValidator(manifest, name)?.(value) ?? true)) {\n issues.push(`Fixture \"${fixture.name}\" sets an invalid value for prop \"${name}\".`)\n }\n }\n\n // EC-547: fixture-built inserts are v2 block-sequence citizens like every\n // other factory node — the root carries the `elytra/component` discriminator.\n const node = componentBlockNode(nodeFromProps(manifest, fixtureProps, baseId))\n\n if (fixture.slotChildren) {\n // EC-386: blocks-prop regions are declared slots too (`manifestSlots`), so a\n // fixture may seed a Section-style body without a false \"undeclared slot\" issue.\n const declared = new Set(manifestSlots(manifest).map((slot) => slot.name))\n const slots: NonNullable<ComponentNode['slots']> = {}\n for (const [slotName, childIds] of Object.entries(fixture.slotChildren)) {\n if (!declared.has(slotName)) {\n issues.push(`Fixture \"${fixture.name}\" fills undeclared slot \"${slotName}\".`)\n }\n slots[slotName] = childIds.map((childId, index) => {\n const id = `${baseId}-${slotName}-${index}`\n const childManifest = studio.registry.getManifest(childId)\n if (!childManifest) {\n issues.push(`Fixture \"${fixture.name}\" references unknown component \"${childId}\" in slot \"${slotName}\".`)\n // Keep the reference: the renderer shows a visible missing-component fallback.\n return componentBlockNode({ id, componentId: childId })\n }\n return defaultNode(childManifest, id)\n })\n }\n if (Object.keys(slots).length > 0) node.slots = slots\n }\n\n return { node, issues }\n}\n\n/**\n * The ONE tile for a manifest (EC-439 AD-1): component identity plus one\n * variant per declared fixture — or a single implicit `Default` variant using\n * the seeded manifest default (a bare layout container is seeded so it\n * survives EC-161 normalization instead of vanishing on commit, EC-383). The\n * tile's own `issues`/`previewUrl`/`build` mirror the default variant\n * (`variants[0]`), so tile click/Enter/drag inserts exactly what the first\n * fixture tile inserted before the reshape.\n */\nexport function tileForManifest(\n studio: StudioRegistry,\n manifest: ComponentManifest,\n generatedPreview?: GeneratedPreviewLookup,\n): BlockTile {\n const title = localizedLabel(manifest.title, manifest.id)\n const baseIssues = manifestIssues(studio, manifest)\n // EC-409: a curated preview image previews the COMPONENT, not one fixture —\n // when declared it wins on the tile and on every variant row (AD-2 tier 1).\n const curated = manifest.previewImage\n\n const variants: BlockVariant[] =\n manifest.fixtures && manifest.fixtures.length > 0\n ? manifest.fixtures.map((fixture, index) => {\n const { issues } = fixtureSubtree(studio, manifest, fixture, `probe-${manifest.id}-${index}`)\n return {\n name: fixture.name,\n issues: [...baseIssues, ...issues],\n ...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, fixture.name)),\n build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,\n }\n })\n : [\n {\n name: DEFAULT_VARIANT,\n issues: baseIssues,\n ...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, DEFAULT_VARIANT)),\n build: () => seededNodeFromManifest(manifest),\n },\n ]\n\n const defaultVariant = variants[0]!\n return {\n key: manifest.id,\n componentId: manifest.id,\n title,\n description: manifest.description,\n issues: defaultVariant.issues,\n ...previewUrlEntry(defaultVariant.previewUrl),\n variants,\n build: defaultVariant.build,\n }\n}\n\n/** Spreadable `{ previewUrl }` entry — omitted (not `undefined`-valued) when absent. */\nfunction previewUrlEntry(url: string | undefined): { previewUrl?: string } {\n return url === undefined ? {} : { previewUrl: url }\n}\n\n/**\n * A two-letter monogram from a tile title — the deliberate icon+name\n * placeholder glyph (EC-439 AD-2 tier 3) for tiles with neither a curated nor\n * a generated preview image: first letters of the first two words, else the\n * first two letters of a single word. Deterministic and pure.\n */\nexport function tileMonogram(title: string): string {\n const words = title.trim().split(/\\s+/).filter(Boolean)\n if (words.length === 0) return '?'\n const glyph = words.length === 1 ? words[0]!.slice(0, 2) : `${words[0]!.charAt(0)}${words[1]!.charAt(0)}`\n return glyph.toUpperCase()\n}\n\nfunction matchedFixtureNames(manifest: ComponentManifest, query: string): string[] {\n return (manifest.fixtures ?? [])\n .map((fixture) => fixture.name)\n .filter((name) => name.toLowerCase().includes(query))\n}\n\nfunction matches(manifest: ComponentManifest, query: string): boolean {\n const hay = [manifest.id, ...localizedStringVariants(manifest.title), manifest.description, manifest.category]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n // EC-439: fixture names are searchable too — a content-seeded fixture name\n // (\"Startseite\") must find its component even when no manifest field matches.\n return hay.includes(query) || matchedFixtureNames(manifest, query).length > 0\n}\n\n/**\n * Derive the panel's category groups from the registry: filter by slot allow\n * list and query, derive ONE tile per manifest, group by `category`, and sort\n * categories alphabetically (registry order within a group). Deterministic.\n * When a query is active, each tile is stamped with the fixture names it\n * matched (see {@link BlockTile.matchedVariants}).\n */\nexport function blockGroups(\n studio: StudioRegistry,\n filter: BlockFilter = {},\n generatedPreview?: GeneratedPreviewLookup,\n): BlockGroup[] {\n const query = filter.query?.trim().toLowerCase()\n const allow = filter.slotAllow\n\n const groups = new Map<string, BlockTile[]>()\n for (const manifest of studio.registry.list()) {\n // EC-191: the canvas is client-rendered, so only canvas-eligible\n // (client-renderable) components are offered for insertion; server-only / RSC\n // components are usable in dev frames, not placed on the canvas.\n if (!isCanvasEligible(manifest)) continue\n if (allow && !allow.includes(manifest.id)) continue\n if (query && !matches(manifest, query)) continue\n const tile = tileForManifest(studio, manifest, generatedPreview)\n if (query) {\n const matched = matchedFixtureNames(manifest, query)\n if (matched.length > 0) tile.matchedVariants = matched\n }\n const category = manifest.category ?? UNCATEGORIZED\n const tiles = groups.get(category) ?? []\n tiles.push(tile)\n groups.set(category, tiles)\n }\n\n return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, tiles]) => ({ category, tiles }))\n}\n\n/**\n * Whether ANY component could be inserted under a slot allow-list (EC-405) — the\n * cheap existence check behind the canvas insertion affordances: a \"+\" only shows\n * at a position where the blocks panel would offer at least one tile. Mirrors the\n * exact filter {@link blockGroups} applies (canvas-eligible AND, when an allow-list\n * is present, on that list), short-circuiting on the first match instead of\n * building tiles; falls back to the layout-helper presets (which are also\n * insertable there). `undefined`/absent `slotAllow` means \"allow any\" → true when\n * the registry has any canvas-eligible component.\n */\nexport function hasInsertableComponent(studio: StudioRegistry, slotAllow?: string[]): boolean {\n for (const manifest of studio.registry.list()) {\n if (!isCanvasEligible(manifest)) continue\n if (slotAllow && !slotAllow.includes(manifest.id)) continue\n return true\n }\n return presetTiles(slotAllow).length > 0\n}\n\n/**\n * Layout helper presets (EC-067) as panel tiles — already one tile per preset.\n * A preset has a single implicit variant and never a preview image, so its\n * tile renders the placeholder treatment.\n */\nexport function presetTiles(slotAllow?: string[]): BlockTile[] {\n return LAYOUT_PRESETS.filter((preset) => !slotAllow || slotAllow.includes(preset.build().componentId)).map(\n (preset) => ({\n key: `preset::${preset.key}`,\n componentId: preset.build().componentId,\n title: preset.label,\n description: preset.description,\n issues: [],\n variants: [{ name: DEFAULT_VARIANT, issues: [], build: () => preset.build() }],\n build: () => preset.build(),\n }),\n )\n}\n"]}
|
|
@@ -94,7 +94,7 @@ export function attachCanvasInlineEditingListeners(doc, deps) {
|
|
|
94
94
|
// non-editable atoms and survive the session byte-exact. A pairing
|
|
95
95
|
// mismatch means the DOM diverged from the deterministic render —
|
|
96
96
|
// refuse to start rather than risk lossy parsing ("polished or absent").
|
|
97
|
-
const pairs = collectPreservedPairs(target.value.
|
|
97
|
+
const pairs = collectPreservedPairs({ type: 'doc', content: (target.value.content ?? []) }, host);
|
|
98
98
|
if (!pairs)
|
|
99
99
|
return;
|
|
100
100
|
const session = createInlineRichTextSession({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-inline-editing.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-inline-editing.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport {\n PRESERVE_NODE_ATTR,\n collectPreservedPairs,\n richTextDocFromDom,\n} from '@elytracms/core/rich-text'\nimport type { RtDomElement } from '@elytracms/core/rich-text'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { BuilderAction } from './builder-store'\nimport {\n createInlineEditSession,\n createInlineRichTextSession,\n findInlineTextHost,\n resolveInlineEditableProp,\n resolveInlineRichTextProp,\n} from './inline-edit'\nimport type { InlineEditSession, InlineRichTextProp } from './inline-edit'\nimport { wrapperAt } from './canvas-dom'\n\n/**\n * Inline editing on the canvas (EC-158 plain text, EC-159 rich text),\n * extracted from `BuilderCanvas` (EC-330): double-click on a node whose\n * manifest flags an inline-editable prop turns the *rendered element itself*\n * contenteditable — no overlay, no input swap, so typography and layout are\n * identical in edit and render states. A session finishes exactly once, and a\n * commit dispatches exactly one `setProp` action — one undo step. The graph\n * stays the single source of truth.\n *\n * Commit/cancel gestures: plain text commits on Enter or blur; rich text is\n * multiline, so plain Enter inserts a paragraph and the commit gesture is\n * Cmd/Ctrl+Enter or blur (Enter-to-commit would make multi-paragraph content\n * unauthorable in place). Esc always cancels, dispatching nothing.\n *\n * The listener wiring is framework-free\n * ({@link attachCanvasInlineEditingListeners}, the `clipboard-actions`\n * pattern) so the begin/commit/cancel state machine is unit-testable without\n * a DOM environment; {@link useCanvasInlineEditing} owns the React state the\n * canvas renders from (the live rich-text host, the remount epochs) and the\n * session ref other canvas listeners consult.\n */\n\n/** A live inline-editing session bound to its DOM host (EC-158/EC-159). */\nexport interface ActiveInlineEdit {\n nodeId: string\n host: HTMLElement\n /** Finish the session exactly once (later calls are no-ops). */\n finish: (mode: 'commit' | 'cancel') => void\n}\n\n/** The structural slice of the iframe document the editing wiring needs. */\nexport interface CanvasInlineEditingDocument {\n addEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n removeEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n getSelection(): Selection | null\n createRange(): Range\n}\n\nexport interface CanvasInlineEditingDeps {\n /** The current composition root (read live — listeners outlive renders). */\n currentRoot(): ComponentNode\n getManifest(componentId: string): ComponentManifest | undefined\n dispatch(action: BuilderAction): void\n /** The live session — imperative DOM state, mirrored for other listeners. */\n getEditing(): ActiveInlineEdit | null\n setEditing(edit: ActiveInlineEdit | null): void\n /** Mirror the rich-text host into render state (mounts the mark toolbar). */\n setRichEditHost(host: HTMLElement | null): void\n /**\n * A rich-text session mutates the rendered DOM behind React's back\n * (contenteditable + mark commands), so finishing one must REMOUNT the\n * node's subtree — bumping its epoch changes the decorate key, React\n * discards the edited DOM wholesale and re-renders from the graph (the\n * committed value, or the untouched original on cancel). Patching the old\n * fiber tree against editor-mutated DOM would leave stale artifacts.\n */\n bumpRichEditEpoch(nodeId: string): void\n}\n\n/**\n * The rendered `.rich-text` element inside a node's wrapper — the rich-text\n * editing host (EC-159). Never crosses into a nested node's wrapper.\n */\nfunction findRichTextHost(wrapper: HTMLElement): HTMLElement | null {\n const host = wrapper.querySelector('.rich-text') as HTMLElement | null\n return host && host.closest('[data-ec-node-id]') === wrapper ? host : null\n}\n\n/**\n * Wire the inline-editing dblclick listener onto the iframe document (native —\n * React's synthetic events don't cross frames). Returns the detach function;\n * detaching mid-session cancels it (never dispatch from cleanup).\n */\nexport function attachCanvasInlineEditingListeners(\n doc: CanvasInlineEditingDocument,\n deps: CanvasInlineEditingDeps,\n): () => void {\n const { dispatch } = deps\n\n // ---- plain text (EC-158) ------------------------------------------------\n const beginText = (wrapper: HTMLElement, host: HTMLElement, session: InlineEditSession) => {\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n const text = host.textContent ?? ''\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n if (mode === 'commit') {\n const outcome = session.commit(text)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value),\n })\n }\n } else {\n const outcome = session.cancel()\n if (outcome) host.textContent = outcome.value\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n } else if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n const onBlur = () => finish('commit')\n // `plaintext-only` keeps paste/typing plain; fall back where unsupported.\n host.setAttribute('contenteditable', 'plaintext-only')\n if (!host.isContentEditable) host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'text')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId: session.nodeId, host, finish })\n host.focus({ preventScroll: true })\n // Keep the gesture's native caret/word selection when it landed inside\n // the host; otherwise select the whole value.\n const sel = doc.getSelection()\n if (!sel || sel.rangeCount === 0 || !host.contains(sel.anchorNode)) {\n const range = doc.createRange()\n range.selectNodeContents(host)\n sel?.removeAllRanges()\n sel?.addRange(range)\n }\n }\n\n // ---- rich text (EC-159) -------------------------------------------------\n const beginRichText = (wrapper: HTMLElement, nodeId: string, target: InlineRichTextProp) => {\n const host = findRichTextHost(wrapper)\n if (!host) return\n // Pair the storage tree against the rendered DOM and stamp every\n // non-invertible node (custom blocks, unknown nodes, images, embeds)\n // with its exact JSON: they stay rendered *inside* the live editor as\n // non-editable atoms and survive the session byte-exact. A pairing\n // mismatch means the DOM diverged from the deterministic render —\n // refuse to start rather than risk lossy parsing (\"polished or absent\").\n const pairs = collectPreservedPairs(target.value.doc, host as unknown as RtDomElement)\n if (!pairs) return\n const session = createInlineRichTextSession({\n nodeId,\n propName: target.propName,\n original: target.value,\n })\n for (const pair of pairs) {\n const el = pair.element as unknown as HTMLElement\n el.setAttribute(PRESERVE_NODE_ATTR, JSON.stringify(pair.node))\n el.setAttribute('contenteditable', 'false')\n }\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n deps.setRichEditHost(null)\n // Parse the edited DOM, detach the session, then bump the node's\n // remount epoch: React discards the editor-mutated DOM and renders\n // fresh from the graph — committed content or, on cancel, the\n // untouched original. Never patch a fiber tree against DOM the\n // editing commands rewrote.\n const parsed = mode === 'commit' ? richTextDocFromDom(host as unknown as RtDomElement) : null\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n deps.bumpRichEditEpoch(nodeId)\n if (parsed) {\n // An unchanged document commits nothing: the stored value is never\n // rewritten, so untouched content round-trips byte-identical.\n const outcome = session.commit(parsed.doc)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value as unknown as JsonValue),\n })\n }\n } else {\n session.cancel()\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n }\n // Plain Enter falls through: native paragraph insertion (multiline).\n }\n const onBlur = (e: FocusEvent) => {\n // Interacting with the floating toolbar is part of the session.\n const next = e.relatedTarget as HTMLElement | null\n if (next?.closest?.('[data-ec-rt-toolbar]')) return\n finish('commit')\n }\n host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'rich')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId, host, finish })\n deps.setRichEditHost(host)\n host.focus({ preventScroll: true })\n }\n\n const onDblClick = (e: Event) => {\n const wrapper = wrapperAt(e.target)\n if (!wrapper) return\n const nodeId = wrapper.getAttribute('data-ec-node-id')!\n const editing = deps.getEditing()\n if (editing) {\n // Double-click inside the live editor is native word selection.\n if (editing.nodeId === nodeId) return\n editing.finish('commit')\n }\n const node = getNode(deps.currentRoot(), nodeId)\n if (!node) return\n const manifest = deps.getManifest(node.componentId)\n const target = resolveInlineEditableProp(manifest, node)\n if (target) {\n const host = findInlineTextHost(wrapper, target.value) as HTMLElement | null\n if (!host || typeof host.focus !== 'function') return\n dispatch({ type: 'select', id: nodeId })\n beginText(\n wrapper,\n host,\n createInlineEditSession({ nodeId, propName: target.propName, original: target.value }),\n )\n return\n }\n const richTarget = resolveInlineRichTextProp(manifest, node)\n if (!richTarget) return\n dispatch({ type: 'select', id: nodeId })\n beginRichText(wrapper, nodeId, richTarget)\n }\n\n doc.addEventListener('dblclick', onDblClick, true)\n return () => {\n doc.removeEventListener('dblclick', onDblClick, true)\n // Teardown mid-session (doc swap/unmount): cancel — never dispatch from cleanup.\n deps.getEditing()?.finish('cancel')\n }\n}\n\n/** What `useCanvasInlineEditing` hands back to the canvas. */\nexport interface CanvasInlineEditing {\n /**\n * The active session — imperative DOM state inside the iframe, not render\n * state. Other canvas listeners (selection clicks, DnD arming, clipboard\n * blocking) consult it live through this ref.\n */\n editingRef: RefObject<ActiveInlineEdit | null>\n /** The live rich-text host, mirrored into render state (mark toolbar mount). */\n richEditHost: HTMLElement | null\n /** Per-node remount epochs — decorate keys change when a rich session ends. */\n richEditEpochs: Record<string, number>\n /**\n * Reactive mirror of `editingRef` for renderers that must react to the\n * edit state (EC-404: the selection overlay suppresses its frame + chrome\n * while a text/rich-text session owns the node, so the `[data-ec-editing]`\n * outline alone marks it).\n */\n editing: boolean\n}\n\n/**\n * The React face of the canvas inline editing (EC-158/EC-159): owns the\n * session ref and the render state (`richEditHost`, `richEditEpochs`), wires\n * the dblclick listener when the iframe document exists, and cancels a live\n * session on teardown.\n */\nexport function useCanvasInlineEditing(\n doc: Document | null,\n deps: {\n /** The live composition root (`builder.present`, mirrored into a ref). */\n presentRef: RefObject<ComponentNode>\n dispatch: (action: BuilderAction) => void\n studio: StudioRegistry\n },\n): CanvasInlineEditing {\n const editingRef = useRef<ActiveInlineEdit | null>(null)\n const [editing, setEditing] = useState(false)\n const [richEditHost, setRichEditHost] = useState<HTMLElement | null>(null)\n const [richEditEpochs, setRichEditEpochs] = useState<Record<string, number>>({})\n const { presentRef, dispatch, studio } = deps\n\n useEffect(() => {\n if (!doc) return\n return attachCanvasInlineEditingListeners(doc, {\n currentRoot: () => presentRef.current,\n getManifest: (id) => studio.registry.getManifest(id),\n dispatch,\n getEditing: () => editingRef.current,\n setEditing: (edit) => {\n editingRef.current = edit\n setEditing(edit !== null)\n },\n setRichEditHost,\n bumpRichEditEpoch: (nodeId) =>\n setRichEditEpochs((prev) => ({ ...prev, [nodeId]: (prev[nodeId] ?? 0) + 1 })),\n })\n }, [doc, dispatch, studio, presentRef])\n\n return { editingRef, richEditHost, richEditEpochs, editing }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"canvas-inline-editing.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-inline-editing.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport {\n PRESERVE_NODE_ATTR,\n collectPreservedPairs,\n richTextDocFromDom,\n} from '@elytracms/core/rich-text'\nimport type { RtDomElement, RichTextNode } from '@elytracms/core/rich-text'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { BuilderAction } from './builder-store'\nimport {\n createInlineEditSession,\n createInlineRichTextSession,\n findInlineTextHost,\n resolveInlineEditableProp,\n resolveInlineRichTextProp,\n} from './inline-edit'\nimport type { InlineEditSession, InlineRichTextProp } from './inline-edit'\nimport { wrapperAt } from './canvas-dom'\n\n/**\n * Inline editing on the canvas (EC-158 plain text, EC-159 rich text),\n * extracted from `BuilderCanvas` (EC-330): double-click on a node whose\n * manifest flags an inline-editable prop turns the *rendered element itself*\n * contenteditable — no overlay, no input swap, so typography and layout are\n * identical in edit and render states. A session finishes exactly once, and a\n * commit dispatches exactly one `setProp` action — one undo step. The graph\n * stays the single source of truth.\n *\n * Commit/cancel gestures: plain text commits on Enter or blur; rich text is\n * multiline, so plain Enter inserts a paragraph and the commit gesture is\n * Cmd/Ctrl+Enter or blur (Enter-to-commit would make multi-paragraph content\n * unauthorable in place). Esc always cancels, dispatching nothing.\n *\n * The listener wiring is framework-free\n * ({@link attachCanvasInlineEditingListeners}, the `clipboard-actions`\n * pattern) so the begin/commit/cancel state machine is unit-testable without\n * a DOM environment; {@link useCanvasInlineEditing} owns the React state the\n * canvas renders from (the live rich-text host, the remount epochs) and the\n * session ref other canvas listeners consult.\n */\n\n/** A live inline-editing session bound to its DOM host (EC-158/EC-159). */\nexport interface ActiveInlineEdit {\n nodeId: string\n host: HTMLElement\n /** Finish the session exactly once (later calls are no-ops). */\n finish: (mode: 'commit' | 'cancel') => void\n}\n\n/** The structural slice of the iframe document the editing wiring needs. */\nexport interface CanvasInlineEditingDocument {\n addEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n removeEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n getSelection(): Selection | null\n createRange(): Range\n}\n\nexport interface CanvasInlineEditingDeps {\n /** The current composition root (read live — listeners outlive renders). */\n currentRoot(): ComponentNode\n getManifest(componentId: string): ComponentManifest | undefined\n dispatch(action: BuilderAction): void\n /** The live session — imperative DOM state, mirrored for other listeners. */\n getEditing(): ActiveInlineEdit | null\n setEditing(edit: ActiveInlineEdit | null): void\n /** Mirror the rich-text host into render state (mounts the mark toolbar). */\n setRichEditHost(host: HTMLElement | null): void\n /**\n * A rich-text session mutates the rendered DOM behind React's back\n * (contenteditable + mark commands), so finishing one must REMOUNT the\n * node's subtree — bumping its epoch changes the decorate key, React\n * discards the edited DOM wholesale and re-renders from the graph (the\n * committed value, or the untouched original on cancel). Patching the old\n * fiber tree against editor-mutated DOM would leave stale artifacts.\n */\n bumpRichEditEpoch(nodeId: string): void\n}\n\n/**\n * The rendered `.rich-text` element inside a node's wrapper — the rich-text\n * editing host (EC-159). Never crosses into a nested node's wrapper.\n */\nfunction findRichTextHost(wrapper: HTMLElement): HTMLElement | null {\n const host = wrapper.querySelector('.rich-text') as HTMLElement | null\n return host && host.closest('[data-ec-node-id]') === wrapper ? host : null\n}\n\n/**\n * Wire the inline-editing dblclick listener onto the iframe document (native —\n * React's synthetic events don't cross frames). Returns the detach function;\n * detaching mid-session cancels it (never dispatch from cleanup).\n */\nexport function attachCanvasInlineEditingListeners(\n doc: CanvasInlineEditingDocument,\n deps: CanvasInlineEditingDeps,\n): () => void {\n const { dispatch } = deps\n\n // ---- plain text (EC-158) ------------------------------------------------\n const beginText = (wrapper: HTMLElement, host: HTMLElement, session: InlineEditSession) => {\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n const text = host.textContent ?? ''\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n if (mode === 'commit') {\n const outcome = session.commit(text)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value),\n })\n }\n } else {\n const outcome = session.cancel()\n if (outcome) host.textContent = outcome.value\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n } else if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n const onBlur = () => finish('commit')\n // `plaintext-only` keeps paste/typing plain; fall back where unsupported.\n host.setAttribute('contenteditable', 'plaintext-only')\n if (!host.isContentEditable) host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'text')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId: session.nodeId, host, finish })\n host.focus({ preventScroll: true })\n // Keep the gesture's native caret/word selection when it landed inside\n // the host; otherwise select the whole value.\n const sel = doc.getSelection()\n if (!sel || sel.rangeCount === 0 || !host.contains(sel.anchorNode)) {\n const range = doc.createRange()\n range.selectNodeContents(host)\n sel?.removeAllRanges()\n sel?.addRange(range)\n }\n }\n\n // ---- rich text (EC-159) -------------------------------------------------\n const beginRichText = (wrapper: HTMLElement, nodeId: string, target: InlineRichTextProp) => {\n const host = findRichTextHost(wrapper)\n if (!host) return\n // Pair the storage tree against the rendered DOM and stamp every\n // non-invertible node (custom blocks, unknown nodes, images, embeds)\n // with its exact JSON: they stay rendered *inside* the live editor as\n // non-editable atoms and survive the session byte-exact. A pairing\n // mismatch means the DOM diverged from the deterministic render —\n // refuse to start rather than risk lossy parsing (\"polished or absent\").\n const pairs = collectPreservedPairs(\n { type: 'doc', content: (target.value.content ?? []) as RichTextNode[] },\n host as unknown as RtDomElement,\n )\n if (!pairs) return\n const session = createInlineRichTextSession({\n nodeId,\n propName: target.propName,\n original: target.value,\n })\n for (const pair of pairs) {\n const el = pair.element as unknown as HTMLElement\n el.setAttribute(PRESERVE_NODE_ATTR, JSON.stringify(pair.node))\n el.setAttribute('contenteditable', 'false')\n }\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n deps.setRichEditHost(null)\n // Parse the edited DOM, detach the session, then bump the node's\n // remount epoch: React discards the editor-mutated DOM and renders\n // fresh from the graph — committed content or, on cancel, the\n // untouched original. Never patch a fiber tree against DOM the\n // editing commands rewrote.\n const parsed = mode === 'commit' ? richTextDocFromDom(host as unknown as RtDomElement) : null\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n deps.bumpRichEditEpoch(nodeId)\n if (parsed) {\n // An unchanged document commits nothing: the stored value is never\n // rewritten, so untouched content round-trips byte-identical.\n const outcome = session.commit(parsed.doc)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value as unknown as JsonValue),\n })\n }\n } else {\n session.cancel()\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n }\n // Plain Enter falls through: native paragraph insertion (multiline).\n }\n const onBlur = (e: FocusEvent) => {\n // Interacting with the floating toolbar is part of the session.\n const next = e.relatedTarget as HTMLElement | null\n if (next?.closest?.('[data-ec-rt-toolbar]')) return\n finish('commit')\n }\n host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'rich')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId, host, finish })\n deps.setRichEditHost(host)\n host.focus({ preventScroll: true })\n }\n\n const onDblClick = (e: Event) => {\n const wrapper = wrapperAt(e.target)\n if (!wrapper) return\n const nodeId = wrapper.getAttribute('data-ec-node-id')!\n const editing = deps.getEditing()\n if (editing) {\n // Double-click inside the live editor is native word selection.\n if (editing.nodeId === nodeId) return\n editing.finish('commit')\n }\n const node = getNode(deps.currentRoot(), nodeId)\n if (!node) return\n const manifest = deps.getManifest(node.componentId)\n const target = resolveInlineEditableProp(manifest, node)\n if (target) {\n const host = findInlineTextHost(wrapper, target.value) as HTMLElement | null\n if (!host || typeof host.focus !== 'function') return\n dispatch({ type: 'select', id: nodeId })\n beginText(\n wrapper,\n host,\n createInlineEditSession({ nodeId, propName: target.propName, original: target.value }),\n )\n return\n }\n const richTarget = resolveInlineRichTextProp(manifest, node)\n if (!richTarget) return\n dispatch({ type: 'select', id: nodeId })\n beginRichText(wrapper, nodeId, richTarget)\n }\n\n doc.addEventListener('dblclick', onDblClick, true)\n return () => {\n doc.removeEventListener('dblclick', onDblClick, true)\n // Teardown mid-session (doc swap/unmount): cancel — never dispatch from cleanup.\n deps.getEditing()?.finish('cancel')\n }\n}\n\n/** What `useCanvasInlineEditing` hands back to the canvas. */\nexport interface CanvasInlineEditing {\n /**\n * The active session — imperative DOM state inside the iframe, not render\n * state. Other canvas listeners (selection clicks, DnD arming, clipboard\n * blocking) consult it live through this ref.\n */\n editingRef: RefObject<ActiveInlineEdit | null>\n /** The live rich-text host, mirrored into render state (mark toolbar mount). */\n richEditHost: HTMLElement | null\n /** Per-node remount epochs — decorate keys change when a rich session ends. */\n richEditEpochs: Record<string, number>\n /**\n * Reactive mirror of `editingRef` for renderers that must react to the\n * edit state (EC-404: the selection overlay suppresses its frame + chrome\n * while a text/rich-text session owns the node, so the `[data-ec-editing]`\n * outline alone marks it).\n */\n editing: boolean\n}\n\n/**\n * The React face of the canvas inline editing (EC-158/EC-159): owns the\n * session ref and the render state (`richEditHost`, `richEditEpochs`), wires\n * the dblclick listener when the iframe document exists, and cancels a live\n * session on teardown.\n */\nexport function useCanvasInlineEditing(\n doc: Document | null,\n deps: {\n /** The live composition root (`builder.present`, mirrored into a ref). */\n presentRef: RefObject<ComponentNode>\n dispatch: (action: BuilderAction) => void\n studio: StudioRegistry\n },\n): CanvasInlineEditing {\n const editingRef = useRef<ActiveInlineEdit | null>(null)\n const [editing, setEditing] = useState(false)\n const [richEditHost, setRichEditHost] = useState<HTMLElement | null>(null)\n const [richEditEpochs, setRichEditEpochs] = useState<Record<string, number>>({})\n const { presentRef, dispatch, studio } = deps\n\n useEffect(() => {\n if (!doc) return\n return attachCanvasInlineEditingListeners(doc, {\n currentRoot: () => presentRef.current,\n getManifest: (id) => studio.registry.getManifest(id),\n dispatch,\n getEditing: () => editingRef.current,\n setEditing: (edit) => {\n editingRef.current = edit\n setEditing(edit !== null)\n },\n setRichEditHost,\n bumpRichEditEpoch: (nodeId) =>\n setRichEditEpochs((prev) => ({ ...prev, [nodeId]: (prev[nodeId] ?? 0) + 1 })),\n })\n }, [doc, dispatch, studio, presentRef])\n\n return { editingRef, richEditHost, richEditEpochs, editing }\n}\n"]}
|
|
@@ -1,5 +1,53 @@
|
|
|
1
|
+
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
1
2
|
import type { BlocksFieldDef } from '@elytracms/core/cms-core';
|
|
3
|
+
import type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text';
|
|
2
4
|
import type { InspectorPane } from '../inspector/inspector-sidebar.js';
|
|
5
|
+
/**
|
|
6
|
+
* A sequence item the canvas does not natively edit (EC-547 / EC-500 D3):
|
|
7
|
+
* typography nodes, unknown/future node kinds, even entries the v2 schema would
|
|
8
|
+
* reject — the editor is not a validator, so they are carried BYTE-FOR-BYTE and
|
|
9
|
+
* re-merged on write-back rather than parsed, rewritten, or dropped.
|
|
10
|
+
* `componentPrefix` records how many component nodes preceded the item in the
|
|
11
|
+
* original sequence — the anchor {@link mergeCompositionValue} splices by.
|
|
12
|
+
*/
|
|
13
|
+
export interface PreservedBlockNode {
|
|
14
|
+
node: BlockNode;
|
|
15
|
+
componentPrefix: number;
|
|
16
|
+
}
|
|
17
|
+
/** A field value split for canvas editing: component roots + everything else. */
|
|
18
|
+
export interface SplitCompositionValue {
|
|
19
|
+
roots: ComponentNode[];
|
|
20
|
+
preserved: PreservedBlockNode[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Split a stored blocks-field value into the component nodes the canvas edits
|
|
24
|
+
* and the sequence items it must preserve (EC-547, the EC-500 round-trip law:
|
|
25
|
+
* a projection may only rewrite nodes it natively edits). Typography items are
|
|
26
|
+
* deliberately NOT surfaced as editable canvas blocks in v1 (EC-500 D3) — they
|
|
27
|
+
* survive the editing session and keep their order on write-back.
|
|
28
|
+
*/
|
|
29
|
+
export declare function splitCompositionValue(value: unknown): SplitCompositionValue;
|
|
30
|
+
/**
|
|
31
|
+
* Merge canvas-edited component roots back into the stored sequence (EC-547).
|
|
32
|
+
*
|
|
33
|
+
* The merge rule, exactly: each preserved item re-enters the sequence after
|
|
34
|
+
* `min(componentPrefix, roots.length)` component roots — it stays anchored to
|
|
35
|
+
* its ORDINAL position between components, not to any component's identity.
|
|
36
|
+
* Consequences, all deliberate:
|
|
37
|
+
* - no canvas edit ⇒ the original sequence is reproduced exactly;
|
|
38
|
+
* - reordering components on canvas reorders them WITHIN the component
|
|
39
|
+
* positions while preserved items stay put between those positions;
|
|
40
|
+
* - deleting components clamps now-out-of-range items to the sequence end;
|
|
41
|
+
* - a component appended on canvas takes the NEXT component position; a
|
|
42
|
+
* trailing typography item is anchored after all prior components, so the
|
|
43
|
+
* appended component lands after it — anchors hold, positions fill.
|
|
44
|
+
* Preserved items keep their relative order (their prefixes are non-decreasing
|
|
45
|
+
* by construction in {@link splitCompositionValue}).
|
|
46
|
+
*
|
|
47
|
+
* Every component root is passed through `componentBlockNode`, so the write is
|
|
48
|
+
* v2-native even when the roots were read from a legacy untagged value.
|
|
49
|
+
*/
|
|
50
|
+
export declare function mergeCompositionValue(roots: readonly ComponentNode[], preserved: readonly PreservedBlockNode[]): BlockSequenceValue;
|
|
3
51
|
export declare function CompositionFieldEditor({ field, value, onChange, readOnly, trailingPanes, }: {
|
|
4
52
|
field: BlocksFieldDef;
|
|
5
53
|
value: unknown;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react';
|
|
4
|
-
import { DEFAULT_SLOT,
|
|
4
|
+
import { DEFAULT_SLOT, isComponentBlockNode, isComponentNode } from '@elytracms/core/project-graph';
|
|
5
|
+
import { componentBlockNode, createBlockSequenceValue, isBlockSequenceValueShape } from '@elytracms/core/rich-text';
|
|
5
6
|
import { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui';
|
|
6
7
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
8
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
@@ -24,7 +25,15 @@ import { ReuseProvider } from './reuse-context.js';
|
|
|
24
25
|
* a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a
|
|
25
26
|
* no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`
|
|
26
27
|
* into the CMS document draft; the document's own Save persists it (the tree is
|
|
27
|
-
* stored as document data, `v.any()`).
|
|
28
|
+
* stored as document data, `v.any()`).
|
|
29
|
+
*
|
|
30
|
+
* EC-547 (EC-500 D1–D3): the stored value is the converged v2 block sequence
|
|
31
|
+
* `{ version: 2, content: BlockNode[] }`. The canvas edits the sequence's
|
|
32
|
+
* `elytra/component` nodes; typography nodes survive the session byte-for-byte
|
|
33
|
+
* and keep their order relative to components on write-back (see
|
|
34
|
+
* {@link splitCompositionValue} / {@link mergeCompositionValue}). Legacy bare
|
|
35
|
+
* node/array values are still ACCEPTED on read during the staging window, but
|
|
36
|
+
* every write emits v2. This reuses the real canvas + inspector +
|
|
28
37
|
* blocks panel unchanged — the same EC-158–164 mechanics — without touching the
|
|
29
38
|
* project-wide builder store. Surfaced on a dedicated "Canvas" fieldset tab in
|
|
30
39
|
* the CMS detail editor so the page builder gets full screen real estate rather
|
|
@@ -34,37 +43,100 @@ const SYNTH_ROOT_ID = 'composition.root';
|
|
|
34
43
|
/** The implicit root container the field's blocks compose inside (like a page root). */
|
|
35
44
|
const SYNTH_ROOT_COMPONENT = 'base.Stack';
|
|
36
45
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
46
|
+
* Split a stored blocks-field value into the component nodes the canvas edits
|
|
47
|
+
* and the sequence items it must preserve (EC-547, the EC-500 round-trip law:
|
|
48
|
+
* a projection may only rewrite nodes it natively edits). Typography items are
|
|
49
|
+
* deliberately NOT surfaced as editable canvas blocks in v1 (EC-500 D3) — they
|
|
50
|
+
* survive the editing session and keep their order on write-back.
|
|
51
|
+
*/
|
|
52
|
+
export function splitCompositionValue(value) {
|
|
53
|
+
if (isBlockSequenceValueShape(value)) {
|
|
54
|
+
const roots = [];
|
|
55
|
+
const preserved = [];
|
|
56
|
+
for (const item of value.content) {
|
|
57
|
+
if (isComponentBlockNode(item))
|
|
58
|
+
roots.push(item);
|
|
59
|
+
// Preservation over parsing: an item the schema would reject still rides
|
|
60
|
+
// along untouched (validation reports it; the canvas must not destroy it).
|
|
61
|
+
else
|
|
62
|
+
preserved.push({ node: item, componentPrefix: roots.length });
|
|
63
|
+
}
|
|
64
|
+
return { roots, preserved };
|
|
65
|
+
}
|
|
66
|
+
// EC-547 staging: dies at the tightening commit — legacy bare values (a
|
|
67
|
+
// single ComponentNode for cardinality one, an array for many) are still
|
|
68
|
+
// accepted on READ because fixtures haven't flipped; write-back is v2 always.
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return { roots: value.filter(isComponentNode), preserved: [] };
|
|
71
|
+
return { roots: isComponentNode(value) ? [value] : [], preserved: [] };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Merge canvas-edited component roots back into the stored sequence (EC-547).
|
|
75
|
+
*
|
|
76
|
+
* The merge rule, exactly: each preserved item re-enters the sequence after
|
|
77
|
+
* `min(componentPrefix, roots.length)` component roots — it stays anchored to
|
|
78
|
+
* its ORDINAL position between components, not to any component's identity.
|
|
79
|
+
* Consequences, all deliberate:
|
|
80
|
+
* - no canvas edit ⇒ the original sequence is reproduced exactly;
|
|
81
|
+
* - reordering components on canvas reorders them WITHIN the component
|
|
82
|
+
* positions while preserved items stay put between those positions;
|
|
83
|
+
* - deleting components clamps now-out-of-range items to the sequence end;
|
|
84
|
+
* - a component appended on canvas takes the NEXT component position; a
|
|
85
|
+
* trailing typography item is anchored after all prior components, so the
|
|
86
|
+
* appended component lands after it — anchors hold, positions fill.
|
|
87
|
+
* Preserved items keep their relative order (their prefixes are non-decreasing
|
|
88
|
+
* by construction in {@link splitCompositionValue}).
|
|
89
|
+
*
|
|
90
|
+
* Every component root is passed through `componentBlockNode`, so the write is
|
|
91
|
+
* v2-native even when the roots were read from a legacy untagged value.
|
|
92
|
+
*/
|
|
93
|
+
export function mergeCompositionValue(roots, preserved) {
|
|
94
|
+
const content = [];
|
|
95
|
+
let next = 0;
|
|
96
|
+
for (let position = 0; position <= roots.length; position++) {
|
|
97
|
+
while (next < preserved.length &&
|
|
98
|
+
Math.min(preserved[next].componentPrefix, roots.length) === position) {
|
|
99
|
+
content.push(preserved[next].node);
|
|
100
|
+
next += 1;
|
|
101
|
+
}
|
|
102
|
+
if (position < roots.length)
|
|
103
|
+
content.push(componentBlockNode(roots[position]));
|
|
104
|
+
}
|
|
105
|
+
return createBlockSequenceValue(content);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Wrap the field value's component roots in a synthetic root container (EC-287).
|
|
109
|
+
* The builder store edits this single root-bearing composition directly; its
|
|
110
|
+
* children are the field's component blocks, lifted back out via `onChange`.
|
|
40
111
|
*/
|
|
41
|
-
function buildSyntheticRoot(
|
|
112
|
+
function buildSyntheticRoot(roots) {
|
|
42
113
|
return {
|
|
43
114
|
id: SYNTH_ROOT_ID,
|
|
44
115
|
componentId: SYNTH_ROOT_COMPONENT,
|
|
45
|
-
slots: { [DEFAULT_SLOT]:
|
|
116
|
+
slots: { [DEFAULT_SLOT]: [...roots] },
|
|
46
117
|
};
|
|
47
118
|
}
|
|
48
|
-
/** Unwrap the synthetic root's children back to a field value per cardinality. */
|
|
49
|
-
function unwrapValue(blocks, multiple) {
|
|
50
|
-
return multiple ? [...blocks] : blocks[0];
|
|
51
|
-
}
|
|
52
119
|
export function CompositionFieldEditor({ field, value, onChange, readOnly, trailingPanes, }) {
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
120
|
+
// Split the value ONCE at mount (EC-547): component nodes become the canvas
|
|
121
|
+
// roots; every other sequence item is preserved aside for write-back. The
|
|
122
|
+
// editor is uncontrolled thereafter — it is the source of truth while open and
|
|
123
|
+
// lifts changes out via onChange; re-opening the tab remounts from the current
|
|
124
|
+
// draft. (BuilderProvider reads `composition` only in its lazy initial state,
|
|
125
|
+
// so later prop changes are ignored regardless.)
|
|
126
|
+
const initial = useMemo(() => splitCompositionValue(value), []);
|
|
127
|
+
const initialRoot = useMemo(() => buildSyntheticRoot(initial.roots), [initial]);
|
|
128
|
+
return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { preserved: initial.preserved, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(PropSyncProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, fieldAllow: field.allow, trailingPanes: trailingPanes }) }) })] }));
|
|
60
129
|
}
|
|
61
130
|
/**
|
|
62
131
|
* Lifts committed edits out of the scoped store into the CMS draft. The first
|
|
63
132
|
* render establishes the (container-normalized) baseline so simply opening the
|
|
64
133
|
* tab never marks the document dirty; every subsequent change to the block list
|
|
65
|
-
* emits the
|
|
134
|
+
* emits the merged v2 sequence value (EC-547) — the canvas roots spliced back
|
|
135
|
+
* between the preserved typography items. Cardinality (`multiple`) no longer
|
|
136
|
+
* shapes the stored value: it is a validation constraint over the one envelope
|
|
137
|
+
* (EC-500 D2), so the bare-object special case for `multiple: false` is gone.
|
|
66
138
|
*/
|
|
67
|
-
function CompositionLift({
|
|
139
|
+
function CompositionLift({ preserved, onChange, readOnly, }) {
|
|
68
140
|
const builder = useBuilder();
|
|
69
141
|
const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS;
|
|
70
142
|
const onChangeRef = useRef(onChange);
|
|
@@ -85,9 +157,9 @@ function CompositionLift({ multiple, onChange, readOnly, }) {
|
|
|
85
157
|
}
|
|
86
158
|
if (blocks !== baselineRef.current) {
|
|
87
159
|
baselineRef.current = blocks;
|
|
88
|
-
onChangeRef.current(
|
|
160
|
+
onChangeRef.current(mergeCompositionValue(blocks, preserved));
|
|
89
161
|
}
|
|
90
|
-
}, [blocks,
|
|
162
|
+
}, [blocks, preserved, readOnly]);
|
|
91
163
|
return null;
|
|
92
164
|
}
|
|
93
165
|
const EMPTY_BLOCKS = [];
|