@elytracms/studio 0.0.58 → 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.
Files changed (75) hide show
  1. package/dist/components/app-views/studio-config.d.ts +34 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/builder/blocks-model.js +12 -7
  4. package/dist/components/builder/blocks-model.js.map +1 -1
  5. package/dist/components/builder/canvas-chrome.d.ts +65 -0
  6. package/dist/components/builder/canvas-chrome.js +125 -0
  7. package/dist/components/builder/canvas-chrome.js.map +1 -0
  8. package/dist/components/builder/canvas-dnd.d.ts +59 -0
  9. package/dist/components/builder/canvas-dnd.js +207 -0
  10. package/dist/components/builder/canvas-dnd.js.map +1 -0
  11. package/dist/components/builder/canvas-dom.d.ts +11 -0
  12. package/dist/components/builder/canvas-dom.js +17 -0
  13. package/dist/components/builder/canvas-dom.js.map +1 -0
  14. package/dist/components/builder/canvas-inline-editing.d.ts +91 -0
  15. package/dist/components/builder/canvas-inline-editing.js +242 -0
  16. package/dist/components/builder/canvas-inline-editing.js.map +1 -0
  17. package/dist/components/builder/canvas-render-context.d.ts +83 -0
  18. package/dist/components/builder/canvas-render-context.js +196 -0
  19. package/dist/components/builder/canvas-render-context.js.map +1 -0
  20. package/dist/components/builder/canvas.d.ts +4 -1
  21. package/dist/components/builder/canvas.js +44 -657
  22. package/dist/components/builder/canvas.js.map +1 -1
  23. package/dist/components/builder/composition-field-editor.js +14 -4
  24. package/dist/components/builder/composition-field-editor.js.map +1 -1
  25. package/dist/components/builder/insert-context.d.ts +5 -2
  26. package/dist/components/builder/insert-context.js +1 -3
  27. package/dist/components/builder/insert-context.js.map +1 -1
  28. package/dist/components/builder/node-factory.d.ts +12 -0
  29. package/dist/components/builder/node-factory.js +18 -1
  30. package/dist/components/builder/node-factory.js.map +1 -1
  31. package/dist/components/cms/asset-field.d.ts +22 -0
  32. package/dist/components/cms/asset-field.js +300 -0
  33. package/dist/components/cms/asset-field.js.map +1 -0
  34. package/dist/components/cms/field-control-lib.d.ts +43 -0
  35. package/dist/components/cms/field-control-lib.js +98 -0
  36. package/dist/components/cms/field-control-lib.js.map +1 -0
  37. package/dist/components/cms/field-control.d.ts +7 -35
  38. package/dist/components/cms/field-control.js +13 -556
  39. package/dist/components/cms/field-control.js.map +1 -1
  40. package/dist/components/cms/object-array-field.d.ts +29 -0
  41. package/dist/components/cms/object-array-field.js +205 -0
  42. package/dist/components/cms/object-array-field.js.map +1 -0
  43. package/dist/components/cms/prop-fields.d.ts +15 -0
  44. package/dist/components/cms/prop-fields.js +38 -1
  45. package/dist/components/cms/prop-fields.js.map +1 -1
  46. package/dist/components/cms/rich-text/editor.d.ts +8 -0
  47. package/dist/components/cms/rich-text/editor.js +46 -3
  48. package/dist/components/cms/rich-text/editor.js.map +1 -1
  49. package/dist/components/cms/rich-text/toolbar.d.ts +5 -1
  50. package/dist/components/cms/rich-text/toolbar.js +4 -4
  51. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  52. package/dist/components/project-session.d.ts +50 -1
  53. package/dist/components/project-session.js +75 -14
  54. package/dist/components/project-session.js.map +1 -1
  55. package/dist/components/use-hydration-phase.d.ts +6 -4
  56. package/dist/components/use-hydration-phase.js.map +1 -1
  57. package/dist/customization.d.ts +1 -1
  58. package/dist/customization.js.map +1 -1
  59. package/dist/lib/data/collection-content.d.ts +12 -7
  60. package/dist/lib/data/collection-content.js +6 -4
  61. package/dist/lib/data/collection-content.js.map +1 -1
  62. package/dist/lib/i18n/messages.en.js +5 -0
  63. package/dist/lib/i18n/messages.en.js.map +1 -1
  64. package/dist/lib/operations/session-hydration.d.ts +4 -1
  65. package/dist/lib/operations/session-hydration.js +4 -1
  66. package/dist/lib/operations/session-hydration.js.map +1 -1
  67. package/dist/lib/persistence/index.js +13 -1
  68. package/dist/lib/persistence/index.js.map +1 -1
  69. package/dist/lib/workspace/index.d.ts +9 -1
  70. package/dist/lib/workspace/index.js +11 -1
  71. package/dist/lib/workspace/index.js.map +1 -1
  72. package/dist/lib/workspace/workspace-source.d.ts +17 -22
  73. package/dist/lib/workspace/workspace-source.js +1 -49
  74. package/dist/lib/workspace/workspace-source.js.map +1 -1
  75. 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":"AA0EA;;;;;;;;;;;;;;;;;;;;;;;;;;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\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"]}
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"]}
@@ -1,8 +1,8 @@
1
- import { cloneWithNewIds } from '@elytracms/core/project-graph';
1
+ import { cloneWithNewIds, isLayoutContainerId } from '@elytracms/core/project-graph';
2
2
  import { isCanvasEligible, propValueValidator } from '@elytracms/core/component-registry';
3
3
  import { componentStatus } from '@elytracms/studio/lib/registry';
4
4
  import { nodeFromProps, previewSpecs } from '@elytracms/studio/lib/registry/preview';
5
- import { LAYOUT_PRESETS, nodeFromManifest } from './node-factory.js';
5
+ import { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory.js';
6
6
  /**
7
7
  * Intersect two component-id allow-lists. `undefined` means "no constraint from
8
8
  * this source", so the result is the other list (copied); two present lists yield
@@ -97,9 +97,14 @@ export function tilesForManifest(studio, manifest) {
97
97
  const title = manifest.title ?? manifest.id;
98
98
  const baseIssues = manifestIssues(studio, manifest);
99
99
  if (!manifest.fixtures || manifest.fixtures.length === 0) {
100
- // No declared fixtures: preview via the shared preview specs (authored
101
- // primitive previews or plain defaults), insert the bare manifest defaults.
102
- const preview = previewSpecs(manifest)[0];
100
+ // No declared fixtures: insert the manifest defaults, seeding layout
101
+ // containers so a bare Stack/Grid survives EC-161 normalization instead of
102
+ // vanishing on commit (EC-383). Preview from the seeded node for containers
103
+ // (WYSIWYG with the insert) and from the shared preview specs otherwise
104
+ // (authored primitive previews or plain defaults).
105
+ const previewNode = isLayoutContainerId(manifest.id)
106
+ ? cloneWithNewIds(seededNodeFromManifest(manifest), prefixedCounter(`preview-${manifest.id}`))
107
+ : previewSpecs(manifest)[0].node;
103
108
  return [
104
109
  {
105
110
  key: `${manifest.id}::default`,
@@ -108,8 +113,8 @@ export function tilesForManifest(studio, manifest) {
108
113
  variant: 'Default',
109
114
  description: manifest.description,
110
115
  issues: baseIssues,
111
- previewNode: preview.node,
112
- build: () => nodeFromManifest(manifest),
116
+ previewNode,
117
+ build: () => seededNodeFromManifest(manifest),
113
118
  },
114
119
  ];
115
120
  }
@@ -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 { cloneWithNewIds } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry'\nimport { isCanvasEligible, propValueValidator } from '@elytracms/core/component-registry'\nimport { componentStatus } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { nodeFromProps, previewSpecs } from '@elytracms/studio/lib/registry/preview'\nimport { LAYOUT_PRESETS, nodeFromManifest } from './node-factory'\n\n/**\n * Blocks panel model (EC-164) — the pure, React-free derivation behind the\n * visual insert surface. Every registry manifest yields one tile per declared\n * fixture (or a single default-props tile), grouped by manifest `category`\n * and filtered by a free-text query over name / description / category — no\n * hand-maintained panel config. Broken fixtures degrade to tiles carrying\n * explicit `issues`, never to a missing entry or a crash.\n */\n\n/** One insertable tile in the blocks panel. */\nexport interface BlockTile {\n /** Stable identity (`<componentId>::<variant>` or `preset::<key>`). */\n key: string\n /** Component id of the node the tile inserts. */\n componentId: string\n title: string\n /** Variation label: the fixture name, `Default`, or a preset description. */\n variant: string\n description?: string\n /**\n * Validation problems detected while building the preview (unknown child\n * component, undeclared slot, invalid fixture prop, missing\n * implementation). A non-empty list marks the tile as degraded — it still\n * renders and still inserts.\n */\n issues: string[]\n /** Deterministic-id subtree rendered live in the tile thumbnail. */\n previewNode: ComponentNode\n /** Build the subtree to insert (ids are reassigned by the builder store). */\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, and category. */\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\nfunction prefixedCounter(prefix: string): () => string {\n let n = 0\n return () => {\n n += 1\n return `${prefix}-${n}`\n }\n}\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\n .filter((issue) => issue.componentId === manifest.id)\n .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 const declared = new Set(manifest.slots.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(\n `Fixture \"${fixture.name}\" references unknown component \"${childId}\" in slot \"${slotName}\".`,\n )\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/** All tiles for one manifest: one per fixture, else a single default tile. */\nexport function tilesForManifest(studio: StudioRegistry, manifest: ComponentManifest): BlockTile[] {\n const title = manifest.title ?? manifest.id\n const baseIssues = manifestIssues(studio, manifest)\n\n if (!manifest.fixtures || manifest.fixtures.length === 0) {\n // No declared fixtures: preview via the shared preview specs (authored\n // primitive previews or plain defaults), insert the bare manifest defaults.\n const preview = previewSpecs(manifest)[0]!\n return [\n {\n key: `${manifest.id}::default`,\n componentId: manifest.id,\n title,\n variant: 'Default',\n description: manifest.description,\n issues: baseIssues,\n previewNode: preview.node,\n build: () => nodeFromManifest(manifest),\n },\n ]\n }\n\n return manifest.fixtures.map((fixture, index) => {\n const { node, issues } = fixtureSubtree(\n studio,\n manifest,\n fixture,\n `preview-${manifest.id}-${index}`,\n )\n return {\n key: `${manifest.id}::${fixture.name}`,\n componentId: manifest.id,\n title,\n variant: fixture.name,\n description: manifest.description,\n issues: [...baseIssues, ...issues],\n previewNode: node,\n build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,\n }\n })\n}\n\nfunction matches(manifest: ComponentManifest, query: string): boolean {\n const hay = [manifest.id, manifest.title, manifest.description, manifest.category]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n return hay.includes(query)\n}\n\n/**\n * Derive the panel's category groups from the registry: filter by slot allow\n * list and query, expand manifests into tiles, group by `category`, and sort\n * categories alphabetically (registry order within a group). Deterministic.\n */\nexport function blockGroups(studio: StudioRegistry, filter: BlockFilter = {}): 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 category = manifest.category ?? UNCATEGORIZED\n const tiles = groups.get(category) ?? []\n tiles.push(...tilesForManifest(studio, manifest))\n groups.set(category, tiles)\n }\n\n return [...groups.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([category, tiles]) => ({ category, tiles }))\n}\n\n/**\n * Layout helper presets (EC-067) as panel tiles. Preview nodes are re-id'd\n * deterministically so sibling React keys stay unique.\n */\nexport function presetTiles(slotAllow?: string[]): BlockTile[] {\n return LAYOUT_PRESETS.filter(\n (preset) => !slotAllow || slotAllow.includes(preset.build().componentId),\n ).map((preset) => ({\n key: `preset::${preset.key}`,\n componentId: preset.build().componentId,\n title: preset.label,\n variant: preset.description,\n description: preset.description,\n issues: [],\n previewNode: cloneWithNewIds(preset.build(), prefixedCounter(`preview-preset-${preset.key}`)),\n build: () => preset.build(),\n }))\n}\n"]}
1
+ {"version":3,"file":"blocks-model.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAA;AAGpF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAA;AACzF,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAEhE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AACpF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AA+CzF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAwC,EACxC,UAAyC;IAEzC,IAAI,CAAC,SAAS;QAAE,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC/D,IAAI,CAAC,UAAU;QAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAA;IACtC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;IACnC,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;AAClD,CAAC;AAED,MAAM,aAAa,GAAG,OAAO,CAAA;AAE7B,SAAS,eAAe,CAAC,MAAc;IACrC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,OAAO,GAAG,EAAE;QACV,CAAC,IAAI,CAAC,CAAA;QACN,OAAO,GAAG,MAAM,IAAI,CAAC,EAAE,CAAA;IACzB,CAAC,CAAA;AACH,CAAC;AAED,mEAAmE;AACnE,SAAS,WAAW,CAAC,QAA2B,EAAE,EAAU;IAC1D,OAAO,EAAE,GAAG,gBAAgB,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAA;AAC9C,CAAC;AAED,8EAA8E;AAC9E,SAAS,cAAc,CAAC,MAAsB,EAAE,QAA2B;IACzE,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAChD,IAAI,MAAM,KAAK,cAAc,EAAE,CAAC;QAC9B,OAAO,CAAC,wCAAwC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAA;IAClE,CAAC;IACD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM;aAC1B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,KAAK,QAAQ,CAAC,EAAE,CAAC;aACpD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAsB,EACtB,QAA2B,EAC3B,OAAyB,EACzB,MAAc;IAEd,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAA;IAExC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,IAAI,wBAAwB,IAAI,IAAI,CAAC,CAAA;YACrE,SAAQ;QACV,CAAC;QACD,0EAA0E;QAC1E,IAAI,CAAC,CAAC,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC;YAC3D,MAAM,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,IAAI,qCAAqC,IAAI,IAAI,CAAC,CAAA;QACpF,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,CAAC,CAAA;IAE1D,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACjE,MAAM,KAAK,GAAwC,EAAE,CAAA;QACrD,KAAK,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YACxE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5B,MAAM,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,IAAI,4BAA4B,QAAQ,IAAI,CAAC,CAAA;YAC/E,CAAC;YACD,KAAK,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;gBAChD,MAAM,EAAE,GAAG,GAAG,MAAM,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAA;gBAC3C,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;gBAC1D,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,MAAM,CAAC,IAAI,CACT,YAAY,OAAO,CAAC,IAAI,mCAAmC,OAAO,cAAc,QAAQ,IAAI,CAC7F,CAAA;oBACD,+EAA+E;oBAC/E,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,CAAA;gBACrC,CAAC;gBACD,OAAO,WAAW,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;YACvC,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACvD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;AACzB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,MAAsB,EAAE,QAA2B;IAClF,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,EAAE,CAAA;IAC3C,MAAM,UAAU,GAAG,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAEnD,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzD,qEAAqE;QACrE,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,mDAAmD;QACnD,MAAM,WAAW,GAAG,mBAAmB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClD,CAAC,CAAC,eAAe,CAAC,sBAAsB,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC,WAAW,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9F,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,CAAA;QACnC,OAAO;YACL;gBACE,GAAG,EAAE,GAAG,QAAQ,CAAC,EAAE,WAAW;gBAC9B,WAAW,EAAE,QAAQ,CAAC,EAAE;gBACxB,KAAK;gBACL,OAAO,EAAE,SAAS;gBAClB,WAAW,EAAE,QAAQ,CAAC,WAAW;gBACjC,MAAM,EAAE,UAAU;gBAClB,WAAW;gBACX,KAAK,EAAE,GAAG,EAAE,CAAC,sBAAsB,CAAC,QAAQ,CAAC;aAC9C;SACF,CAAA;IACH,CAAC;IAED,OAAO,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QAC9C,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,cAAc,CACrC,MAAM,EACN,QAAQ,EACR,OAAO,EACP,WAAW,QAAQ,CAAC,EAAE,IAAI,KAAK,EAAE,CAClC,CAAA;QACD,OAAO;YACL,GAAG,EAAE,GAAG,QAAQ,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,EAAE;YACtC,WAAW,EAAE,QAAQ,CAAC,EAAE;YACxB,KAAK;YACL,OAAO,EAAE,OAAO,CAAC,IAAI;YACrB,WAAW,EAAE,QAAQ,CAAC,WAAW;YACjC,MAAM,EAAE,CAAC,GAAG,UAAU,EAAE,GAAG,MAAM,CAAC;YAClC,WAAW,EAAE,IAAI;YACjB,KAAK,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,IAAI;SACnE,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,QAA2B,EAAE,KAAa;IACzD,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC;SAC/E,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC;SACT,WAAW,EAAE,CAAA;IAChB,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,MAAsB,EAAE,SAAsB,EAAE;IAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAChD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAA;IAE9B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC7C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9C,iEAAiE;QACjE,8EAA8E;QAC9E,iEAAiE;QACjE,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC;YAAE,SAAQ;QACzC,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,SAAQ;QACnD,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC;YAAE,SAAQ;QAChD,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,IAAI,aAAa,CAAA;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;QACxC,KAAK,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;QACjD,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC7B,CAAC;IAED,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;AACtD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAoB;IAC9C,OAAO,cAAc,CAAC,MAAM,CAC1B,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,CACzE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjB,GAAG,EAAE,WAAW,MAAM,CAAC,GAAG,EAAE;QAC5B,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,WAAW;QACvC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,MAAM,CAAC,WAAW;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,MAAM,EAAE,EAAE;QACV,WAAW,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,eAAe,CAAC,kBAAkB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7F,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;KAC5B,CAAC,CAAC,CAAA;AACL,CAAC","sourcesContent":["import { cloneWithNewIds, isLayoutContainerId } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry'\nimport { isCanvasEligible, propValueValidator } from '@elytracms/core/component-registry'\nimport { componentStatus } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { nodeFromProps, previewSpecs } from '@elytracms/studio/lib/registry/preview'\nimport { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory'\n\n/**\n * Blocks panel model (EC-164) — the pure, React-free derivation behind the\n * visual insert surface. Every registry manifest yields one tile per declared\n * fixture (or a single default-props tile), grouped by manifest `category`\n * and filtered by a free-text query over name / description / category — no\n * hand-maintained panel config. Broken fixtures degrade to tiles carrying\n * explicit `issues`, never to a missing entry or a crash.\n */\n\n/** One insertable tile in the blocks panel. */\nexport interface BlockTile {\n /** Stable identity (`<componentId>::<variant>` or `preset::<key>`). */\n key: string\n /** Component id of the node the tile inserts. */\n componentId: string\n title: string\n /** Variation label: the fixture name, `Default`, or a preset description. */\n variant: string\n description?: string\n /**\n * Validation problems detected while building the preview (unknown child\n * component, undeclared slot, invalid fixture prop, missing\n * implementation). A non-empty list marks the tile as degraded — it still\n * renders and still inserts.\n */\n issues: string[]\n /** Deterministic-id subtree rendered live in the tile thumbnail. */\n previewNode: ComponentNode\n /** Build the subtree to insert (ids are reassigned by the builder store). */\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, and category. */\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\nfunction prefixedCounter(prefix: string): () => string {\n let n = 0\n return () => {\n n += 1\n return `${prefix}-${n}`\n }\n}\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\n .filter((issue) => issue.componentId === manifest.id)\n .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 const declared = new Set(manifest.slots.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(\n `Fixture \"${fixture.name}\" references unknown component \"${childId}\" in slot \"${slotName}\".`,\n )\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/** All tiles for one manifest: one per fixture, else a single default tile. */\nexport function tilesForManifest(studio: StudioRegistry, manifest: ComponentManifest): BlockTile[] {\n const title = manifest.title ?? manifest.id\n const baseIssues = manifestIssues(studio, manifest)\n\n if (!manifest.fixtures || manifest.fixtures.length === 0) {\n // No declared fixtures: insert the manifest defaults, seeding layout\n // containers so a bare Stack/Grid survives EC-161 normalization instead of\n // vanishing on commit (EC-383). Preview from the seeded node for containers\n // (WYSIWYG with the insert) and from the shared preview specs otherwise\n // (authored primitive previews or plain defaults).\n const previewNode = isLayoutContainerId(manifest.id)\n ? cloneWithNewIds(seededNodeFromManifest(manifest), prefixedCounter(`preview-${manifest.id}`))\n : previewSpecs(manifest)[0]!.node\n return [\n {\n key: `${manifest.id}::default`,\n componentId: manifest.id,\n title,\n variant: 'Default',\n description: manifest.description,\n issues: baseIssues,\n previewNode,\n build: () => seededNodeFromManifest(manifest),\n },\n ]\n }\n\n return manifest.fixtures.map((fixture, index) => {\n const { node, issues } = fixtureSubtree(\n studio,\n manifest,\n fixture,\n `preview-${manifest.id}-${index}`,\n )\n return {\n key: `${manifest.id}::${fixture.name}`,\n componentId: manifest.id,\n title,\n variant: fixture.name,\n description: manifest.description,\n issues: [...baseIssues, ...issues],\n previewNode: node,\n build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,\n }\n })\n}\n\nfunction matches(manifest: ComponentManifest, query: string): boolean {\n const hay = [manifest.id, manifest.title, manifest.description, manifest.category]\n .filter(Boolean)\n .join(' ')\n .toLowerCase()\n return hay.includes(query)\n}\n\n/**\n * Derive the panel's category groups from the registry: filter by slot allow\n * list and query, expand manifests into tiles, group by `category`, and sort\n * categories alphabetically (registry order within a group). Deterministic.\n */\nexport function blockGroups(studio: StudioRegistry, filter: BlockFilter = {}): 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 category = manifest.category ?? UNCATEGORIZED\n const tiles = groups.get(category) ?? []\n tiles.push(...tilesForManifest(studio, manifest))\n groups.set(category, tiles)\n }\n\n return [...groups.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([category, tiles]) => ({ category, tiles }))\n}\n\n/**\n * Layout helper presets (EC-067) as panel tiles. Preview nodes are re-id'd\n * deterministically so sibling React keys stay unique.\n */\nexport function presetTiles(slotAllow?: string[]): BlockTile[] {\n return LAYOUT_PRESETS.filter(\n (preset) => !slotAllow || slotAllow.includes(preset.build().componentId),\n ).map((preset) => ({\n key: `preset::${preset.key}`,\n componentId: preset.build().componentId,\n title: preset.label,\n variant: preset.description,\n description: preset.description,\n issues: [],\n previewNode: cloneWithNewIds(preset.build(), prefixedCounter(`preview-preset-${preset.key}`)),\n build: () => preset.build(),\n }))\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"]}
@@ -0,0 +1,59 @@
1
+ import type { RefObject } from 'react';
2
+ import type { ComponentNode } from '@elytracms/core/project-graph';
3
+ import type { ManifestRules } from '@elytracms/studio/lib/dnd/drop-rules';
4
+ import type { StudioRegistry } from '@elytracms/studio/lib/registry';
5
+ import type { BuilderAction } from './builder-store.js';
6
+ import type { ActiveInlineEdit } from './canvas-inline-editing.js';
7
+ /**
8
+ * Drag & drop on the canvas (EC-160), extracted from `BuilderCanvas` (EC-330).
9
+ * Eligibility is the pure shared rule set (`lib/dnd/drop-rules`) — the same
10
+ * functions the page tree consumes — evaluated live during `dragover`; the
11
+ * wrapper under the pointer shows the resolution (`data-ec-drop`:
12
+ * into/before/after, or `blocked` for a visible rejection). A completed drop
13
+ * dispatches exactly one builder action (move or insert) → one operations
14
+ * command → one undo step. Ineligible targets never `preventDefault`, so
15
+ * releasing over one is a native no-op.
16
+ *
17
+ * The listener wiring is framework-free ({@link attachCanvasDndListeners},
18
+ * the `clipboard-actions` pattern) so the whole gesture state machine —
19
+ * arm-on-mousedown, live indicator, plan resolution, one-dispatch drop — is
20
+ * unit-testable without a DOM environment; {@link useCanvasDnd} only owns the
21
+ * React effect lifecycle.
22
+ */
23
+ /** The structural slice of the iframe document the DnD wiring needs. */
24
+ export interface CanvasDndDocument {
25
+ addEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void;
26
+ removeEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void;
27
+ querySelector(selectors: string): Element | null;
28
+ }
29
+ export interface CanvasDndDeps {
30
+ /** The current composition root (read live — listeners outlive renders). */
31
+ currentRoot(): ComponentNode;
32
+ /** Manifest lookup feeding the pure drop rules. */
33
+ getManifest(componentId: string): ManifestRules | undefined;
34
+ dispatch(action: BuilderAction): void;
35
+ /**
36
+ * Whether `target` sits inside the live inline editor (EC-158): a press
37
+ * there must keep native caret clicks/text selection, never start a node drag.
38
+ */
39
+ isEditingTarget(target: EventTarget | null): boolean;
40
+ }
41
+ /**
42
+ * Wire the canvas drag-and-drop listeners onto the iframe document (native —
43
+ * React's synthetic events don't cross frames). Returns the detach function.
44
+ */
45
+ export declare function attachCanvasDndListeners(doc: CanvasDndDocument, deps: CanvasDndDeps): () => void;
46
+ /**
47
+ * The React face of the canvas DnD wiring: attach on the iframe document once
48
+ * it exists, detach on doc swap/unmount. State lives in refs on purpose —
49
+ * listeners outlive renders and must read the *current* graph and editing
50
+ * session without re-subscribing on every edit.
51
+ */
52
+ export declare function useCanvasDnd(doc: Document | null, deps: {
53
+ /** The live composition root (`builder.present`, mirrored into a ref). */
54
+ presentRef: RefObject<ComponentNode>;
55
+ dispatch: (action: BuilderAction) => void;
56
+ studio: StudioRegistry;
57
+ /** The live inline-edit session (owned by `useCanvasInlineEditing`). */
58
+ editingRef: RefObject<ActiveInlineEdit | null>;
59
+ }): void;