@elytracms/studio 0.0.15 → 0.0.16

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 (69) hide show
  1. package/dist/components/app-views/default-modules.d.ts +41 -0
  2. package/dist/components/app-views/default-modules.js +201 -0
  3. package/dist/components/app-views/default-modules.js.map +1 -0
  4. package/dist/components/app-views/module.d.ts +93 -0
  5. package/dist/components/app-views/module.js +75 -0
  6. package/dist/components/app-views/module.js.map +1 -0
  7. package/dist/components/app-views/sidebar.d.ts +95 -0
  8. package/dist/components/app-views/sidebar.js +153 -0
  9. package/dist/components/app-views/sidebar.js.map +1 -0
  10. package/dist/components/builder/builder-store.d.ts +7 -1
  11. package/dist/components/builder/builder-store.js +1 -1
  12. package/dist/components/builder/builder-store.js.map +1 -1
  13. package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
  14. package/dist/components/builder/canvas-breakpoints.js +55 -0
  15. package/dist/components/builder/canvas-breakpoints.js.map +1 -0
  16. package/dist/components/builder/canvas.js +14 -8
  17. package/dist/components/builder/canvas.js.map +1 -1
  18. package/dist/components/builder/composition-field-editor.js +18 -17
  19. package/dist/components/builder/composition-field-editor.js.map +1 -1
  20. package/dist/components/cms/bulk-actions.d.ts +23 -5
  21. package/dist/components/cms/bulk-actions.js +7 -3
  22. package/dist/components/cms/bulk-actions.js.map +1 -1
  23. package/dist/components/cms/bulk-bar.d.ts +22 -6
  24. package/dist/components/cms/bulk-bar.js +46 -9
  25. package/dist/components/cms/bulk-bar.js.map +1 -1
  26. package/dist/components/cms/cms-index.d.ts +4 -2
  27. package/dist/components/cms/cms-index.js +6 -4
  28. package/dist/components/cms/cms-index.js.map +1 -1
  29. package/dist/components/cms/cms-store.d.ts +6 -5
  30. package/dist/components/cms/cms-store.js +51 -9
  31. package/dist/components/cms/cms-store.js.map +1 -1
  32. package/dist/components/cms/collection-table.js +7 -3
  33. package/dist/components/cms/collection-table.js.map +1 -1
  34. package/dist/components/cms/collections-sidebar.d.ts +10 -1
  35. package/dist/components/cms/collections-sidebar.js +13 -2
  36. package/dist/components/cms/collections-sidebar.js.map +1 -1
  37. package/dist/components/cms/field-control.d.ts +11 -2
  38. package/dist/components/cms/field-control.js +22 -1
  39. package/dist/components/cms/field-control.js.map +1 -1
  40. package/dist/components/cms/field-renderer.d.ts +71 -0
  41. package/dist/components/cms/field-renderer.js +67 -0
  42. package/dist/components/cms/field-renderer.js.map +1 -0
  43. package/dist/components/cms/filter-builder.js +6 -28
  44. package/dist/components/cms/filter-builder.js.map +1 -1
  45. package/dist/components/cms/scalar-value-editor.d.ts +35 -0
  46. package/dist/components/cms/scalar-value-editor.js +43 -0
  47. package/dist/components/cms/scalar-value-editor.js.map +1 -0
  48. package/dist/components/project-session.js +8 -3
  49. package/dist/components/project-session.js.map +1 -1
  50. package/dist/components/section-nav.d.ts +10 -10
  51. package/dist/components/section-nav.js +48 -25
  52. package/dist/components/section-nav.js.map +1 -1
  53. package/dist/components/section-surface.js +2 -3
  54. package/dist/components/section-surface.js.map +1 -1
  55. package/dist/components/studio-project-screen.d.ts +3 -4
  56. package/dist/components/studio-project-screen.js +29 -147
  57. package/dist/components/studio-project-screen.js.map +1 -1
  58. package/dist/components/studio-shell.js +13 -9
  59. package/dist/components/studio-shell.js.map +1 -1
  60. package/dist/lib/data/collection-content.d.ts +19 -1
  61. package/dist/lib/data/collection-content.js +16 -0
  62. package/dist/lib/data/collection-content.js.map +1 -1
  63. package/dist/lib/workspace/index.d.ts +27 -1
  64. package/dist/lib/workspace/index.js +34 -0
  65. package/dist/lib/workspace/index.js.map +1 -1
  66. package/dist/router/match.d.ts +22 -34
  67. package/dist/router/match.js +22 -40
  68. package/dist/router/match.js.map +1 -1
  69. package/package.json +2 -2
@@ -0,0 +1,153 @@
1
+ import { useMemo } from 'react';
2
+ import { resolveOverrides } from '@elytracms/core/studio-core';
3
+ import { resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace';
4
+ import { moduleAvailability } from './module';
5
+ import { useAppViews } from './default-modules';
6
+ // --- Builder helpers (the typed consumer API; carry live icons) ---
7
+ /** A nav item; `id` defaults to `moduleId` when omitted. */
8
+ export function navItem(config) {
9
+ return {
10
+ kind: 'item',
11
+ id: config.id ?? config.moduleId,
12
+ moduleId: config.moduleId,
13
+ ...(config.label !== undefined ? { label: config.label } : {}),
14
+ ...(config.icon !== undefined ? { icon: config.icon } : {}),
15
+ ...(config.deepLink !== undefined ? { deepLink: config.deepLink } : {}),
16
+ };
17
+ }
18
+ /** A labelled group of sub-items; `id` defaults to a slug of the label. */
19
+ export function navGroup(label, items, opts = {}) {
20
+ return { kind: 'group', id: opts.id ?? slugify(label), label, items };
21
+ }
22
+ /** A divider between rail sections. */
23
+ export function navSeparator(id) {
24
+ return { kind: 'separator', ...(id !== undefined ? { id } : {}) };
25
+ }
26
+ /** Identity helper that types a consumer's `config.sidebar` array. */
27
+ export function defineSidebar(nodes) {
28
+ return nodes;
29
+ }
30
+ function slugify(label) {
31
+ return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'group';
32
+ }
33
+ // --- Resolution (EC-236 seam) ---
34
+ /** The default rail: every non-hidden module as a plain item, in registry order. */
35
+ function defaultSidebar(modules) {
36
+ return modules
37
+ .filter((module) => !module.hidden)
38
+ .map((module) => ({ kind: 'item', id: module.id, moduleId: module.id }));
39
+ }
40
+ /** The `_splat` path for an item (module id + its deep-link segments). */
41
+ function itemPath(item) {
42
+ return [item.moduleId, ...(item.deepLink ?? [])].join('/');
43
+ }
44
+ /** Validate one item against the registry — unknown module or unresolvable deep-link. */
45
+ function validateItem(registry, item) {
46
+ const module = registry.get(item.moduleId);
47
+ if (!module)
48
+ return `targets module "${item.moduleId}", which is not registered`;
49
+ if (item.deepLink && item.deepLink.length > 0) {
50
+ const parse = module.parsePath;
51
+ if (!parse) {
52
+ return `deep-links into module "${item.moduleId}", which takes no path`;
53
+ }
54
+ if (parse([...item.deepLink]) === null) {
55
+ return `deep-link "${item.deepLink.join('/')}" does not resolve in module "${item.moduleId}"`;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * Resolve a consumer sidebar (or the default rail) over the app-view registry
62
+ * through the shared customization core (EC-236). Every node — items, group
63
+ * headers, nested items, separators — is keyed by id and run through
64
+ * {@link resolveOverrides} for dedup + per-node validation, so a duplicate id or
65
+ * an invalid item is a visible issue and is dropped; the surviving nodes are
66
+ * rebuilt into the rail tree (groups keep their surviving sub-items).
67
+ */
68
+ export function resolveSidebar(registry, project, entries) {
69
+ const config = entries ?? defaultSidebar(registry.modules);
70
+ const flat = [];
71
+ config.forEach((node, index) => {
72
+ if (node.kind === 'separator') {
73
+ flat.push({ id: node.id ?? `__separator:${index}`, value: { node, isItem: false } });
74
+ return;
75
+ }
76
+ if (node.kind === 'group') {
77
+ flat.push({ id: node.id, value: { node, isItem: false } });
78
+ node.items.forEach((item) => flat.push({ id: item.id, value: { node: item, isItem: true } }));
79
+ return;
80
+ }
81
+ flat.push({ id: node.id, value: { node, isItem: true } });
82
+ });
83
+ const resolved = resolveOverrides({
84
+ point: 'sidebar',
85
+ defaults: [],
86
+ overrides: flat,
87
+ allowNew: true,
88
+ validate: (entry) => entry.value.isItem
89
+ ? validateItem(registry, entry.value.node)
90
+ : null,
91
+ });
92
+ const survived = (id) => resolved.get(id) !== undefined;
93
+ const toItem = (item) => {
94
+ const module = registry.get(item.moduleId);
95
+ return {
96
+ kind: 'item',
97
+ id: item.id,
98
+ moduleId: item.moduleId,
99
+ label: item.label ?? module.label,
100
+ icon: item.icon ?? module.icon,
101
+ path: itemPath(item),
102
+ availability: moduleAvailability(project, module),
103
+ };
104
+ };
105
+ // Emit each surviving id at most once — `resolveOverrides` keeps the first of a
106
+ // duplicate, so the rebuild must skip later occurrences of that same id too.
107
+ const emitted = new Set();
108
+ const claim = (id) => {
109
+ if (!survived(id) || emitted.has(id))
110
+ return false;
111
+ emitted.add(id);
112
+ return true;
113
+ };
114
+ const nodes = [];
115
+ config.forEach((node, index) => {
116
+ if (node.kind === 'separator') {
117
+ const id = node.id ?? `__separator:${index}`;
118
+ if (claim(id))
119
+ nodes.push({ kind: 'separator', id });
120
+ return;
121
+ }
122
+ if (node.kind === 'group') {
123
+ if (!claim(node.id))
124
+ return;
125
+ const groupItems = node.items.filter((item) => claim(item.id)).map(toItem);
126
+ if (groupItems.length > 0) {
127
+ nodes.push({ kind: 'group', id: node.id, label: node.label, items: groupItems });
128
+ }
129
+ return;
130
+ }
131
+ if (claim(node.id))
132
+ nodes.push(toItem(node));
133
+ });
134
+ return { nodes, issues: resolved.issues };
135
+ }
136
+ /** The active module id parsed from a studio pathname `/<projectId>/<module>[/…]`. */
137
+ export function activeModuleFromPath(pathname) {
138
+ return pathname.split('/')[2] || null;
139
+ }
140
+ /**
141
+ * The active project's resolved sidebar — the default rail or `config.sidebar`,
142
+ * resolved over the module registry through the EC-236 seam (static per boot).
143
+ */
144
+ export function useSidebar(project) {
145
+ const registry = useAppViews();
146
+ return useMemo(() => {
147
+ // Config carries the sidebar opaquely (`SidebarEntry` = object); the studio
148
+ // owns + validates the rich item/group/separator shape, so cast it back here.
149
+ const entries = resolveDeploymentSidebar();
150
+ return resolveSidebar(registry, project, entries);
151
+ }, [registry, project]);
152
+ }
153
+ //# sourceMappingURL=sidebar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../src/components/app-views/sidebar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { resolveOverrides } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core'\nimport { resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace'\nimport { moduleAvailability, type AppViewModule, type ResolvedAppViews } from './module'\nimport { useAppViews } from './default-modules'\n\n/**\n * EC-237 — the configurable sidebar, an instance of the EC-236 customization seam.\n *\n * The section rail is a consumer-extendable ARRAY of nav entries — items (each\n * targeting an EC-239 app-view module, optionally deep-linked to a resource),\n * groups of sub-items, and separators. A consuming repo supplies its own array\n * through `config.sidebar`; omitted → the studio derives the default rail from its\n * module registry (every non-hidden module, in order). The supplied array is\n * resolved through the shared {@link resolveOverrides} core: a duplicate id, an\n * item whose target module isn't registered (\"unknown view\"), or a deep-link that\n * doesn't resolve becomes a visible {@link CustomizationIssue} and the entry is\n * dropped — never a silent failure. Sub-views (a focused \"Taxonomy\") are EC-239\n * modules; this file decides only how the rail PRESENTS and orders them.\n */\n\n// --- Consumer config shapes (typed builder helpers below) ---\n\n/** A nav item targeting an app-view module, optionally deep-linked to a resource. */\nexport interface SidebarItemConfig {\n kind: 'item'\n /** Stable id (defaults to `moduleId` via {@link navItem}). */\n id: string\n /** The app-view module this item mounts — must be in the registry. */\n moduleId: string\n /** Overrides the module's rail label. */\n label?: string\n /** Overrides the module's rail icon. */\n icon?: LucideIcon\n /** Trailing path segments targeting a specific resource, e.g. `['category']`. */\n deepLink?: readonly string[]\n}\n\nexport interface SidebarGroupConfig {\n kind: 'group'\n id: string\n label: string\n items: SidebarItemConfig[]\n}\n\nexport interface SidebarSeparatorConfig {\n kind: 'separator'\n id?: string\n}\n\nexport type SidebarNodeConfig = SidebarItemConfig | SidebarGroupConfig | SidebarSeparatorConfig\n\n// --- Resolved (rail-ready) shapes ---\n\nexport interface ResolvedSidebarItem {\n kind: 'item'\n id: string\n moduleId: string\n label: string\n icon: LucideIcon\n /** The `_splat` to navigate to (moduleId + deep-link segments). */\n path: string\n availability: SectionAvailability\n}\nexport interface ResolvedSidebarGroup {\n kind: 'group'\n id: string\n label: string\n items: ResolvedSidebarItem[]\n}\nexport interface ResolvedSidebarSeparator {\n kind: 'separator'\n id: string\n}\nexport type ResolvedSidebarNode =\n | ResolvedSidebarItem\n | ResolvedSidebarGroup\n | ResolvedSidebarSeparator\n\nexport interface ResolvedSidebar {\n nodes: ResolvedSidebarNode[]\n issues: CustomizationIssue[]\n}\n\n// --- Builder helpers (the typed consumer API; carry live icons) ---\n\n/** A nav item; `id` defaults to `moduleId` when omitted. */\nexport function navItem(\n config: Omit<SidebarItemConfig, 'kind' | 'id'> & { id?: string },\n): SidebarItemConfig {\n return {\n kind: 'item',\n id: config.id ?? config.moduleId,\n moduleId: config.moduleId,\n ...(config.label !== undefined ? { label: config.label } : {}),\n ...(config.icon !== undefined ? { icon: config.icon } : {}),\n ...(config.deepLink !== undefined ? { deepLink: config.deepLink } : {}),\n }\n}\n\n/** A labelled group of sub-items; `id` defaults to a slug of the label. */\nexport function navGroup(\n label: string,\n items: SidebarItemConfig[],\n opts: { id?: string } = {},\n): SidebarGroupConfig {\n return { kind: 'group', id: opts.id ?? slugify(label), label, items }\n}\n\n/** A divider between rail sections. */\nexport function navSeparator(id?: string): SidebarSeparatorConfig {\n return { kind: 'separator', ...(id !== undefined ? { id } : {}) }\n}\n\n/** Identity helper that types a consumer's `config.sidebar` array. */\nexport function defineSidebar(nodes: SidebarNodeConfig[]): SidebarNodeConfig[] {\n return nodes\n}\n\nfunction slugify(label: string): string {\n return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'group'\n}\n\n// --- Resolution (EC-236 seam) ---\n\n/** The default rail: every non-hidden module as a plain item, in registry order. */\nfunction defaultSidebar(modules: readonly AppViewModule[]): SidebarItemConfig[] {\n return modules\n .filter((module) => !module.hidden)\n .map((module) => ({ kind: 'item', id: module.id, moduleId: module.id }))\n}\n\n/** The `_splat` path for an item (module id + its deep-link segments). */\nfunction itemPath(item: SidebarItemConfig): string {\n return [item.moduleId, ...(item.deepLink ?? [])].join('/')\n}\n\n/** Validate one item against the registry — unknown module or unresolvable deep-link. */\nfunction validateItem(registry: ResolvedAppViews, item: SidebarItemConfig): string | null {\n const module = registry.get(item.moduleId)\n if (!module) return `targets module \"${item.moduleId}\", which is not registered`\n if (item.deepLink && item.deepLink.length > 0) {\n const parse = module.parsePath\n if (!parse) {\n return `deep-links into module \"${item.moduleId}\", which takes no path`\n }\n if (parse([...item.deepLink]) === null) {\n return `deep-link \"${item.deepLink.join('/')}\" does not resolve in module \"${item.moduleId}\"`\n }\n }\n return null\n}\n\n/**\n * Resolve a consumer sidebar (or the default rail) over the app-view registry\n * through the shared customization core (EC-236). Every node — items, group\n * headers, nested items, separators — is keyed by id and run through\n * {@link resolveOverrides} for dedup + per-node validation, so a duplicate id or\n * an invalid item is a visible issue and is dropped; the surviving nodes are\n * rebuilt into the rail tree (groups keep their surviving sub-items).\n */\nexport function resolveSidebar(\n registry: ResolvedAppViews,\n project: Project,\n entries?: readonly SidebarNodeConfig[],\n): ResolvedSidebar {\n const config = entries ?? defaultSidebar(registry.modules)\n\n // Flatten the tree into a single keyspace so dedup + validation route through\n // the shared core. Separators without an id get a positional one (never a user\n // collision); groups + items keep theirs. `value` carries the node for validate.\n type Flat = { node: SidebarNodeConfig; isItem: boolean }\n const flat: { id: string; value: Flat }[] = []\n config.forEach((node, index) => {\n if (node.kind === 'separator') {\n flat.push({ id: node.id ?? `__separator:${index}`, value: { node, isItem: false } })\n return\n }\n if (node.kind === 'group') {\n flat.push({ id: node.id, value: { node, isItem: false } })\n node.items.forEach((item) => flat.push({ id: item.id, value: { node: item, isItem: true } }))\n return\n }\n flat.push({ id: node.id, value: { node, isItem: true } })\n })\n\n const resolved = resolveOverrides<Flat>({\n point: 'sidebar',\n defaults: [],\n overrides: flat,\n allowNew: true,\n validate: (entry) =>\n entry.value.isItem\n ? validateItem(registry, entry.value.node as SidebarItemConfig)\n : null,\n })\n const survived = (id: string) => resolved.get(id) !== undefined\n\n const toItem = (item: SidebarItemConfig): ResolvedSidebarItem => {\n const module = registry.get(item.moduleId)!\n return {\n kind: 'item',\n id: item.id,\n moduleId: item.moduleId,\n label: item.label ?? module.label,\n icon: item.icon ?? module.icon,\n path: itemPath(item),\n availability: moduleAvailability(project, module),\n }\n }\n\n // Emit each surviving id at most once — `resolveOverrides` keeps the first of a\n // duplicate, so the rebuild must skip later occurrences of that same id too.\n const emitted = new Set<string>()\n const claim = (id: string) => {\n if (!survived(id) || emitted.has(id)) return false\n emitted.add(id)\n return true\n }\n\n const nodes: ResolvedSidebarNode[] = []\n config.forEach((node, index) => {\n if (node.kind === 'separator') {\n const id = node.id ?? `__separator:${index}`\n if (claim(id)) nodes.push({ kind: 'separator', id })\n return\n }\n if (node.kind === 'group') {\n if (!claim(node.id)) return\n const groupItems = node.items.filter((item) => claim(item.id)).map(toItem)\n if (groupItems.length > 0) {\n nodes.push({ kind: 'group', id: node.id, label: node.label, items: groupItems })\n }\n return\n }\n if (claim(node.id)) nodes.push(toItem(node))\n })\n\n return { nodes, issues: resolved.issues }\n}\n\n/** The active module id parsed from a studio pathname `/<projectId>/<module>[/…]`. */\nexport function activeModuleFromPath(pathname: string): string | null {\n return pathname.split('/')[2] || null\n}\n\n/**\n * The active project's resolved sidebar — the default rail or `config.sidebar`,\n * resolved over the module registry through the EC-236 seam (static per boot).\n */\nexport function useSidebar(project: Project): ResolvedSidebar {\n const registry = useAppViews()\n return useMemo(() => {\n // Config carries the sidebar opaquely (`SidebarEntry` = object); the studio\n // owns + validates the rich item/group/separator shape, so cast it back here.\n const entries = resolveDeploymentSidebar() as readonly SidebarNodeConfig[] | undefined\n return resolveSidebar(registry, project, entries)\n }, [registry, project])\n}\n"]}
@@ -1,6 +1,12 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { ComponentNode, InsertTarget, PropValue } from '@elytracms/core/project-graph';
3
- export type Viewport = 'desktop' | 'tablet' | 'mobile';
3
+ /**
4
+ * A canvas preview-width key (EC-250): `'full'` (unconstrained) or a configured
5
+ * breakpoint name (`'sm'`, `'md'`, …). Dynamic because the set is sourced from
6
+ * the consuming site's breakpoints, so it is a string, not a fixed union — the
7
+ * switcher resolves the available options via {@link resolveCanvasViewports}.
8
+ */
9
+ export type Viewport = string;
4
10
  /**
5
11
  * The builder store's action union. EC-287 retired the DB-resident project graph
6
12
  * and its persistence mirror, so these actions are pure in-memory editor state —
@@ -148,7 +148,7 @@ export function initialBuilderState(composition) {
148
148
  future: [],
149
149
  selectedId: present.id,
150
150
  hoveredId: null,
151
- viewport: 'desktop',
151
+ viewport: 'full',
152
152
  // Seed past the ids the composition already carries (EC-203): a restored
153
153
  // tree holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`
154
154
  // on the first insert and produce a duplicate-node-id.
@@ -1 +1 @@
1
- {"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n cloneWithNewIds,\n duplicateNode,\n getNode,\n getParentInfo,\n insertNode,\n maxNodeIdSeq,\n moveNode,\n normalizeContainers,\n removeNode,\n removeNodeProp,\n reorderNode,\n setNodeProp,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget, PropValue } from '@elytracms/core/project-graph'\nimport { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules'\n\nconst HISTORY_LIMIT = 100\n\nexport type Viewport = 'desktop' | 'tablet' | 'mobile'\n\n/**\n * The builder store's action union. EC-287 retired the DB-resident project graph\n * and its persistence mirror, so these actions are pure in-memory editor state —\n * no action maps to a persisted `graph.*` command anymore. The block-field editor\n * lifts committed edits into the CMS document via `onChange`\n * (`composition-field-editor`); nothing persists a graph.\n */\nexport type BuilderAction =\n | { type: 'select'; id: string | null }\n | { type: 'hover'; id: string | null }\n | { type: 'viewport'; viewport: Viewport }\n | { type: 'undo' }\n | { type: 'redo' }\n | { type: 'insert'; target: InsertTarget; node: ComponentNode; select?: boolean }\n | { type: 'remove'; id: string }\n | { type: 'duplicate'; id: string }\n | { type: 'reorder'; id: string; delta: -1 | 1 }\n | { type: 'move'; id: string; target: InsertTarget }\n /**\n * EC-162 layout gesture: wrap the reference node and the dragged subject in\n * a two-column Grid container as ONE atomic structural edit (one undo step).\n */\n | {\n type: 'wrap'\n referenceId: string\n side: 'left' | 'right'\n subject: { kind: 'node'; nodeId: string } | { kind: 'component'; node: ComponentNode }\n }\n | { type: 'setProp'; id: string; name: string; value: PropValue; coalesceKey?: string }\n | { type: 'removeProp'; id: string; name: string }\n\nexport interface BuilderState {\n /**\n * The edited composition: one root-bearing component tree (EC-287). The canvas\n * wraps a page document's `body` blocks into a synthetic root and edits it\n * directly — no page/layout wrapper. Undo/redo snapshot this root.\n */\n past: ComponentNode[]\n present: ComponentNode\n future: ComponentNode[]\n selectedId: string | null\n hoveredId: string | null\n viewport: Viewport\n /** Monotonic counter feeding fresh node ids. */\n nodeSeq: number\n /** Coalescing key of the last edit (consecutive same-key edits don't stack history). */\n lastCoalesceKey: string | null\n}\n\ntype Action = BuilderAction\n\ninterface Counter {\n genId: () => string\n count: number\n}\nfunction counter(start: number): Counter {\n const c: Counter = {\n count: start,\n genId: () => {\n c.count += 1\n return `node-${c.count}`\n },\n }\n return c\n}\n\n/**\n * Push a new graph onto history, coalescing consecutive edits with the same key.\n *\n * Every commit normalizes layout containers (EC-162 ledger fix): the\n * operations layer normalizes every persisted `graph.*` write (EC-161), so\n * the optimistic working graph must apply the same `normalizeContainers` pass\n * or the UI state diverges from the persisted revision after\n * container-emptying moves/removes. The mirrored command stays un-normalized\n * (`graphInvocationFor` sees the raw action); both sides normalize\n * identically, so they land on the same graph.\n */\nfunction commit(\n state: BuilderState,\n root: ComponentNode,\n opts: { coalesceKey?: string; nodeSeq?: number; selectedId?: string | null } = {},\n): BuilderState {\n const normalized = normalizeContainers(root)\n const present = normalized\n const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey\n // Normalization can dissolve the node a caller wanted selected (e.g. the\n // parent container of a removed child) — selection never points at a ghost.\n const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId\n const selectedId = desired !== null && getNode(normalized, desired) ? desired : null\n return {\n ...state,\n past: coalesce ? state.past : [...state.past, state.present].slice(-HISTORY_LIMIT),\n present,\n future: [],\n nodeSeq: opts.nodeSeq ?? state.nodeSeq,\n lastCoalesceKey: opts.coalesceKey ?? null,\n selectedId,\n }\n}\n\n/**\n * The pure state transition. Exported (with `initialBuilderState`) for unit\n * tests — gesture determinism and normalization lockstep are reducer-level\n * properties, not render behavior.\n */\nexport function builderReducer(state: BuilderState, action: Action): BuilderState {\n const root = state.present\n switch (action.type) {\n case 'select':\n return { ...state, selectedId: action.id }\n case 'hover':\n return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id }\n case 'viewport':\n return { ...state, viewport: action.viewport }\n\n case 'undo': {\n const prev = state.past.at(-1)\n if (!prev) return state\n return {\n ...state,\n past: state.past.slice(0, -1),\n present: prev,\n future: [state.present, ...state.future],\n lastCoalesceKey: null,\n selectedId: getNode(prev, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n case 'redo': {\n const next = state.future[0]\n if (!next) return state\n return {\n ...state,\n past: [...state.past, state.present],\n present: next,\n future: state.future.slice(1),\n lastCoalesceKey: null,\n selectedId: getNode(next, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n\n case 'insert': {\n const c = counter(state.nodeSeq)\n const node = cloneWithNewIds(action.node, c.genId)\n const next = insertNode(root, action.target, node)\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : node.id,\n })\n }\n case 'remove': {\n const parent = getParentInfo(root, action.id)\n const next = removeNode(root, action.id)\n const selectedId =\n state.selectedId === action.id ? (parent?.parent.id ?? null) : state.selectedId\n return commit(state, next, { selectedId })\n }\n case 'duplicate': {\n const c = counter(state.nodeSeq)\n const { root: next, newId } = duplicateNode(root, action.id, c.genId)\n return commit(state, next, { nodeSeq: c.count, selectedId: newId ?? state.selectedId })\n }\n case 'reorder':\n return commit(state, reorderNode(root, action.id, action.delta))\n case 'move':\n return commit(state, moveNode(root, action.id, action.target))\n case 'wrap': {\n // Layout gesture (EC-162): the reference and the dragged subject become\n // the two columns of a new Grid container — ONE structural edit, one\n // history entry, one mirrored command, so a single undo dissolves the\n // whole gesture.\n const c = counter(state.nodeSeq)\n const partner =\n action.subject.kind === 'node'\n ? { kind: 'node' as const, nodeId: action.subject.nodeId }\n : { kind: 'subtree' as const, node: cloneWithNewIds(action.subject.node, c.genId) }\n const containerId = c.genId()\n const wrapped = applyWrap({\n root,\n referenceId: action.referenceId,\n side: action.side,\n partner,\n containerId,\n })\n if (!wrapped) return state\n return commit(state, wrapped.root, { nodeSeq: c.count, selectedId: containerId })\n }\n case 'setProp':\n return commit(state, setNodeProp(root, action.id, action.name, action.value), {\n coalesceKey: action.coalesceKey,\n })\n case 'removeProp':\n return commit(state, removeNodeProp(root, action.id, action.name))\n default:\n return state\n }\n}\n\nexport interface BuilderStore extends BuilderState {\n canUndo: boolean\n canRedo: boolean\n selectedNode: ComponentNode | null\n dispatch: (action: Action) => void\n}\n\nconst BuilderContext = createContext<BuilderStore | null>(null)\n\nexport function initialBuilderState(composition: ComponentNode): BuilderState {\n // Start container-normal (idempotent on healthy trees) so the session and the\n // lifted baseline agree from the first edit.\n const present = normalizeContainers(composition)\n return {\n past: [],\n present,\n future: [],\n selectedId: present.id,\n hoveredId: null,\n viewport: 'desktop',\n // Seed past the ids the composition already carries (EC-203): a restored\n // tree holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`\n // on the first insert and produce a duplicate-node-id.\n nodeSeq: maxNodeIdSeq(present),\n lastCoalesceKey: null,\n }\n}\n\n/**\n * Undo/redo design (EC-141): the history stack is *UI state* — an in-memory\n * stack of composition-root snapshots. EC-287 retired the DB-resident project\n * graph and its append-only revision log, so the store no longer mirrors edits onto a\n * persistence layer: it is a pure in-memory editor. The block-field editor lifts\n * its committed composition into the open CMS document via `onChange`\n * (`composition-field-editor`); the document's own Save persists it as content.\n */\nexport function BuilderProvider({\n composition,\n children,\n}: {\n /** The composition root to edit (the composition-field editor's synthetic root). */\n composition: ComponentNode\n children: ReactNode\n}) {\n const [state, setState] = useState<BuilderState>(() => initialBuilderState(composition))\n const stateRef = useRef(state)\n\n // Plain-function dispatch (not `useReducer`) so each action sees the pre/post\n // state synchronously. EC-287: nothing persists a graph — committed edits are\n // lifted into the CMS document by the composition-field editor's `onChange`.\n const dispatch = useCallback((action: Action) => {\n const next = builderReducer(stateRef.current, action)\n stateRef.current = next\n setState(next)\n }, [])\n\n const store = useMemo<BuilderStore>(() => {\n const selectedNode = state.selectedId\n ? (getNode(state.present, state.selectedId) ?? null)\n : null\n return {\n ...state,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n selectedNode,\n dispatch,\n }\n }, [state, dispatch])\n\n return <BuilderContext.Provider value={store}>{children}</BuilderContext.Provider>\n}\n\nexport function useBuilder(): BuilderStore {\n const store = useContext(BuilderContext)\n if (!store) throw new Error('useBuilder must be used within a <BuilderProvider>.')\n return store\n}\n"]}
1
+ {"version":3,"file":"builder-store.js","sourceRoot":"","sources":["../../../src/components/builder/builder-store.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEzF,OAAO,EACL,eAAe,EACf,aAAa,EACb,OAAO,EACP,aAAa,EACb,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,WAAW,EACX,WAAW,GACZ,MAAM,+BAA+B,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAA;AAEhE,MAAM,aAAa,GAAG,GAAG,CAAA;AAiEzB,SAAS,OAAO,CAAC,KAAa;IAC5B,MAAM,CAAC,GAAY;QACjB,KAAK,EAAE,KAAK;QACZ,KAAK,EAAE,GAAG,EAAE;YACV,CAAC,CAAC,KAAK,IAAI,CAAC,CAAA;YACZ,OAAO,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAA;QAC1B,CAAC;KACF,CAAA;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,MAAM,CACb,KAAmB,EACnB,IAAmB,EACnB,OAA+E,EAAE;IAEjF,MAAM,UAAU,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,OAAO,GAAG,UAAU,CAAA;IAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK,CAAC,eAAe,CAAA;IACvF,yEAAyE;IACzE,4EAA4E;IAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAA;IAClF,MAAM,UAAU,GAAG,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IACpF,OAAO;QACL,GAAG,KAAK;QACR,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC;QAClF,OAAO;QACP,MAAM,EAAE,EAAE;QACV,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO;QACtC,eAAe,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;QACzC,UAAU;KACX,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmB,EAAE,MAAc;IAChE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAA;IAC1B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,QAAQ;YACX,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,CAAA;QAC5C,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,CAAA;QACnF,KAAK,UAAU;YACb,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAA;QAEhD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;YAC9B,IAAI,CAAC,IAAI;gBAAE,OAAO,KAAK,CAAA;YACvB,OAAO;gBACL,GAAG,KAAK;gBACR,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7B,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC;gBACxC,eAAe,EAAE,IAAI;gBACrB,UAAU,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI;aAC5E,CAAA;QACH,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YAC5B,IAAI,CAAC,IAAI;gBAAE,OAAO,KAAK,CAAA;YACvB,OAAO;gBACL,GAAG,KAAK;gBACR,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC;gBACpC,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC7B,eAAe,EAAE,IAAI;gBACrB,UAAU,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI;aAC5E,CAAA;QACH,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAChC,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;YAClD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YAClD,OAAO,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE;gBACzB,OAAO,EAAE,CAAC,CAAC,KAAK;gBAChB,UAAU,EAAE,MAAM,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE;aACjE,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YAC7C,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YACxC,MAAM,UAAU,GACd,KAAK,CAAC,UAAU,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAA;YACjF,OAAO,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,CAAC,CAAA;QAC5C,CAAC;QACD,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAChC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;YACrE,OAAO,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;QACzF,CAAC;QACD,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QAClE,KAAK,MAAM;YACT,OAAO,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;QAChE,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,wEAAwE;YACxE,qEAAqE;YACrE,sEAAsE;YACtE,iBAAiB;YACjB,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAChC,MAAM,OAAO,GACX,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM;gBAC5B,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE;gBAC1D,CAAC,CAAC,EAAE,IAAI,EAAE,SAAkB,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAA;YACvF,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,EAAE,CAAA;YAC7B,MAAM,OAAO,GAAG,SAAS,CAAC;gBACxB,IAAI;gBACJ,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,OAAO;gBACP,WAAW;aACZ,CAAC,CAAA;YACF,IAAI,CAAC,OAAO;gBAAE,OAAO,KAAK,CAAA;YAC1B,OAAO,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAA;QACnF,CAAC;QACD,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE;gBAC5E,WAAW,EAAE,MAAM,CAAC,WAAW;aAChC,CAAC,CAAA;QACJ,KAAK,YAAY;YACf,OAAO,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;QACpE;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC;AASD,MAAM,cAAc,GAAG,aAAa,CAAsB,IAAI,CAAC,CAAA;AAE/D,MAAM,UAAU,mBAAmB,CAAC,WAA0B;IAC5D,8EAA8E;IAC9E,6CAA6C;IAC7C,MAAM,OAAO,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAA;IAChD,OAAO;QACL,IAAI,EAAE,EAAE;QACR,OAAO;QACP,MAAM,EAAE,EAAE;QACV,UAAU,EAAE,OAAO,CAAC,EAAE;QACtB,SAAS,EAAE,IAAI;QACf,QAAQ,EAAE,MAAM;QAChB,yEAAyE;QACzE,4EAA4E;QAC5E,uDAAuD;QACvD,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC;QAC9B,eAAe,EAAE,IAAI;KACtB,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,WAAW,EACX,QAAQ,GAKT;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,GAAG,EAAE,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAE9B,8EAA8E;IAC9E,8EAA8E;IAC9E,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,MAAc,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QACrD,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACvB,QAAQ,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CAAe,GAAG,EAAE;QACvC,MAAM,YAAY,GAAG,KAAK,CAAC,UAAU;YACnC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;YACpD,CAAC,CAAC,IAAI,CAAA;QACR,OAAO;YACL,GAAG,KAAK;YACR,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAC9B,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;YAChC,YAAY;YACZ,QAAQ;SACT,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;IAErB,OAAO,KAAC,cAAc,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA2B,CAAA;AACpF,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,KAAK,GAAG,UAAU,CAAC,cAAc,CAAC,CAAA;IACxC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;IAClF,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n cloneWithNewIds,\n duplicateNode,\n getNode,\n getParentInfo,\n insertNode,\n maxNodeIdSeq,\n moveNode,\n normalizeContainers,\n removeNode,\n removeNodeProp,\n reorderNode,\n setNodeProp,\n} from '@elytracms/core/project-graph'\nimport type { ComponentNode, InsertTarget, PropValue } from '@elytracms/core/project-graph'\nimport { applyWrap } from '@elytracms/studio/lib/dnd/drop-rules'\n\nconst HISTORY_LIMIT = 100\n\n/**\n * A canvas preview-width key (EC-250): `'full'` (unconstrained) or a configured\n * breakpoint name (`'sm'`, `'md'`, …). Dynamic because the set is sourced from\n * the consuming site's breakpoints, so it is a string, not a fixed union — the\n * switcher resolves the available options via {@link resolveCanvasViewports}.\n */\nexport type Viewport = string\n\n/**\n * The builder store's action union. EC-287 retired the DB-resident project graph\n * and its persistence mirror, so these actions are pure in-memory editor state —\n * no action maps to a persisted `graph.*` command anymore. The block-field editor\n * lifts committed edits into the CMS document via `onChange`\n * (`composition-field-editor`); nothing persists a graph.\n */\nexport type BuilderAction =\n | { type: 'select'; id: string | null }\n | { type: 'hover'; id: string | null }\n | { type: 'viewport'; viewport: Viewport }\n | { type: 'undo' }\n | { type: 'redo' }\n | { type: 'insert'; target: InsertTarget; node: ComponentNode; select?: boolean }\n | { type: 'remove'; id: string }\n | { type: 'duplicate'; id: string }\n | { type: 'reorder'; id: string; delta: -1 | 1 }\n | { type: 'move'; id: string; target: InsertTarget }\n /**\n * EC-162 layout gesture: wrap the reference node and the dragged subject in\n * a two-column Grid container as ONE atomic structural edit (one undo step).\n */\n | {\n type: 'wrap'\n referenceId: string\n side: 'left' | 'right'\n subject: { kind: 'node'; nodeId: string } | { kind: 'component'; node: ComponentNode }\n }\n | { type: 'setProp'; id: string; name: string; value: PropValue; coalesceKey?: string }\n | { type: 'removeProp'; id: string; name: string }\n\nexport interface BuilderState {\n /**\n * The edited composition: one root-bearing component tree (EC-287). The canvas\n * wraps a page document's `body` blocks into a synthetic root and edits it\n * directly — no page/layout wrapper. Undo/redo snapshot this root.\n */\n past: ComponentNode[]\n present: ComponentNode\n future: ComponentNode[]\n selectedId: string | null\n hoveredId: string | null\n viewport: Viewport\n /** Monotonic counter feeding fresh node ids. */\n nodeSeq: number\n /** Coalescing key of the last edit (consecutive same-key edits don't stack history). */\n lastCoalesceKey: string | null\n}\n\ntype Action = BuilderAction\n\ninterface Counter {\n genId: () => string\n count: number\n}\nfunction counter(start: number): Counter {\n const c: Counter = {\n count: start,\n genId: () => {\n c.count += 1\n return `node-${c.count}`\n },\n }\n return c\n}\n\n/**\n * Push a new graph onto history, coalescing consecutive edits with the same key.\n *\n * Every commit normalizes layout containers (EC-162 ledger fix): the\n * operations layer normalizes every persisted `graph.*` write (EC-161), so\n * the optimistic working graph must apply the same `normalizeContainers` pass\n * or the UI state diverges from the persisted revision after\n * container-emptying moves/removes. The mirrored command stays un-normalized\n * (`graphInvocationFor` sees the raw action); both sides normalize\n * identically, so they land on the same graph.\n */\nfunction commit(\n state: BuilderState,\n root: ComponentNode,\n opts: { coalesceKey?: string; nodeSeq?: number; selectedId?: string | null } = {},\n): BuilderState {\n const normalized = normalizeContainers(root)\n const present = normalized\n const coalesce = opts.coalesceKey != null && opts.coalesceKey === state.lastCoalesceKey\n // Normalization can dissolve the node a caller wanted selected (e.g. the\n // parent container of a removed child) — selection never points at a ghost.\n const desired = opts.selectedId !== undefined ? opts.selectedId : state.selectedId\n const selectedId = desired !== null && getNode(normalized, desired) ? desired : null\n return {\n ...state,\n past: coalesce ? state.past : [...state.past, state.present].slice(-HISTORY_LIMIT),\n present,\n future: [],\n nodeSeq: opts.nodeSeq ?? state.nodeSeq,\n lastCoalesceKey: opts.coalesceKey ?? null,\n selectedId,\n }\n}\n\n/**\n * The pure state transition. Exported (with `initialBuilderState`) for unit\n * tests — gesture determinism and normalization lockstep are reducer-level\n * properties, not render behavior.\n */\nexport function builderReducer(state: BuilderState, action: Action): BuilderState {\n const root = state.present\n switch (action.type) {\n case 'select':\n return { ...state, selectedId: action.id }\n case 'hover':\n return state.hoveredId === action.id ? state : { ...state, hoveredId: action.id }\n case 'viewport':\n return { ...state, viewport: action.viewport }\n\n case 'undo': {\n const prev = state.past.at(-1)\n if (!prev) return state\n return {\n ...state,\n past: state.past.slice(0, -1),\n present: prev,\n future: [state.present, ...state.future],\n lastCoalesceKey: null,\n selectedId: getNode(prev, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n case 'redo': {\n const next = state.future[0]\n if (!next) return state\n return {\n ...state,\n past: [...state.past, state.present],\n present: next,\n future: state.future.slice(1),\n lastCoalesceKey: null,\n selectedId: getNode(next, state.selectedId ?? '') ? state.selectedId : null,\n }\n }\n\n case 'insert': {\n const c = counter(state.nodeSeq)\n const node = cloneWithNewIds(action.node, c.genId)\n const next = insertNode(root, action.target, node)\n return commit(state, next, {\n nodeSeq: c.count,\n selectedId: action.select === false ? state.selectedId : node.id,\n })\n }\n case 'remove': {\n const parent = getParentInfo(root, action.id)\n const next = removeNode(root, action.id)\n const selectedId =\n state.selectedId === action.id ? (parent?.parent.id ?? null) : state.selectedId\n return commit(state, next, { selectedId })\n }\n case 'duplicate': {\n const c = counter(state.nodeSeq)\n const { root: next, newId } = duplicateNode(root, action.id, c.genId)\n return commit(state, next, { nodeSeq: c.count, selectedId: newId ?? state.selectedId })\n }\n case 'reorder':\n return commit(state, reorderNode(root, action.id, action.delta))\n case 'move':\n return commit(state, moveNode(root, action.id, action.target))\n case 'wrap': {\n // Layout gesture (EC-162): the reference and the dragged subject become\n // the two columns of a new Grid container — ONE structural edit, one\n // history entry, one mirrored command, so a single undo dissolves the\n // whole gesture.\n const c = counter(state.nodeSeq)\n const partner =\n action.subject.kind === 'node'\n ? { kind: 'node' as const, nodeId: action.subject.nodeId }\n : { kind: 'subtree' as const, node: cloneWithNewIds(action.subject.node, c.genId) }\n const containerId = c.genId()\n const wrapped = applyWrap({\n root,\n referenceId: action.referenceId,\n side: action.side,\n partner,\n containerId,\n })\n if (!wrapped) return state\n return commit(state, wrapped.root, { nodeSeq: c.count, selectedId: containerId })\n }\n case 'setProp':\n return commit(state, setNodeProp(root, action.id, action.name, action.value), {\n coalesceKey: action.coalesceKey,\n })\n case 'removeProp':\n return commit(state, removeNodeProp(root, action.id, action.name))\n default:\n return state\n }\n}\n\nexport interface BuilderStore extends BuilderState {\n canUndo: boolean\n canRedo: boolean\n selectedNode: ComponentNode | null\n dispatch: (action: Action) => void\n}\n\nconst BuilderContext = createContext<BuilderStore | null>(null)\n\nexport function initialBuilderState(composition: ComponentNode): BuilderState {\n // Start container-normal (idempotent on healthy trees) so the session and the\n // lifted baseline agree from the first edit.\n const present = normalizeContainers(composition)\n return {\n past: [],\n present,\n future: [],\n selectedId: present.id,\n hoveredId: null,\n viewport: 'full',\n // Seed past the ids the composition already carries (EC-203): a restored\n // tree holds minted `node-<n>` ids, so starting at 0 would re-mint `node-1`\n // on the first insert and produce a duplicate-node-id.\n nodeSeq: maxNodeIdSeq(present),\n lastCoalesceKey: null,\n }\n}\n\n/**\n * Undo/redo design (EC-141): the history stack is *UI state* — an in-memory\n * stack of composition-root snapshots. EC-287 retired the DB-resident project\n * graph and its append-only revision log, so the store no longer mirrors edits onto a\n * persistence layer: it is a pure in-memory editor. The block-field editor lifts\n * its committed composition into the open CMS document via `onChange`\n * (`composition-field-editor`); the document's own Save persists it as content.\n */\nexport function BuilderProvider({\n composition,\n children,\n}: {\n /** The composition root to edit (the composition-field editor's synthetic root). */\n composition: ComponentNode\n children: ReactNode\n}) {\n const [state, setState] = useState<BuilderState>(() => initialBuilderState(composition))\n const stateRef = useRef(state)\n\n // Plain-function dispatch (not `useReducer`) so each action sees the pre/post\n // state synchronously. EC-287: nothing persists a graph — committed edits are\n // lifted into the CMS document by the composition-field editor's `onChange`.\n const dispatch = useCallback((action: Action) => {\n const next = builderReducer(stateRef.current, action)\n stateRef.current = next\n setState(next)\n }, [])\n\n const store = useMemo<BuilderStore>(() => {\n const selectedNode = state.selectedId\n ? (getNode(state.present, state.selectedId) ?? null)\n : null\n return {\n ...state,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n selectedNode,\n dispatch,\n }\n }, [state, dispatch])\n\n return <BuilderContext.Provider value={store}>{children}</BuilderContext.Provider>\n}\n\nexport function useBuilder(): BuilderStore {\n const store = useContext(BuilderContext)\n if (!store) throw new Error('useBuilder must be used within a <BuilderProvider>.')\n return store\n}\n"]}
@@ -0,0 +1,53 @@
1
+ import type { BreakpointConfig } from '@elytracms/core/studio-core';
2
+ /**
3
+ * EC-250 — canvas breakpoint preview, sourced from the consuming site's real
4
+ * widths instead of a baked-in desktop/tablet/phone triple.
5
+ *
6
+ * A `Viewport` is a preview-width control only (v1, EC-211): switching one
7
+ * resizes the preview frame so the author can verify components' OWN responsive
8
+ * behavior. It is NOT per-breakpoint authoring and writes no graph command.
9
+ */
10
+ /** One option the breakpoint switcher offers: an id, a label, and a CSS width. */
11
+ export interface ViewportOption {
12
+ /** The `builder.viewport` key this option sets ('full' or a breakpoint name). */
13
+ key: string;
14
+ label: string;
15
+ /** CSS width the canvas frame takes: '100%' for full, else `${width}px`. */
16
+ width: string;
17
+ /** Numeric min-width in px (absent for the full-width option). */
18
+ px?: number;
19
+ }
20
+ /**
21
+ * Tailwind's default `screens` scale. The documented fallback when a project
22
+ * declares no `breakpoints`: an unconfigured Tailwind site uses exactly these,
23
+ * so it is the site's real screens — not the arbitrary legacy triple.
24
+ */
25
+ export declare const TAILWIND_DEFAULT_SCREENS: readonly BreakpointConfig[];
26
+ export interface ResolvedViewports {
27
+ /** Full first, then each breakpoint ascending by width. Never empty. */
28
+ viewports: ViewportOption[];
29
+ /**
30
+ * A visible validation issue (no silent fallback) when a project DECLARED
31
+ * breakpoints but none are usable — the switcher shows Full only, with this
32
+ * surfaced beside it. Absent in the common (config or Tailwind-default) case.
33
+ */
34
+ issue?: string;
35
+ }
36
+ /**
37
+ * Resolve the breakpoint switcher's options from a project's configured
38
+ * breakpoints (EC-250):
39
+ * - `undefined` (none declared) → Tailwind's default screens, no issue,
40
+ * - declared + at least one usable → those (sorted ascending),
41
+ * - declared but none usable (empty / non-positive widths) → Full only + a
42
+ * visible issue, never the legacy desktop/tablet/phone triple.
43
+ * The full-width option is always present and first. Deterministic.
44
+ */
45
+ export declare function resolveCanvasViewports(configBreakpoints: readonly BreakpointConfig[] | undefined): ResolvedViewports;
46
+ /** The CSS width for the active viewport key, falling back to full. */
47
+ export declare function viewportWidth(viewports: ViewportOption[], activeKey: string): string;
48
+ /**
49
+ * The resolved breakpoint switcher for the active config project. The config is
50
+ * static per studio boot, so this memoizes on nothing — both the toolbar switcher
51
+ * and the canvas frame read the SAME resolved set (no drift).
52
+ */
53
+ export declare function useCanvasViewports(): ResolvedViewports;
@@ -0,0 +1,55 @@
1
+ import { useMemo } from 'react';
2
+ import { resolveDeploymentBreakpoints } from '@elytracms/studio/lib/workspace';
3
+ /**
4
+ * Tailwind's default `screens` scale. The documented fallback when a project
5
+ * declares no `breakpoints`: an unconfigured Tailwind site uses exactly these,
6
+ * so it is the site's real screens — not the arbitrary legacy triple.
7
+ */
8
+ export const TAILWIND_DEFAULT_SCREENS = [
9
+ { name: 'sm', width: 640 },
10
+ { name: 'md', width: 768 },
11
+ { name: 'lg', width: 1024 },
12
+ { name: 'xl', width: 1280 },
13
+ { name: '2xl', width: 1536 },
14
+ ];
15
+ /** The always-present unconstrained canvas width (the natural default). */
16
+ const FULL = { key: 'full', label: 'Full', width: '100%' };
17
+ /**
18
+ * Resolve the breakpoint switcher's options from a project's configured
19
+ * breakpoints (EC-250):
20
+ * - `undefined` (none declared) → Tailwind's default screens, no issue,
21
+ * - declared + at least one usable → those (sorted ascending),
22
+ * - declared but none usable (empty / non-positive widths) → Full only + a
23
+ * visible issue, never the legacy desktop/tablet/phone triple.
24
+ * The full-width option is always present and first. Deterministic.
25
+ */
26
+ export function resolveCanvasViewports(configBreakpoints) {
27
+ const source = configBreakpoints ?? TAILWIND_DEFAULT_SCREENS;
28
+ const usable = source.filter((b) => Number.isFinite(b.width) && b.width > 0);
29
+ if (configBreakpoints !== undefined && usable.length === 0) {
30
+ return {
31
+ viewports: [FULL],
32
+ issue: 'No usable canvas breakpoints — `breakpoints` is configured but empty or has no positive widths. Each needs a positive `width` in px.',
33
+ };
34
+ }
35
+ const sorted = [...usable].sort((a, b) => a.width - b.width);
36
+ return {
37
+ viewports: [
38
+ FULL,
39
+ ...sorted.map((b) => ({ key: b.name, label: b.label ?? b.name, width: `${b.width}px`, px: b.width })),
40
+ ],
41
+ };
42
+ }
43
+ /** The CSS width for the active viewport key, falling back to full. */
44
+ export function viewportWidth(viewports, activeKey) {
45
+ return (viewports.find((v) => v.key === activeKey) ?? viewports[0]).width;
46
+ }
47
+ /**
48
+ * The resolved breakpoint switcher for the active config project. The config is
49
+ * static per studio boot, so this memoizes on nothing — both the toolbar switcher
50
+ * and the canvas frame read the SAME resolved set (no drift).
51
+ */
52
+ export function useCanvasViewports() {
53
+ return useMemo(() => resolveCanvasViewports(resolveDeploymentBreakpoints()), []);
54
+ }
55
+ //# sourceMappingURL=canvas-breakpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-breakpoints.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-breakpoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,4BAA4B,EAAE,MAAM,iCAAiC,CAAA;AAsB9E;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAgC;IACnE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE;IAC1B,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE;IAC1B,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC3B,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAC3B,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;CAC7B,CAAA;AAED,2EAA2E;AAC3E,MAAM,IAAI,GAAmB,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AAa1E;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,iBAA0D;IAE1D,MAAM,MAAM,GAAG,iBAAiB,IAAI,wBAAwB,CAAA;IAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;IAC5E,IAAI,iBAAiB,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,OAAO;YACL,SAAS,EAAE,CAAC,IAAI,CAAC;YACjB,KAAK,EACH,sIAAsI;SACzI,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;IAC5D,OAAO;QACL,SAAS,EAAE;YACT,IAAI;YACJ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;SACtG;KACF,CAAA;AACH,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,SAA2B,EAAE,SAAiB;IAC1E,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,IAAI,SAAS,CAAC,CAAC,CAAE,CAAC,CAAC,KAAK,CAAA;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,4BAA4B,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport type { BreakpointConfig } from '@elytracms/core/studio-core'\nimport { resolveDeploymentBreakpoints } from '@elytracms/studio/lib/workspace'\n\n/**\n * EC-250 — canvas breakpoint preview, sourced from the consuming site's real\n * widths instead of a baked-in desktop/tablet/phone triple.\n *\n * A `Viewport` is a preview-width control only (v1, EC-211): switching one\n * resizes the preview frame so the author can verify components' OWN responsive\n * behavior. It is NOT per-breakpoint authoring and writes no graph command.\n */\n\n/** One option the breakpoint switcher offers: an id, a label, and a CSS width. */\nexport interface ViewportOption {\n /** The `builder.viewport` key this option sets ('full' or a breakpoint name). */\n key: string\n label: string\n /** CSS width the canvas frame takes: '100%' for full, else `${width}px`. */\n width: string\n /** Numeric min-width in px (absent for the full-width option). */\n px?: number\n}\n\n/**\n * Tailwind's default `screens` scale. The documented fallback when a project\n * declares no `breakpoints`: an unconfigured Tailwind site uses exactly these,\n * so it is the site's real screens — not the arbitrary legacy triple.\n */\nexport const TAILWIND_DEFAULT_SCREENS: readonly BreakpointConfig[] = [\n { name: 'sm', width: 640 },\n { name: 'md', width: 768 },\n { name: 'lg', width: 1024 },\n { name: 'xl', width: 1280 },\n { name: '2xl', width: 1536 },\n]\n\n/** The always-present unconstrained canvas width (the natural default). */\nconst FULL: ViewportOption = { key: 'full', label: 'Full', width: '100%' }\n\nexport interface ResolvedViewports {\n /** Full first, then each breakpoint ascending by width. Never empty. */\n viewports: ViewportOption[]\n /**\n * A visible validation issue (no silent fallback) when a project DECLARED\n * breakpoints but none are usable — the switcher shows Full only, with this\n * surfaced beside it. Absent in the common (config or Tailwind-default) case.\n */\n issue?: string\n}\n\n/**\n * Resolve the breakpoint switcher's options from a project's configured\n * breakpoints (EC-250):\n * - `undefined` (none declared) → Tailwind's default screens, no issue,\n * - declared + at least one usable → those (sorted ascending),\n * - declared but none usable (empty / non-positive widths) → Full only + a\n * visible issue, never the legacy desktop/tablet/phone triple.\n * The full-width option is always present and first. Deterministic.\n */\nexport function resolveCanvasViewports(\n configBreakpoints: readonly BreakpointConfig[] | undefined,\n): ResolvedViewports {\n const source = configBreakpoints ?? TAILWIND_DEFAULT_SCREENS\n const usable = source.filter((b) => Number.isFinite(b.width) && b.width > 0)\n if (configBreakpoints !== undefined && usable.length === 0) {\n return {\n viewports: [FULL],\n issue:\n 'No usable canvas breakpoints — `breakpoints` is configured but empty or has no positive widths. Each needs a positive `width` in px.',\n }\n }\n const sorted = [...usable].sort((a, b) => a.width - b.width)\n return {\n viewports: [\n FULL,\n ...sorted.map((b) => ({ key: b.name, label: b.label ?? b.name, width: `${b.width}px`, px: b.width })),\n ],\n }\n}\n\n/** The CSS width for the active viewport key, falling back to full. */\nexport function viewportWidth(viewports: ViewportOption[], activeKey: string): string {\n return (viewports.find((v) => v.key === activeKey) ?? viewports[0]!).width\n}\n\n/**\n * The resolved breakpoint switcher for the active config project. The config is\n * static per studio boot, so this memoizes on nothing — both the toolbar switcher\n * and the canvas frame read the SAME resolved set (no drift).\n */\nexport function useCanvasViewports(): ResolvedViewports {\n return useMemo(() => resolveCanvasViewports(resolveDeploymentBreakpoints()), [])\n}\n"]}
@@ -9,20 +9,16 @@ import { activeDrag, beginDrag, endDrag, onDragSessionEnd } from './drag-session
9
9
  import { PRESERVE_NODE_ATTR, collectPreservedPairs, richTextDocFromDom, } from '@elytracms/core/rich-text';
10
10
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
11
11
  import { useOptionalAssets } from '../assets/assets-store';
12
- import { resolveCanvasListing, resolveCanvasRelation, useData } from '@elytracms/studio/lib/data';
12
+ import { resolveCanvasLink, resolveCanvasListing, resolveCanvasRelation, useData, } from '@elytracms/studio/lib/data';
13
13
  import { cmsBindingFallback, useStudioLocale } from '@elytracms/studio/lib/locale';
14
14
  import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
15
15
  import { getCanvasStylesheetUrl } from './canvas-stylesheet';
16
16
  import { useBuilder } from './builder-store';
17
+ import { useCanvasViewports, viewportWidth } from './canvas-breakpoints';
17
18
  import { ClipboardNoticeOverlay, attachClipboardListeners, useStudioClipboard, } from './clipboard-actions';
18
19
  import { createInlineEditSession, createInlineRichTextSession, findInlineTextHost, resolveInlineEditableProp, resolveInlineRichTextProp, } from './inline-edit';
19
20
  import { RichTextToolbar } from './rich-text-toolbar';
20
21
  import { SpacingHandles, hasSpacingHandles } from './spacing-handles';
21
- const VIEWPORT_WIDTH = {
22
- desktop: '100%',
23
- tablet: '768px',
24
- mobile: '390px',
25
- };
26
22
  /** Selection/hover outlines, scoped to the per-node editable wrappers. */
27
23
  const OUTLINE_CSS = `
28
24
  html { color-scheme: dark; }
@@ -657,6 +653,11 @@ export function BuilderCanvas() {
657
653
  // analog of the host's baked listings map; an empty/invalid query → [] → the
658
654
  // block's own empty state.
659
655
  const resolveListing = useCallback((query) => resolveCanvasListing(query, activeLocale), [activeLocale]);
656
+ // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's
657
+ // URL path from the active content session, in the canvas's active locale — the
658
+ // studio analog of the host's baked links map. A missing/unpublished/unroutable
659
+ // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.
660
+ const resolveLink = useCallback((ref) => resolveCanvasLink(ref, activeLocale), [activeLocale]);
660
661
  const localeFallbackOf = useCallback((node) => {
661
662
  for (const value of Object.values(node.props ?? {})) {
662
663
  if (value.kind !== 'binding')
@@ -674,14 +675,19 @@ export function BuilderCanvas() {
674
675
  resolveAsset,
675
676
  resolveRelation,
676
677
  resolveListing,
678
+ resolveLink,
677
679
  decorateNode: (node, rendered) => pageNodeIds.has(node.id) ? (_jsx("div", { "data-ec-node-id": node.id, className: "ec-editable", "data-selected": node.id === builder.selectedId || undefined, "data-hovered": node.id === builder.hoveredId || undefined, "data-ec-locale-fallback": localeFallbackOf(node), children: rendered }, `${node.id}#${richEditEpochs[node.id] ?? 0}`)) : (rendered),
678
- }), [studio, resolveBinding, resolveAsset, resolveRelation, resolveListing, pageNodeIds, builder.selectedId, builder.hoveredId, localeFallbackOf, richEditEpochs]);
680
+ }), [studio, resolveBinding, resolveAsset, resolveRelation, resolveListing, resolveLink, pageNodeIds, builder.selectedId, builder.hoveredId, localeFallbackOf, richEditEpochs]);
679
681
  // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children
680
682
  // gets draggable gap handles between them, portaled into the iframe.
681
683
  const spacingTarget = doc && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)
682
684
  ? builder.selectedNode
683
685
  : null;
684
- return (_jsxs("div", { className: "canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6", children: [_jsx(ClipboardNoticeOverlay, {}), _jsxs("div", { style: { width: VIEWPORT_WIDTH[builder.viewport], maxWidth: '100%' }, className: "flex", children: [_jsx("iframe", { ref: iframeRef, onLoad: wireUp, title: "Page preview", className: "min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md", srcDoc: "<!doctype html><html><head></head><body></body></html>" }), doc
686
+ // EC-250: the preview frame width derives from the config-sourced breakpoints
687
+ // (Tailwind defaults when unconfigured), the same resolved set the toolbar
688
+ // switcher offers.
689
+ const { viewports } = useCanvasViewports();
690
+ return (_jsxs("div", { className: "canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6", children: [_jsx(ClipboardNoticeOverlay, {}), _jsxs("div", { style: { width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }, className: "flex", children: [_jsx("iframe", { ref: iframeRef, onLoad: wireUp, title: "Page preview", className: "min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md", srcDoc: "<!doctype html><html><head></head><body></body></html>" }), doc
685
691
  ? createPortal(
686
692
  // Render the composition root directly (EC-287: no layout
687
693
  // indirection — a page renders its blocks).