@elytracms/studio 0.0.69 → 0.0.70

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 (35) hide show
  1. package/dist/components/app-views/studio-config.d.ts +22 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/builder/blocks-model.d.ts +91 -29
  4. package/dist/components/builder/blocks-model.js +83 -54
  5. package/dist/components/builder/blocks-model.js.map +1 -1
  6. package/dist/components/builder/blocks-panel.d.ts +40 -12
  7. package/dist/components/builder/blocks-panel.js +88 -34
  8. package/dist/components/builder/blocks-panel.js.map +1 -1
  9. package/dist/components/builder/fixture-thumbnail.d.ts +13 -23
  10. package/dist/components/builder/fixture-thumbnail.js +27 -99
  11. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  12. package/dist/components/builder/spacing-handles.d.ts +14 -11
  13. package/dist/components/builder/spacing-handles.js +33 -57
  14. package/dist/components/builder/spacing-handles.js.map +1 -1
  15. package/dist/components/session-expired-overlay.d.ts +16 -0
  16. package/dist/components/session-expired-overlay.js +22 -6
  17. package/dist/components/session-expired-overlay.js.map +1 -1
  18. package/dist/components/sign-in-view.d.ts +10 -1
  19. package/dist/components/sign-in-view.js +14 -3
  20. package/dist/components/sign-in-view.js.map +1 -1
  21. package/dist/customization.d.ts +9 -1
  22. package/dist/customization.js.map +1 -1
  23. package/dist/lib/cms-views/view-model.d.ts +6 -6
  24. package/dist/lib/i18n/messages.de.js +6 -0
  25. package/dist/lib/i18n/messages.de.js.map +1 -1
  26. package/dist/lib/i18n/messages.en.d.ts +5 -0
  27. package/dist/lib/i18n/messages.en.js +6 -0
  28. package/dist/lib/i18n/messages.en.js.map +1 -1
  29. package/dist/lib/workspace/index.d.ts +10 -0
  30. package/dist/lib/workspace/index.js +11 -0
  31. package/dist/lib/workspace/index.js.map +1 -1
  32. package/dist/ui/dialog.d.ts +9 -1
  33. package/dist/ui/dialog.js +8 -2
  34. package/dist/ui/dialog.js.map +1 -1
  35. package/package.json +2 -2
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import type { FieldDef } from '@elytracms/core/cms-core';
3
3
  import type { StudioI18nConfig } from '@elytracms/studio/lib/i18n';
4
4
  import type { StudioAiConfig } from '@elytracms/studio/lib/ai';
5
+ import type { GeneratedPreviewLookup } from '../builder/blocks-model.js';
5
6
  import type { SidebarNode } from './module.js';
6
7
  /**
7
8
  * EC-236 — the studio customization config: the single, studio-only home for
@@ -68,6 +69,27 @@ export interface StudioConfig {
68
69
  * separate key).
69
70
  */
70
71
  canvas?: StudioCanvasTheme;
72
+ /**
73
+ * Insert-menu preview artifacts (EC-439, the seam EC-440 fills): where the
74
+ * studio's blocks panel finds GENERATED preview images for component
75
+ * fixtures. Studio-only (from `defineStudio`, never the delivery bundle) and
76
+ * fully optional — absent, tiles fall back from a curated
77
+ * `manifest.previewImage` straight to the icon+name placeholder. The EC-440
78
+ * CLI (`elytra previews`) renders fixtures headlessly against the consumer
79
+ * theme and writes a previews manifest; the consumer adapts that manifest
80
+ * into `previews.generated` here.
81
+ */
82
+ previews?: StudioPreviewsConfig;
83
+ }
84
+ /**
85
+ * The generated-preview seam (EC-439 AD-2). `generated` is a PURE lookup —
86
+ * component id + fixture name → image URL (`undefined` = no artifact). A
87
+ * fixture-less component's single implicit variant is looked up under the
88
+ * fixture name `'Default'`. Curated `manifest.previewImage` always wins over
89
+ * this lookup; the lookup wins over the placeholder.
90
+ */
91
+ export interface StudioPreviewsConfig {
92
+ generated?: GeneratedPreviewLookup;
71
93
  }
72
94
  /**
73
95
  * The canvas page-surface theme (EC-389). Both knobs target the preview iframe
@@ -1 +1 @@
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
+ {"version":3,"file":"studio-config.js","sourceRoot":"","sources":["../../../src/components/app-views/studio-config.ts"],"names":[],"mappings":"AAoIA;;;;;;;;;;;;;;;;;;;;;;;;;;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 { GeneratedPreviewLookup } from '../builder/blocks-model'\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 * Insert-menu preview artifacts (EC-439, the seam EC-440 fills): where the\n * studio's blocks panel finds GENERATED preview images for component\n * fixtures. Studio-only (from `defineStudio`, never the delivery bundle) and\n * fully optional — absent, tiles fall back from a curated\n * `manifest.previewImage` straight to the icon+name placeholder. The EC-440\n * CLI (`elytra previews`) renders fixtures headlessly against the consumer\n * theme and writes a previews manifest; the consumer adapts that manifest\n * into `previews.generated` here.\n */\n previews?: StudioPreviewsConfig\n}\n\n/**\n * The generated-preview seam (EC-439 AD-2). `generated` is a PURE lookup —\n * component id + fixture name → image URL (`undefined` = no artifact). A\n * fixture-less component's single implicit variant is looked up under the\n * fixture name `'Default'`. Curated `manifest.previewImage` always wins over\n * this lookup; the lookup wins over the placeholder.\n */\nexport interface StudioPreviewsConfig {\n generated?: GeneratedPreviewLookup\n}\n\n/**\n * The canvas page-surface theme (EC-389). Both knobs target the preview iframe\n * document — the isolated page the builder renders into — so a site whose theme\n * styles `body` (background, color, font) previews exactly as it delivers.\n */\nexport interface StudioCanvasTheme {\n /**\n * REPLACES the studio's default canvas body classes\n * (`bg-background text-foreground font-sans`). The default's utility classes\n * out-cascade a site theme's `@layer base body` rules (Tailwind layers lose to\n * later layers/un-layered styles), which is exactly how a site's page background\n * used to paint white in the canvas — pass `''` to drop the default entirely and\n * let the site's own `body` rules (from the canvas stylesheet) take over, or\n * pass your own classes. Omitted → the studio default stays.\n */\n bodyClassName?: string\n /**\n * Raw CSS injected into the canvas iframe as the LAST style element — after the\n * canvas stylesheet, 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"]}
@@ -2,40 +2,84 @@ import type { ComponentNode } from '@elytracms/core/project-graph';
2
2
  import type { ComponentFixture, ComponentManifest } from '@elytracms/core/component-registry';
3
3
  import type { StudioRegistry } from '@elytracms/studio/lib/registry';
4
4
  /**
5
- * Blocks panel model (EC-164) — the pure, React-free derivation behind the
6
- * visual insert surface. Every registry manifest yields one tile per declared
7
- * fixture (or a single default-props tile), grouped by manifest `category`
8
- * and filtered by a free-text query over name / description / category — no
9
- * hand-maintained panel config. Broken fixtures degrade to tiles carrying
10
- * explicit `issues`, never to a missing entry or a crash.
5
+ * Blocks panel model (EC-164, reshaped by EC-439) — the pure, React-free
6
+ * derivation behind the visual insert surface. Every canvas-eligible registry
7
+ * manifest yields **one tile** carrying the component identity plus a
8
+ * `variants` list (one per declared fixture, or a single implicit `Default`
9
+ * variant built from the seeded manifest defaults). Tiles group by manifest
10
+ * `category` and filter by a free-text query over name / description /
11
+ * category / **fixture names** — no hand-maintained panel config. Broken
12
+ * fixtures degrade to variants carrying explicit `issues`, never to a missing
13
+ * entry or a crash.
14
+ *
15
+ * Previews are ARTIFACTS, never runtime renders (EC-439 AD-2). Per tile and
16
+ * per variant, `previewUrl` resolves: curated `manifest.previewImage` (EC-409,
17
+ * always wins) → the {@link GeneratedPreviewLookup} seam (EC-440's build-time
18
+ * fixture renders) → `undefined` (the panel shows an icon+name placeholder).
11
19
  */
12
- /** One insertable tile in the blocks panel. */
20
+ /**
21
+ * The generated-preview seam (EC-439 AD-2, filled by EC-440): a pure lookup
22
+ * from component id + fixture name to an image URL. The EC-440 CLI renders
23
+ * every canvas-eligible component's fixtures headlessly against the consumer
24
+ * theme and writes a previews manifest; the consumer's `studio.config.tsx`
25
+ * adapts that manifest into this function via `defineStudio({ previews:
26
+ * { generated } })`. A fixture-less manifest's single implicit variant is
27
+ * looked up under the name {@link DEFAULT_VARIANT}. Empty/undefined everywhere
28
+ * until EC-440 lands — the panel then falls back to placeholders.
29
+ */
30
+ export type GeneratedPreviewLookup = (componentId: string, fixtureName: string) => string | undefined;
31
+ /**
32
+ * The implicit variant name of a fixture-less manifest (and the fixture name
33
+ * a {@link GeneratedPreviewLookup} is queried with for it).
34
+ */
35
+ export declare const DEFAULT_VARIANT = "Default";
36
+ /**
37
+ * One insertable variant of a component tile: a declared manifest fixture, or
38
+ * the single implicit `Default` for a fixture-less manifest. `variants[0]` is
39
+ * the tile's default — what tile click / Enter / drag inserts.
40
+ */
41
+ export interface BlockVariant {
42
+ /** The fixture name, or {@link DEFAULT_VARIANT} for the implicit variant. */
43
+ name: string;
44
+ /**
45
+ * Validation problems for this variant (manifest-level statuses plus the
46
+ * fixture's own issues). A non-empty list marks the variant as degraded —
47
+ * it still lists and still inserts.
48
+ */
49
+ issues: string[];
50
+ /**
51
+ * Resolved preview image URL: curated `manifest.previewImage` (wins) else
52
+ * the generated-preview lookup's URL for this fixture. `undefined` → the
53
+ * panel renders the icon+name placeholder.
54
+ */
55
+ previewUrl?: string;
56
+ /** Build the subtree this variant inserts (ids are reassigned on insert). */
57
+ build: () => ComponentNode;
58
+ }
59
+ /** One component (or layout preset) in the blocks panel — ONE tile per component (EC-439 AD-1). */
13
60
  export interface BlockTile {
14
- /** Stable identity (`<componentId>::<variant>` or `preset::<key>`). */
61
+ /** Stable identity: the component id, or `preset::<key>` for layout helpers. */
15
62
  key: string;
16
63
  /** Component id of the node the tile inserts. */
17
64
  componentId: string;
18
65
  title: string;
19
- /** Variation label: the fixture name, `Default`, or a preset description. */
20
- variant: string;
21
66
  description?: string;
22
67
  /**
23
- * Validation problems detected while building the preview (unknown child
24
- * component, undeclared slot, invalid fixture prop, missing
25
- * implementation). A non-empty list marks the tile as degraded — it still
26
- * renders and still inserts.
68
+ * The DEFAULT variant's validation problems — what clicking the tile
69
+ * inserts. Per-variant issues live on {@link BlockTile.variants}.
27
70
  */
28
71
  issues: string[];
29
- /** Deterministic-id subtree rendered live in the tile thumbnail. */
30
- previewNode: ComponentNode;
72
+ /** The default variant's resolved preview URL (see {@link BlockVariant.previewUrl}). */
73
+ previewUrl?: string;
74
+ /** All insertable variants; `variants[0]` is the default the tile inserts. */
75
+ variants: BlockVariant[];
31
76
  /**
32
- * Curated preview image URL (EC-409, `manifest.previewImage`). When set, the
33
- * tile renders this image instead of the live `previewNode` render — the
34
- * escape hatch for components whose real render needs data/context (e.g.
35
- * listings) and previews as "just text". Insertion is unaffected.
77
+ * Fixture names the active query matched (stamped by {@link blockGroups}
78
+ * when a query is present) — the panel highlights the variant affordance and
79
+ * the matching flyout rows so "found by fixture name" is visible.
36
80
  */
37
- previewImage?: string;
38
- /** Build the subtree to insert (ids are reassigned by the builder store). */
81
+ matchedVariants?: string[];
82
+ /** Build the DEFAULT subtree to insert: the first declared fixture's, else the seeded manifest default. */
39
83
  build: () => ComponentNode;
40
84
  }
41
85
  /** Tiles of one manifest `category`. */
@@ -44,7 +88,7 @@ export interface BlockGroup {
44
88
  tiles: BlockTile[];
45
89
  }
46
90
  export interface BlockFilter {
47
- /** Case-insensitive match over id, title, description, and category. */
91
+ /** Case-insensitive match over id, title, description, category, and fixture names. */
48
92
  query?: string;
49
93
  /** When set, only these component ids may be inserted (slot.allow). */
50
94
  slotAllow?: string[];
@@ -72,14 +116,31 @@ export declare function fixtureSubtree(studio: StudioRegistry, manifest: Compone
72
116
  node: ComponentNode;
73
117
  issues: string[];
74
118
  };
75
- /** All tiles for one manifest: one per fixture, else a single default tile. */
76
- export declare function tilesForManifest(studio: StudioRegistry, manifest: ComponentManifest): BlockTile[];
119
+ /**
120
+ * The ONE tile for a manifest (EC-439 AD-1): component identity plus one
121
+ * variant per declared fixture — or a single implicit `Default` variant using
122
+ * the seeded manifest default (a bare layout container is seeded so it
123
+ * survives EC-161 normalization instead of vanishing on commit, EC-383). The
124
+ * tile's own `issues`/`previewUrl`/`build` mirror the default variant
125
+ * (`variants[0]`), so tile click/Enter/drag inserts exactly what the first
126
+ * fixture tile inserted before the reshape.
127
+ */
128
+ export declare function tileForManifest(studio: StudioRegistry, manifest: ComponentManifest, generatedPreview?: GeneratedPreviewLookup): BlockTile;
129
+ /**
130
+ * A two-letter monogram from a tile title — the deliberate icon+name
131
+ * placeholder glyph (EC-439 AD-2 tier 3) for tiles with neither a curated nor
132
+ * a generated preview image: first letters of the first two words, else the
133
+ * first two letters of a single word. Deterministic and pure.
134
+ */
135
+ export declare function tileMonogram(title: string): string;
77
136
  /**
78
137
  * Derive the panel's category groups from the registry: filter by slot allow
79
- * list and query, expand manifests into tiles, group by `category`, and sort
138
+ * list and query, derive ONE tile per manifest, group by `category`, and sort
80
139
  * categories alphabetically (registry order within a group). Deterministic.
140
+ * When a query is active, each tile is stamped with the fixture names it
141
+ * matched (see {@link BlockTile.matchedVariants}).
81
142
  */
82
- export declare function blockGroups(studio: StudioRegistry, filter?: BlockFilter): BlockGroup[];
143
+ export declare function blockGroups(studio: StudioRegistry, filter?: BlockFilter, generatedPreview?: GeneratedPreviewLookup): BlockGroup[];
83
144
  /**
84
145
  * Whether ANY component could be inserted under a slot allow-list (EC-405) — the
85
146
  * cheap existence check behind the canvas insertion affordances: a "+" only shows
@@ -92,7 +153,8 @@ export declare function blockGroups(studio: StudioRegistry, filter?: BlockFilter
92
153
  */
93
154
  export declare function hasInsertableComponent(studio: StudioRegistry, slotAllow?: string[]): boolean;
94
155
  /**
95
- * Layout helper presets (EC-067) as panel tiles. Preview nodes are re-id'd
96
- * deterministically so sibling React keys stay unique.
156
+ * Layout helper presets (EC-067) as panel tiles — already one tile per preset.
157
+ * A preset has a single implicit variant and never a preview image, so its
158
+ * tile renders the placeholder treatment.
97
159
  */
98
160
  export declare function presetTiles(slotAllow?: string[]): BlockTile[];
@@ -1,10 +1,14 @@
1
- import { cloneWithNewIds, isLayoutContainerId } from '@elytracms/core/project-graph';
2
1
  import { isCanvasEligible, manifestSlots, propValueValidator } from '@elytracms/core/component-registry';
3
2
  import { localizedStringVariants } from '@elytracms/core/cms-core';
4
3
  import { localizedLabel } from '@elytracms/studio/lib/i18n';
5
4
  import { componentStatus } from '@elytracms/studio/lib/registry';
6
- import { nodeFromProps, previewSpecs } from '@elytracms/studio/lib/registry/preview';
5
+ import { nodeFromProps } from '@elytracms/studio/lib/registry/preview';
7
6
  import { LAYOUT_PRESETS, nodeFromManifest, seededNodeFromManifest } from './node-factory.js';
7
+ /**
8
+ * The implicit variant name of a fixture-less manifest (and the fixture name
9
+ * a {@link GeneratedPreviewLookup} is queried with for it).
10
+ */
11
+ export const DEFAULT_VARIANT = 'Default';
8
12
  /**
9
13
  * Intersect two component-id allow-lists. `undefined` means "no constraint from
10
14
  * this source", so the result is the other list (copied); two present lists yield
@@ -25,13 +29,6 @@ export function intersectAllow(slotAllow, fieldAllow) {
25
29
  return slotAllow.filter((id) => inField.has(id));
26
30
  }
27
31
  const UNCATEGORIZED = 'other';
28
- function prefixedCounter(prefix) {
29
- let n = 0;
30
- return () => {
31
- n += 1;
32
- return `${prefix}-${n}`;
33
- };
34
- }
35
32
  /** A node built from a manifest's defaults with an explicit id. */
36
33
  function defaultNode(manifest, id) {
37
34
  return { ...nodeFromManifest(manifest), id };
@@ -94,64 +91,90 @@ export function fixtureSubtree(studio, manifest, fixture, baseId) {
94
91
  }
95
92
  return { node, issues };
96
93
  }
97
- /** All tiles for one manifest: one per fixture, else a single default tile. */
98
- export function tilesForManifest(studio, manifest) {
94
+ /**
95
+ * The ONE tile for a manifest (EC-439 AD-1): component identity plus one
96
+ * variant per declared fixture — or a single implicit `Default` variant using
97
+ * the seeded manifest default (a bare layout container is seeded so it
98
+ * survives EC-161 normalization instead of vanishing on commit, EC-383). The
99
+ * tile's own `issues`/`previewUrl`/`build` mirror the default variant
100
+ * (`variants[0]`), so tile click/Enter/drag inserts exactly what the first
101
+ * fixture tile inserted before the reshape.
102
+ */
103
+ export function tileForManifest(studio, manifest, generatedPreview) {
99
104
  const title = localizedLabel(manifest.title, manifest.id);
100
105
  const baseIssues = manifestIssues(studio, manifest);
101
- if (!manifest.fixtures || manifest.fixtures.length === 0) {
102
- // No declared fixtures: insert the manifest defaults, seeding layout
103
- // containers so a bare Stack/Grid survives EC-161 normalization instead of
104
- // vanishing on commit (EC-383). Preview from the seeded node for containers
105
- // (WYSIWYG with the insert) and from the shared preview specs otherwise
106
- // (authored primitive previews or plain defaults).
107
- const previewNode = isLayoutContainerId(manifest.id)
108
- ? cloneWithNewIds(seededNodeFromManifest(manifest), prefixedCounter(`preview-${manifest.id}`))
109
- : previewSpecs(manifest)[0].node;
110
- return [
106
+ // EC-409: a curated preview image previews the COMPONENT, not one fixture —
107
+ // when declared it wins on the tile and on every variant row (AD-2 tier 1).
108
+ const curated = manifest.previewImage;
109
+ const variants = manifest.fixtures && manifest.fixtures.length > 0
110
+ ? manifest.fixtures.map((fixture, index) => {
111
+ const { issues } = fixtureSubtree(studio, manifest, fixture, `probe-${manifest.id}-${index}`);
112
+ return {
113
+ name: fixture.name,
114
+ issues: [...baseIssues, ...issues],
115
+ ...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, fixture.name)),
116
+ build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,
117
+ };
118
+ })
119
+ : [
111
120
  {
112
- key: `${manifest.id}::default`,
113
- componentId: manifest.id,
114
- title,
115
- variant: 'Default',
116
- description: manifest.description,
121
+ name: DEFAULT_VARIANT,
117
122
  issues: baseIssues,
118
- previewNode,
119
- // EC-409: the curated preview image (when declared) replaces the live
120
- // render on EVERY tile of the manifest — it previews the component,
121
- // not one fixture variation.
122
- ...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),
123
+ ...previewUrlEntry(curated ?? generatedPreview?.(manifest.id, DEFAULT_VARIANT)),
123
124
  build: () => seededNodeFromManifest(manifest),
124
125
  },
125
126
  ];
126
- }
127
- return manifest.fixtures.map((fixture, index) => {
128
- const { node, issues } = fixtureSubtree(studio, manifest, fixture, `preview-${manifest.id}-${index}`);
129
- return {
130
- key: `${manifest.id}::${fixture.name}`,
131
- componentId: manifest.id,
132
- title,
133
- variant: fixture.name,
134
- description: manifest.description,
135
- issues: [...baseIssues, ...issues],
136
- previewNode: node,
137
- ...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),
138
- build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,
139
- };
140
- });
127
+ const defaultVariant = variants[0];
128
+ return {
129
+ key: manifest.id,
130
+ componentId: manifest.id,
131
+ title,
132
+ description: manifest.description,
133
+ issues: defaultVariant.issues,
134
+ ...previewUrlEntry(defaultVariant.previewUrl),
135
+ variants,
136
+ build: defaultVariant.build,
137
+ };
138
+ }
139
+ /** Spreadable `{ previewUrl }` entry — omitted (not `undefined`-valued) when absent. */
140
+ function previewUrlEntry(url) {
141
+ return url === undefined ? {} : { previewUrl: url };
142
+ }
143
+ /**
144
+ * A two-letter monogram from a tile title — the deliberate icon+name
145
+ * placeholder glyph (EC-439 AD-2 tier 3) for tiles with neither a curated nor
146
+ * a generated preview image: first letters of the first two words, else the
147
+ * first two letters of a single word. Deterministic and pure.
148
+ */
149
+ export function tileMonogram(title) {
150
+ const words = title.trim().split(/\s+/).filter(Boolean);
151
+ if (words.length === 0)
152
+ return '?';
153
+ const glyph = words.length === 1 ? words[0].slice(0, 2) : `${words[0].charAt(0)}${words[1].charAt(0)}`;
154
+ return glyph.toUpperCase();
155
+ }
156
+ function matchedFixtureNames(manifest, query) {
157
+ return (manifest.fixtures ?? [])
158
+ .map((fixture) => fixture.name)
159
+ .filter((name) => name.toLowerCase().includes(query));
141
160
  }
142
161
  function matches(manifest, query) {
143
162
  const hay = [manifest.id, ...localizedStringVariants(manifest.title), manifest.description, manifest.category]
144
163
  .filter(Boolean)
145
164
  .join(' ')
146
165
  .toLowerCase();
147
- return hay.includes(query);
166
+ // EC-439: fixture names are searchable too — a content-seeded fixture name
167
+ // ("Startseite") must find its component even when no manifest field matches.
168
+ return hay.includes(query) || matchedFixtureNames(manifest, query).length > 0;
148
169
  }
149
170
  /**
150
171
  * Derive the panel's category groups from the registry: filter by slot allow
151
- * list and query, expand manifests into tiles, group by `category`, and sort
172
+ * list and query, derive ONE tile per manifest, group by `category`, and sort
152
173
  * categories alphabetically (registry order within a group). Deterministic.
174
+ * When a query is active, each tile is stamped with the fixture names it
175
+ * matched (see {@link BlockTile.matchedVariants}).
153
176
  */
154
- export function blockGroups(studio, filter = {}) {
177
+ export function blockGroups(studio, filter = {}, generatedPreview) {
155
178
  const query = filter.query?.trim().toLowerCase();
156
179
  const allow = filter.slotAllow;
157
180
  const groups = new Map();
@@ -165,9 +188,15 @@ export function blockGroups(studio, filter = {}) {
165
188
  continue;
166
189
  if (query && !matches(manifest, query))
167
190
  continue;
191
+ const tile = tileForManifest(studio, manifest, generatedPreview);
192
+ if (query) {
193
+ const matched = matchedFixtureNames(manifest, query);
194
+ if (matched.length > 0)
195
+ tile.matchedVariants = matched;
196
+ }
168
197
  const category = manifest.category ?? UNCATEGORIZED;
169
198
  const tiles = groups.get(category) ?? [];
170
- tiles.push(...tilesForManifest(studio, manifest));
199
+ tiles.push(tile);
171
200
  groups.set(category, tiles);
172
201
  }
173
202
  return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([category, tiles]) => ({ category, tiles }));
@@ -193,18 +222,18 @@ export function hasInsertableComponent(studio, slotAllow) {
193
222
  return presetTiles(slotAllow).length > 0;
194
223
  }
195
224
  /**
196
- * Layout helper presets (EC-067) as panel tiles. Preview nodes are re-id'd
197
- * deterministically so sibling React keys stay unique.
225
+ * Layout helper presets (EC-067) as panel tiles — already one tile per preset.
226
+ * A preset has a single implicit variant and never a preview image, so its
227
+ * tile renders the placeholder treatment.
198
228
  */
199
229
  export function presetTiles(slotAllow) {
200
230
  return LAYOUT_PRESETS.filter((preset) => !slotAllow || slotAllow.includes(preset.build().componentId)).map((preset) => ({
201
231
  key: `preset::${preset.key}`,
202
232
  componentId: preset.build().componentId,
203
233
  title: preset.label,
204
- variant: preset.description,
205
234
  description: preset.description,
206
235
  issues: [],
207
- previewNode: cloneWithNewIds(preset.build(), prefixedCounter(`preview-preset-${preset.key}`)),
236
+ variants: [{ name: DEFAULT_VARIANT, issues: [], build: () => preset.build() }],
208
237
  build: () => preset.build(),
209
238
  }));
210
239
  }
@@ -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, 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, 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, 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 /**\n * Curated preview image URL (EC-409, `manifest.previewImage`). When set, the\n * tile renders this image instead of the live `previewNode` render — the\n * escape hatch for components whose real render needs data/context (e.g.\n * listings) and previews as \"just text\". Insertion is unaffected.\n */\n previewImage?: string\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.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/** All tiles for one manifest: one per fixture, else a single default tile. */\nexport function tilesForManifest(studio: StudioRegistry, manifest: ComponentManifest): BlockTile[] {\n const title = localizedLabel(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 // EC-409: the curated preview image (when declared) replaces the live\n // render on EVERY tile of the manifest — it previews the component,\n // not one fixture variation.\n ...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),\n build: () => seededNodeFromManifest(manifest),\n },\n ]\n }\n\n return manifest.fixtures.map((fixture, index) => {\n const { node, issues } = fixtureSubtree(studio, manifest, fixture, `preview-${manifest.id}-${index}`)\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 ...(manifest.previewImage ? { previewImage: manifest.previewImage } : {}),\n build: () => fixtureSubtree(studio, manifest, fixture, 'tmp').node,\n }\n })\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 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()].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. 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((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 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}\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 { 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,13 +1,16 @@
1
1
  import type { InsertTarget } from '@elytracms/core/project-graph';
2
2
  import type { ClipboardFragment } from '@elytracms/core/clipboard';
3
3
  import type { StudioRegistry } from '@elytracms/studio/lib/registry';
4
- import type { BlockTile } from './blocks-model.js';
4
+ import type { BlockTile, BlockVariant } from './blocks-model.js';
5
5
  /**
6
- * Blocks panel (EC-164): the visual insert surface. Every registry component
7
- * and each of its fixture variations is a tile with a live, scaled render —
8
- * the component library *is* the insert surface. Clicking a tile inserts the
9
- * fixture's configured subtree through the builder's `insert` action (one
10
- * history entry, mirrored onto the `graph.insertNode` operation).
6
+ * Blocks panel (EC-164, redesigned by EC-439): the visual insert surface.
7
+ * Every canvas-eligible registry component is ONE tile; a component with 2+
8
+ * fixtures carries a compact "n variants" chip opening a flyout with one
9
+ * insert/copy row per fixture. Clicking a tile (or dragging it) inserts the
10
+ * DEFAULT — the first declared fixture's configured subtree, else the seeded
11
+ * manifest default — through the builder's `insert` action (one history entry).
12
+ * Previews are artifacts (curated `previewImage` → generated artifact →
13
+ * icon+name placeholder), never in-document runtime renders.
11
14
  */
12
15
  export interface InsertContext {
13
16
  target: InsertTarget;
@@ -18,13 +21,14 @@ export interface InsertContext {
18
21
  }
19
22
  /**
20
23
  * "Copy exact configuration" (EC-175, vision 3.3): the EC-174 fragment for a
21
- * blocks-panel tile. Tiles backed by a declared manifest fixture go through
22
- * `buildFragmentFromRegistryFixture` (the fixture's exact configuration);
23
- * preset/default tiles fragment the same subtree the tile inserts. Both paths
24
- * share `buildFragmentFromSubtree`, so the fragment shape — and the paste
25
- * path — is identical to copying a live canvas instance.
24
+ * blocks-panel variant. Variants backed by a declared manifest fixture go
25
+ * through `buildFragmentFromRegistryFixture` (the fixture's exact
26
+ * configuration); implicit-default and preset variants fragment the same
27
+ * subtree they insert. Both paths share `buildFragmentFromSubtree`, so the
28
+ * fragment shape — and the paste path — is identical to copying a live canvas
29
+ * instance.
26
30
  */
27
- export declare function tileFragment(studio: StudioRegistry, tile: BlockTile, projectId: string): ClipboardFragment | null;
31
+ export declare function tileFragment(studio: StudioRegistry, tile: BlockTile, variant: BlockVariant, projectId: string): ClipboardFragment | null;
28
32
  /**
29
33
  * The Insert dialog never dismisses on pointer interactions outside its
30
34
  * content. Rows scrolled below the list's fold keep their layout position
@@ -42,3 +46,27 @@ export declare function BlocksPanel({ context, open, onOpenChange, }: {
42
46
  open: boolean;
43
47
  onOpenChange: (open: boolean) => void;
44
48
  }): import("react").JSX.Element | null;
49
+ /**
50
+ * One component tile (exported for markup tests). Not a native `<button>`: the
51
+ * tile CONTAINS interactive controls (the copy-exact button and the variants
52
+ * chip), and nested `<button>`s are invalid HTML — so it stays a keyboard-
53
+ * operable `role="button"` element whose own key handler ignores events
54
+ * bubbling from those inner controls.
55
+ */
56
+ export declare function BlockTileButton({ tile, onInsert, onCopy, onDragStart, onDragEnd, }: {
57
+ tile: BlockTile;
58
+ onInsert: (variant: BlockVariant) => void;
59
+ onCopy: (tile: BlockTile, variant: BlockVariant) => void;
60
+ onDragStart: (e: React.DragEvent) => void;
61
+ onDragEnd: () => void;
62
+ }): import("react").JSX.Element;
63
+ /**
64
+ * The flyout's variant rows (exported for markup tests): one insert row +
65
+ * copy-exact button per fixture (EC-175 semantics per variant). Rows matched
66
+ * by the active fixture-name search are highlighted.
67
+ */
68
+ export declare function VariantRows({ tile, onInsert, onCopy, }: {
69
+ tile: BlockTile;
70
+ onInsert: (variant: BlockVariant) => void;
71
+ onCopy: (tile: BlockTile, variant: BlockVariant) => void;
72
+ }): import("react").JSX.Element;