@kywi-software/core 0.10.0 → 0.12.0

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 (248) hide show
  1. package/AGENT-PATTERNS.md +111 -23
  2. package/README.md +67 -0
  3. package/dist/admin/content-form.d.ts.map +1 -1
  4. package/dist/admin/content-form.js +97 -10
  5. package/dist/admin/content-form.js.map +1 -1
  6. package/dist/admin/layout-editor/component-actions.d.ts +219 -22
  7. package/dist/admin/layout-editor/component-actions.d.ts.map +1 -1
  8. package/dist/admin/layout-editor/component-actions.js +356 -36
  9. package/dist/admin/layout-editor/component-actions.js.map +1 -1
  10. package/dist/admin/layout-editor/component-fork.d.ts +224 -0
  11. package/dist/admin/layout-editor/component-fork.d.ts.map +1 -0
  12. package/dist/admin/layout-editor/component-fork.js +342 -0
  13. package/dist/admin/layout-editor/component-fork.js.map +1 -0
  14. package/dist/admin/layout-editor/editor-canvas.d.ts +28 -7
  15. package/dist/admin/layout-editor/editor-canvas.d.ts.map +1 -1
  16. package/dist/admin/layout-editor/editor-canvas.js +145 -32
  17. package/dist/admin/layout-editor/editor-canvas.js.map +1 -1
  18. package/dist/admin/layout-editor/editor-dialog.d.ts +164 -0
  19. package/dist/admin/layout-editor/editor-dialog.d.ts.map +1 -0
  20. package/dist/admin/layout-editor/editor-dialog.js +195 -0
  21. package/dist/admin/layout-editor/editor-dialog.js.map +1 -0
  22. package/dist/admin/layout-editor/editor-state.d.ts +76 -0
  23. package/dist/admin/layout-editor/editor-state.d.ts.map +1 -1
  24. package/dist/admin/layout-editor/editor-state.js +47 -1
  25. package/dist/admin/layout-editor/editor-state.js.map +1 -1
  26. package/dist/admin/layout-editor/editor.css +366 -4
  27. package/dist/admin/layout-editor/index.d.ts +10 -1
  28. package/dist/admin/layout-editor/index.d.ts.map +1 -1
  29. package/dist/admin/layout-editor/index.js +11 -1
  30. package/dist/admin/layout-editor/index.js.map +1 -1
  31. package/dist/admin/layout-editor/inline-text.d.ts +56 -4
  32. package/dist/admin/layout-editor/inline-text.d.ts.map +1 -1
  33. package/dist/admin/layout-editor/inline-text.js +134 -11
  34. package/dist/admin/layout-editor/inline-text.js.map +1 -1
  35. package/dist/admin/layout-editor/layout-editor.d.ts +58 -1
  36. package/dist/admin/layout-editor/layout-editor.d.ts.map +1 -1
  37. package/dist/admin/layout-editor/layout-editor.js +160 -190
  38. package/dist/admin/layout-editor/layout-editor.js.map +1 -1
  39. package/dist/admin/layout-editor/linked-component.d.ts +39 -0
  40. package/dist/admin/layout-editor/linked-component.d.ts.map +1 -0
  41. package/dist/admin/layout-editor/linked-component.js +42 -0
  42. package/dist/admin/layout-editor/linked-component.js.map +1 -0
  43. package/dist/admin/layout-editor/module-overlay.d.ts +36 -14
  44. package/dist/admin/layout-editor/module-overlay.d.ts.map +1 -1
  45. package/dist/admin/layout-editor/module-overlay.js +233 -25
  46. package/dist/admin/layout-editor/module-overlay.js.map +1 -1
  47. package/dist/admin/layout-editor/node-lookup.d.ts +35 -0
  48. package/dist/admin/layout-editor/node-lookup.d.ts.map +1 -1
  49. package/dist/admin/layout-editor/node-lookup.js +91 -0
  50. package/dist/admin/layout-editor/node-lookup.js.map +1 -1
  51. package/dist/admin/layout-editor/overlay-shell.d.ts.map +1 -1
  52. package/dist/admin/layout-editor/overlay-shell.js +150 -32
  53. package/dist/admin/layout-editor/overlay-shell.js.map +1 -1
  54. package/dist/admin/layout-editor/props-panel.d.ts +54 -1
  55. package/dist/admin/layout-editor/props-panel.d.ts.map +1 -1
  56. package/dist/admin/layout-editor/props-panel.js +119 -10
  57. package/dist/admin/layout-editor/props-panel.js.map +1 -1
  58. package/dist/admin/layout-editor/section-overlay.d.ts +16 -1
  59. package/dist/admin/layout-editor/section-overlay.d.ts.map +1 -1
  60. package/dist/admin/layout-editor/section-overlay.js +21 -11
  61. package/dist/admin/layout-editor/section-overlay.js.map +1 -1
  62. package/dist/admin/layout-editor/section-template-picker.d.ts +27 -1
  63. package/dist/admin/layout-editor/section-template-picker.d.ts.map +1 -1
  64. package/dist/admin/layout-editor/section-template-picker.js +71 -17
  65. package/dist/admin/layout-editor/section-template-picker.js.map +1 -1
  66. package/dist/admin/layout-editor/section-variant-container-config.js +5 -5
  67. package/dist/admin/layout-editor/section-variant-container-config.js.map +1 -1
  68. package/dist/admin/layout-editor/use-chrome-flip.d.ts +111 -11
  69. package/dist/admin/layout-editor/use-chrome-flip.d.ts.map +1 -1
  70. package/dist/admin/layout-editor/use-chrome-flip.js +233 -33
  71. package/dist/admin/layout-editor/use-chrome-flip.js.map +1 -1
  72. package/dist/admin/layout-editor/use-component-connect.d.ts +61 -0
  73. package/dist/admin/layout-editor/use-component-connect.d.ts.map +1 -0
  74. package/dist/admin/layout-editor/use-component-connect.js +292 -0
  75. package/dist/admin/layout-editor/use-component-connect.js.map +1 -0
  76. package/dist/admin/layout-editor/use-component-fork.d.ts +123 -0
  77. package/dist/admin/layout-editor/use-component-fork.d.ts.map +1 -0
  78. package/dist/admin/layout-editor/use-component-fork.js +346 -0
  79. package/dist/admin/layout-editor/use-component-fork.js.map +1 -0
  80. package/dist/admin/layout-editor/use-component-library.d.ts +94 -0
  81. package/dist/admin/layout-editor/use-component-library.d.ts.map +1 -0
  82. package/dist/admin/layout-editor/use-component-library.js +55 -0
  83. package/dist/admin/layout-editor/use-component-library.js.map +1 -0
  84. package/dist/admin/registry.d.ts +11 -2
  85. package/dist/admin/registry.d.ts.map +1 -1
  86. package/dist/admin/registry.js +40 -4
  87. package/dist/admin/registry.js.map +1 -1
  88. package/dist/admin/server.d.ts +1 -0
  89. package/dist/admin/server.d.ts.map +1 -1
  90. package/dist/admin/server.js +5 -0
  91. package/dist/admin/server.js.map +1 -1
  92. package/dist/admin/styles/admin.css +57 -1
  93. package/dist/admin/surfaces/components/component-edit.d.ts +3 -0
  94. package/dist/admin/surfaces/components/component-edit.d.ts.map +1 -0
  95. package/dist/admin/surfaces/components/component-edit.js +235 -0
  96. package/dist/admin/surfaces/components/component-edit.js.map +1 -0
  97. package/dist/admin/surfaces/components/components-data.d.ts +153 -0
  98. package/dist/admin/surfaces/components/components-data.d.ts.map +1 -0
  99. package/dist/admin/surfaces/components/components-data.js +154 -0
  100. package/dist/admin/surfaces/components/components-data.js.map +1 -0
  101. package/dist/admin/surfaces/components/components-list.d.ts +3 -0
  102. package/dist/admin/surfaces/components/components-list.d.ts.map +1 -0
  103. package/dist/admin/surfaces/components/components-list.js +196 -0
  104. package/dist/admin/surfaces/components/components-list.js.map +1 -0
  105. package/dist/admin/surfaces/components/definition-canvas.d.ts +73 -0
  106. package/dist/admin/surfaces/components/definition-canvas.d.ts.map +1 -0
  107. package/dist/admin/surfaces/components/definition-canvas.js +207 -0
  108. package/dist/admin/surfaces/components/definition-canvas.js.map +1 -0
  109. package/dist/admin/surfaces/layouts/layout-editor-client.d.ts.map +1 -1
  110. package/dist/admin/surfaces/layouts/layout-editor-client.js +14 -2
  111. package/dist/admin/surfaces/layouts/layout-editor-client.js.map +1 -1
  112. package/dist/admin/surfaces/layouts/layouts-data.d.ts +3 -14
  113. package/dist/admin/surfaces/layouts/layouts-data.d.ts.map +1 -1
  114. package/dist/admin/surfaces/layouts/layouts-data.js +4 -7
  115. package/dist/admin/surfaces/layouts/layouts-data.js.map +1 -1
  116. package/dist/admin/surfaces/layouts/layouts-list.d.ts.map +1 -1
  117. package/dist/admin/surfaces/layouts/layouts-list.js +16 -22
  118. package/dist/admin/surfaces/layouts/layouts-list.js.map +1 -1
  119. package/dist/admin/surfaces/settings/admin-features.d.ts.map +1 -1
  120. package/dist/admin/surfaces/settings/admin-features.js +1 -0
  121. package/dist/admin/surfaces/settings/admin-features.js.map +1 -1
  122. package/dist/api/bulk-routes.d.ts.map +1 -1
  123. package/dist/api/bulk-routes.js +25 -0
  124. package/dist/api/bulk-routes.js.map +1 -1
  125. package/dist/api/changeset-routes.d.ts.map +1 -1
  126. package/dist/api/changeset-routes.js +17 -3
  127. package/dist/api/changeset-routes.js.map +1 -1
  128. package/dist/api/component-routes.d.ts.map +1 -1
  129. package/dist/api/component-routes.js +228 -8
  130. package/dist/api/component-routes.js.map +1 -1
  131. package/dist/api/content-routes.d.ts.map +1 -1
  132. package/dist/api/content-routes.js +64 -0
  133. package/dist/api/content-routes.js.map +1 -1
  134. package/dist/api/feed-routes.d.ts +6 -6
  135. package/dist/api/graphql/resolvers.d.ts +4 -4
  136. package/dist/api/layout-routes.d.ts.map +1 -1
  137. package/dist/api/layout-routes.js +4 -17
  138. package/dist/api/layout-routes.js.map +1 -1
  139. package/dist/api/layout-write-guard.d.ts +80 -0
  140. package/dist/api/layout-write-guard.d.ts.map +1 -0
  141. package/dist/api/layout-write-guard.js +88 -0
  142. package/dist/api/layout-write-guard.js.map +1 -0
  143. package/dist/api/section-template-routes.d.ts.map +1 -1
  144. package/dist/api/section-template-routes.js +11 -0
  145. package/dist/api/section-template-routes.js.map +1 -1
  146. package/dist/api/translation-routes.d.ts.map +1 -1
  147. package/dist/api/translation-routes.js +8 -0
  148. package/dist/api/translation-routes.js.map +1 -1
  149. package/dist/api/version-routes.d.ts.map +1 -1
  150. package/dist/api/version-routes.js +20 -8
  151. package/dist/api/version-routes.js.map +1 -1
  152. package/dist/api/workflow-routes.d.ts.map +1 -1
  153. package/dist/api/workflow-routes.js +53 -8
  154. package/dist/api/workflow-routes.js.map +1 -1
  155. package/dist/db/analytics-queries.d.ts +3 -3
  156. package/dist/db/apply-snapshot.d.ts +75 -8
  157. package/dist/db/apply-snapshot.d.ts.map +1 -1
  158. package/dist/db/apply-snapshot.js +104 -8
  159. package/dist/db/apply-snapshot.js.map +1 -1
  160. package/dist/db/audit-queries.d.ts +12 -12
  161. package/dist/db/category-queries.d.ts +3 -3
  162. package/dist/db/changeset-audit-queries.d.ts +4 -4
  163. package/dist/db/changeset-queries.d.ts +7 -7
  164. package/dist/db/comment-queries.d.ts +8 -8
  165. package/dist/db/comment-subscription-queries.d.ts +2 -2
  166. package/dist/db/component-instance-queries.d.ts +197 -0
  167. package/dist/db/component-instance-queries.d.ts.map +1 -0
  168. package/dist/db/component-instance-queries.js +405 -0
  169. package/dist/db/component-instance-queries.js.map +1 -0
  170. package/dist/db/content-queries.d.ts.map +1 -1
  171. package/dist/db/content-queries.js +39 -2
  172. package/dist/db/content-queries.js.map +1 -1
  173. package/dist/db/extension-queries.d.ts +7 -7
  174. package/dist/db/feed-queries.d.ts +3 -3
  175. package/dist/db/index.d.ts +1 -0
  176. package/dist/db/index.d.ts.map +1 -1
  177. package/dist/db/index.js +1 -0
  178. package/dist/db/index.js.map +1 -1
  179. package/dist/db/layout-queries.d.ts +13 -1
  180. package/dist/db/layout-queries.d.ts.map +1 -1
  181. package/dist/db/layout-queries.js +52 -7
  182. package/dist/db/layout-queries.js.map +1 -1
  183. package/dist/db/locale-queries.d.ts.map +1 -1
  184. package/dist/db/locale-queries.js +0 -0
  185. package/dist/db/locale-queries.js.map +1 -1
  186. package/dist/db/media-queries.d.ts +12 -12
  187. package/dist/db/revision-queries.d.ts +3 -3
  188. package/dist/db/site-admin-queries.d.ts +2 -2
  189. package/dist/db/site-membership-queries.d.ts +3 -3
  190. package/dist/db/site-plugin-queries.d.ts +2 -2
  191. package/dist/db/site-queries.d.ts +6 -6
  192. package/dist/db/version-queries.d.ts +12 -12
  193. package/dist/db/webhook-queries.d.ts +11 -11
  194. package/dist/db/workflow-queries.d.ts +6 -6
  195. package/dist/db/workflow-queries.d.ts.map +1 -1
  196. package/dist/db/workflow-queries.js +11 -2
  197. package/dist/db/workflow-queries.js.map +1 -1
  198. package/dist/layout/comments-module.d.ts.map +1 -1
  199. package/dist/layout/comments-module.js +6 -2
  200. package/dist/layout/comments-module.js.map +1 -1
  201. package/dist/layout/component-refs.d.ts +465 -0
  202. package/dist/layout/component-refs.d.ts.map +1 -0
  203. package/dist/layout/component-refs.js +848 -0
  204. package/dist/layout/component-refs.js.map +1 -0
  205. package/dist/layout/index.d.ts +2 -1
  206. package/dist/layout/index.d.ts.map +1 -1
  207. package/dist/layout/index.js +1 -0
  208. package/dist/layout/index.js.map +1 -1
  209. package/dist/layout/module-permissions.d.ts +49 -1
  210. package/dist/layout/module-permissions.d.ts.map +1 -1
  211. package/dist/layout/module-permissions.js +117 -6
  212. package/dist/layout/module-permissions.js.map +1 -1
  213. package/dist/layout/module-render-context.d.ts +32 -0
  214. package/dist/layout/module-render-context.d.ts.map +1 -1
  215. package/dist/layout/module-render-context.js +32 -1
  216. package/dist/layout/module-render-context.js.map +1 -1
  217. package/dist/layout/modules-extended.d.ts.map +1 -1
  218. package/dist/layout/modules-extended.js +21 -11
  219. package/dist/layout/modules-extended.js.map +1 -1
  220. package/dist/layout/modules.d.ts.map +1 -1
  221. package/dist/layout/modules.js +15 -10
  222. package/dist/layout/modules.js.map +1 -1
  223. package/dist/layout/renderer.d.ts +51 -4
  224. package/dist/layout/renderer.d.ts.map +1 -1
  225. package/dist/layout/renderer.js +36 -38
  226. package/dist/layout/renderer.js.map +1 -1
  227. package/dist/layout/types.d.ts +42 -0
  228. package/dist/layout/types.d.ts.map +1 -1
  229. package/dist/layout/types.js +54 -11
  230. package/dist/layout/types.js.map +1 -1
  231. package/dist/schema/base.d.ts +1 -1
  232. package/dist/schema/components.d.ts +150 -0
  233. package/dist/schema/components.d.ts.map +1 -1
  234. package/dist/schema/components.js +50 -4
  235. package/dist/schema/components.js.map +1 -1
  236. package/dist/scope/inline-editing.d.ts.map +1 -1
  237. package/dist/scope/inline-editing.js +10 -0
  238. package/dist/scope/inline-editing.js.map +1 -1
  239. package/dist/scope/sticky-offset.d.ts +185 -0
  240. package/dist/scope/sticky-offset.d.ts.map +1 -0
  241. package/dist/scope/sticky-offset.js +432 -0
  242. package/dist/scope/sticky-offset.js.map +1 -0
  243. package/dist/site/styles.css +41 -3
  244. package/dist/versioning/scheduler.d.ts +8 -0
  245. package/dist/versioning/scheduler.d.ts.map +1 -1
  246. package/dist/versioning/scheduler.js +21 -7
  247. package/dist/versioning/scheduler.js.map +1 -1
  248. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ import type { LayoutDocument, LayoutSection, ModuleNode, VariantContainer } from '../../layout/types.js';
2
+ import { type ComponentDetachSource, type ComponentInstance, type ComponentPlacementLookup, type ComponentRef, type ComponentRefKind } from '../../layout/component-refs.js';
3
+ /**
4
+ * component-fork.ts
5
+ * The pure half of the save fork (kywi-cms#147 T4): which linked instances the
6
+ * author has EDITED, and what each of the two answers writes.
7
+ *
8
+ * THE FEATURE, in one paragraph. A linked instance is editable in place like any
9
+ * other content. At save time the editor notices that an instance no longer
10
+ * matches the component it points at and asks: **Save and update all instances**
11
+ * (the edit becomes the component's definition — every instance everywhere,
12
+ * published pages included, renders it from the next request on) or **Save to
13
+ * this page only** (this instance stops being an instance: it keeps what the
14
+ * author sees and drops the link). There are no per-prop overrides in v1 — the
15
+ * link is strict (spec decision 5) — so those two are the only coherent answers,
16
+ * and the editor must not persist a third.
17
+ *
18
+ * Everything here is pure and layout-shaped: no fetch, no React, no dialog. The
19
+ * hook that drives it lives in `use-component-fork.tsx`.
20
+ */
21
+ /** One linked instance whose content no longer matches its baseline. */
22
+ export interface LinkedInstanceEdit {
23
+ componentId: string;
24
+ nodeId: string;
25
+ kind: ComponentRefKind;
26
+ /** The instance as the editor holds it now. */
27
+ instance: ComponentInstance;
28
+ /** The instance as it was when the editor and the server last agreed. */
29
+ baseline: ComponentInstance;
30
+ }
31
+ /**
32
+ * A baseline for a placement the STORED document does not have.
33
+ *
34
+ * Consulted only for links that appeared during this session — a node just
35
+ * connected by "save as component", or a component just placed from the picker.
36
+ * Both write the definition (or a clone of it) at that moment, so the instance
37
+ * starts out matching its component and only a LATER edit makes it dirty; the
38
+ * shell records what it looked like at that moment and hands it back through
39
+ * this. Without it, "save as component, then fix the typo you just noticed"
40
+ * would save the typo fix into the page as a divergent fallback copy that the
41
+ * page never renders — an edit silently lost, which is the worst outcome this
42
+ * feature can produce.
43
+ */
44
+ export type LinkedInstanceBaseline = (ref: ComponentRef) => ComponentInstance | null | undefined;
45
+ /**
46
+ * Which linked instances in `current` differ from their stored selves.
47
+ *
48
+ * WHAT COUNTS AS AN EDIT OF THE INSTANCE (and what deliberately does not):
49
+ *
50
+ * - **Anything inside a linked section or container counts.** The whole subtree
51
+ * IS the instance: a section component resolves wholesale, so retyping a
52
+ * heading three levels down is an edit of the component's content, not of the
53
+ * page's.
54
+ * - **Position does not count.** Which region a linked node sits in, and where
55
+ * among its siblings, belongs to the PAGE, not to the component — identity is
56
+ * (componentId, nodeId), so moving or reordering an instance finds the same
57
+ * baseline and compares equal. Same for the placement's own `id`.
58
+ * - **A variant container's `experimentId` / `winnerId` do not count.** They
59
+ * bind THIS page's container to THIS page's experiment run, which is why
60
+ * `applyVariantContainerPlacement` gives the placement's binding priority over
61
+ * the definition's, and why the rail keeps them editable on a linked
62
+ * container. Counting them would open the fork on "declare arm B the winner"
63
+ * and then write nothing.
64
+ * - **A link that only exists in `current` is not an edit.** It was created this
65
+ * session, from the definition — see {@link LinkedInstanceBaseline} for the
66
+ * one case where a baseline is supplied anyway.
67
+ * - **A link that only exists in `stored` is not an edit.** It was detached (or
68
+ * deleted) this session; there is no instance left to ask about.
69
+ *
70
+ * `stored` is the last-saved document (the editor's `originalLayout`), not the
71
+ * document the session opened with: after a successful save the baseline moves,
72
+ * so a second round of edits forks again.
73
+ *
74
+ * The comparison is over the STORED node, not over what it renders, so it is
75
+ * conservative in one direction: typing a prop and then typing the component's
76
+ * own value back leaves an override that reads the same as the definition, and
77
+ * that still counts as an edit. The fork then asks a question whose "update all"
78
+ * answer writes a byte-identical definition. Conservative is the right way round
79
+ * here — the failure it rules out is a MISSED edit, which loses work.
80
+ */
81
+ export declare function diffLinkedInstances(stored: LayoutDocument | null | undefined, current: LayoutDocument | null | undefined, fallbackBaseline?: LinkedInstanceBaseline): LinkedInstanceEdit[];
82
+ /**
83
+ * Do these two instances hold the same CONTENT — everything the component owns,
84
+ * nothing the placement owns? See {@link diffLinkedInstances} for the rules and
85
+ * why each exclusion is there.
86
+ */
87
+ export declare function linkedInstanceContentEquals(a: ComponentInstance, b: ComponentInstance): boolean;
88
+ /** What the two writes of an update-all look like, built from ONE source of truth. */
89
+ export interface ForkUpdateAll {
90
+ /**
91
+ * The `definition` for `PUT /components/:id`. Deliberately the only field the
92
+ * fork sends: `moduleType` decides how the definition is READ, and a linked
93
+ * node's own `type` is not a reliable source for it (a component placed from
94
+ * the palette picker is a `component` placeholder node), so a fork that sent
95
+ * one could quietly re-type the component.
96
+ */
97
+ definition: Record<string, unknown>;
98
+ /**
99
+ * What the page stores in place of the edited node once the definition holds
100
+ * the edit — a clean instance again, so the strict link holds and the next
101
+ * diff sees no edit. Built from the same flattened node as `definition`, which
102
+ * is what makes "the fallback copy shows what the link resolves to" true.
103
+ */
104
+ cleanInstance: ModuleNode | LayoutSection | VariantContainer;
105
+ }
106
+ /**
107
+ * Turn an edited instance into the component definition it should become, plus
108
+ * the clean instance the page keeps. Returns null when that cannot be done
109
+ * honestly (see the module case).
110
+ *
111
+ * THE INVERSE OF PLACEMENT, kind by kind:
112
+ *
113
+ * - **module** — the node holds only OVERRIDES (a clean instance holds nothing
114
+ * but the link), so the definition is what this placement currently RENDERS:
115
+ * `resolveModulePlacement(node, resolved)`, the very function the renderer and
116
+ * the canvas preview call. That needs the current definition, so an
117
+ * unresolvable component returns null rather than shipping the overrides alone
118
+ * — which would silently wipe every prop the author never touched. The caller
119
+ * offers only "this page only" in that case.
120
+ * - **section** — the node holds the WHOLE content (its inline copy is the
121
+ * definition's, as edited), so the definition is that content with the link
122
+ * dropped and the PLACEMENT'S SCOPING undone (`unapplySectionPlacement`, the
123
+ * exact inverse of the `applySectionPlacement` the copy was materialised
124
+ * with). That needs the current definition too — for its root id, and because
125
+ * a definition the editor could not read is one whose copy it cannot vouch
126
+ * for — so an unresolvable component returns null here as well.
127
+ * - **variantContainer** — as a section, minus the experiment binding: the
128
+ * definition keeps whatever binding it was SAVED with and never absorbs this
129
+ * placement's. `buildConnectedVariantContainerNode` drops the definition's
130
+ * binding when creating a placement precisely so a component cannot enrol a
131
+ * second page in the first page's experiment; the inverse has to hold, or
132
+ * "update all" would do exactly that in reverse. Arms, audiences, weights and
133
+ * `mode` ARE the component's, and travel. An unresolvable container source
134
+ * returns null rather than emitting a definition with NO binding at all,
135
+ * which would silently clear the component's own experiment (T4 review, I3).
136
+ *
137
+ * Nested links are materialised first (`flattenComponentLinks`) because the API
138
+ * refuses a definition that links to another component — the same reason
139
+ * "save as component" flattens. The instance's OWN link is dropped before
140
+ * flattening runs, or the flattening would materialise it and hand back the
141
+ * STORED definition, throwing away the very edit being saved.
142
+ *
143
+ * IDS ARE STABLE ACROSS ROUNDS (kywi-cms#147 T4 review, C1). `cleanInstance` is
144
+ * built by re-applying the NEW definition to this placement rather than by keeping
145
+ * the edited node, so it is byte-identical to what the next editor session would
146
+ * refresh the copy to (`refreshComponentCopies`), and the definition's own
147
+ * ids never accumulate the placement's scope. The compatibility surface documented
148
+ * at `rekeyResolvedVariantContainer` still applies to ids the author's edit
149
+ * introduces or removes, but an update-all that changes only content now leaves
150
+ * every other instance's rendered ids exactly where they were.
151
+ */
152
+ export declare function buildUpdateAll(edit: LinkedInstanceEdit, options?: {
153
+ /** The component's current stored definition, as materialisation reads it. */
154
+ source?: ComponentDetachSource | null | undefined;
155
+ /** Resolves links INSIDE the selection, for the flattening. */
156
+ resolveLink?: ComponentPlacementLookup;
157
+ }): ForkUpdateAll | null;
158
+ /**
159
+ * Would an update-all built from this edit's BASELINE reproduce the component's
160
+ * current definition — or has the definition moved on since this page stored its
161
+ * copy? (kywi-cms#147 T4 review, C1, the belt.)
162
+ *
163
+ * WHY THIS IS NEEDED EVEN WITH THE REFRESH (`refreshComponentCopies` in
164
+ * `layout/component-refs.ts`). That pass brings every
165
+ * copy in line the moment the library loads, which makes staleness impossible for
166
+ * the ordinary session. It cannot help when there IS no definition to refresh
167
+ * from — the library fetch failed, the row was deleted — or when the library
168
+ * arrived after the document was already dirty, where refreshing would clobber the
169
+ * author's edits and is therefore skipped. In those cases the baseline is whatever
170
+ * the page happened to store, and building the definition out of it would revert
171
+ * every newer edit of that component everywhere. So update-all is WITHDRAWN and
172
+ * the owner is told to save locally and re-edit from an up-to-date page.
173
+ *
174
+ * MODULES ARE EXEMPT, and not by oversight: a module placement stores only
175
+ * OVERRIDES, and its definition is built as `resolveModulePlacement(node,
176
+ * currentDefinition)` — the current definition is read on the spot, so untouched
177
+ * props come from it and nothing can be reverted. There is no copy to be stale.
178
+ *
179
+ * The comparison ignores ids at every structural level. An unrefreshed baseline
180
+ * holds the ids its clone was given rather than the ones a fresh materialisation
181
+ * derives, and refusing update-all over an id difference — with content that
182
+ * agrees — would withdraw the feature's whole point on a technicality.
183
+ */
184
+ export declare function isBaselineStale(edit: LinkedInstanceEdit, source?: ComponentDetachSource | null | undefined): boolean;
185
+ /**
186
+ * The node the page stores when the author keeps the edit here: an independent
187
+ * copy of WHAT THEY SEE, with the link gone.
188
+ *
189
+ * HOW THIS DIFFERS FROM THE RAIL'S "Detach from component", and why it must.
190
+ * Rail-detach materialises the component's CURRENT definition into the node
191
+ * (`detachSectionFromComponent` → `applySectionPlacement`) — the right answer
192
+ * there, because that node is showing the definition and detaching must not
193
+ * change the page. Here the node is NOT showing the definition: the author has
194
+ * edited it, and their edit is the whole point of the save. Materialising the
195
+ * stored definition would discard it. So:
196
+ *
197
+ * - **section / container** — keep the node's current subtree, drop the link.
198
+ * Nothing is flattened: a link that was nested inside this instance becomes a
199
+ * real, visible placement of its own the moment the outer link goes (that is
200
+ * exactly what `collectComponentRefs` starts reporting), so the index, the
201
+ * usage counts and the renderer all pick it up. Flattening it would silently
202
+ * turn a live link into a copy.
203
+ * - **module** — the node holds only overrides, so what it renders is the
204
+ * definition merged with them: `materializeModuleNode`, the canonical
205
+ * materialisation the renderer and the server's detach-all share. The local
206
+ * edits survive because the merge puts them on top. The `componentId` echo in
207
+ * `props` is stripped: it is a link marker, and there is no link left. (An
208
+ * unresolvable component degrades to the node's own props alone — again
209
+ * exactly what such an instance renders today.)
210
+ */
211
+ export declare function detachInstanceKeepingEdits(edit: LinkedInstanceEdit, source?: ComponentDetachSource | null | undefined): ModuleNode | LayoutSection | VariantContainer;
212
+ /**
213
+ * Replace the named placements in a document, keyed by
214
+ * {@link componentRefKey} — the identity everything else in this feature uses.
215
+ *
216
+ * Goes through `rewriteComponentInstances` (the canonical traversal) rather than
217
+ * walking the tree here, so the fork can only ever rewrite nodes the instance
218
+ * index and the usage counts agree are placements — including the ones sitting in
219
+ * page variants and A/B experiment regions, which the reducer's REPLACE_* actions
220
+ * cannot reach (they address the ACTIVE regions only). That is why the fork
221
+ * applies one whole document rather than a series of node swaps.
222
+ */
223
+ export declare function applyInstanceReplacements(layout: LayoutDocument, replacements: Map<string, ModuleNode | LayoutSection | VariantContainer>): LayoutDocument;
224
+ //# sourceMappingURL=component-fork.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-fork.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/component-fork.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,UAAU,EACV,gBAAgB,EACjB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAUL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACtB,MAAM,gCAAgC,CAAA;AAGvC;;;;;;;;;;;;;;;;;GAiBG;AAIH,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;IACtB,+CAA+C;IAC/C,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,yEAAyE;IACzE,QAAQ,EAAE,iBAAiB,CAAA;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAA;AAEhG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EACzC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC1C,gBAAgB,CAAC,EAAE,sBAAsB,GACxC,kBAAkB,EAAE,CAsBtB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAG/F;AA4DD,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC;;;;;OAKG;IACH,aAAa,EAAE,UAAU,GAAG,aAAa,GAAG,gBAAgB,CAAA;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,kBAAkB,EACxB,OAAO,GAAE;IACP,8EAA8E;IAC9E,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,CAAA;IACjD,+DAA+D;IAC/D,WAAW,CAAC,EAAE,wBAAwB,CAAA;CAClC,GACL,aAAa,GAAG,IAAI,CAsDtB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,kBAAkB,EACxB,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,GAChD,OAAO,CAeT;AA2CD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,kBAAkB,EACxB,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,GAChD,UAAU,GAAG,aAAa,GAAG,gBAAgB,CAU/C;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,GAAG,gBAAgB,CAAC,GACvE,cAAc,CAShB"}
@@ -0,0 +1,342 @@
1
+ import { applySectionPlacement, applyVariantContainerPlacement, collectComponentInstances, componentRefKey, materializeModuleNode, resolveModulePlacement, rewriteComponentInstances, unapplySectionPlacement, unapplyVariantContainerPlacement, } from '../../layout/component-refs.js';
2
+ import { flattenComponentLinks } from './component-actions.js';
3
+ /**
4
+ * Which linked instances in `current` differ from their stored selves.
5
+ *
6
+ * WHAT COUNTS AS AN EDIT OF THE INSTANCE (and what deliberately does not):
7
+ *
8
+ * - **Anything inside a linked section or container counts.** The whole subtree
9
+ * IS the instance: a section component resolves wholesale, so retyping a
10
+ * heading three levels down is an edit of the component's content, not of the
11
+ * page's.
12
+ * - **Position does not count.** Which region a linked node sits in, and where
13
+ * among its siblings, belongs to the PAGE, not to the component — identity is
14
+ * (componentId, nodeId), so moving or reordering an instance finds the same
15
+ * baseline and compares equal. Same for the placement's own `id`.
16
+ * - **A variant container's `experimentId` / `winnerId` do not count.** They
17
+ * bind THIS page's container to THIS page's experiment run, which is why
18
+ * `applyVariantContainerPlacement` gives the placement's binding priority over
19
+ * the definition's, and why the rail keeps them editable on a linked
20
+ * container. Counting them would open the fork on "declare arm B the winner"
21
+ * and then write nothing.
22
+ * - **A link that only exists in `current` is not an edit.** It was created this
23
+ * session, from the definition — see {@link LinkedInstanceBaseline} for the
24
+ * one case where a baseline is supplied anyway.
25
+ * - **A link that only exists in `stored` is not an edit.** It was detached (or
26
+ * deleted) this session; there is no instance left to ask about.
27
+ *
28
+ * `stored` is the last-saved document (the editor's `originalLayout`), not the
29
+ * document the session opened with: after a successful save the baseline moves,
30
+ * so a second round of edits forks again.
31
+ *
32
+ * The comparison is over the STORED node, not over what it renders, so it is
33
+ * conservative in one direction: typing a prop and then typing the component's
34
+ * own value back leaves an override that reads the same as the definition, and
35
+ * that still counts as an edit. The fork then asks a question whose "update all"
36
+ * answer writes a byte-identical definition. Conservative is the right way round
37
+ * here — the failure it rules out is a MISSED edit, which loses work.
38
+ */
39
+ export function diffLinkedInstances(stored, current, fallbackBaseline) {
40
+ const storedInstances = new Map(collectComponentInstances(stored).map(instance => [componentRefKey(instance), instance]));
41
+ const edits = [];
42
+ for (const instance of collectComponentInstances(current)) {
43
+ const baseline = storedInstances.get(componentRefKey(instance)) ?? fallbackBaseline?.(instance) ?? null;
44
+ // A baseline of a different kind is not a baseline: the same node id now
45
+ // holds a different sort of node (a section replaced by a container, say),
46
+ // which is a new placement rather than an edited one.
47
+ if (!baseline || baseline.kind !== instance.kind)
48
+ continue;
49
+ if (linkedInstanceContentEquals(baseline, instance))
50
+ continue;
51
+ edits.push({
52
+ componentId: instance.componentId,
53
+ nodeId: instance.nodeId,
54
+ kind: instance.kind,
55
+ instance,
56
+ baseline,
57
+ });
58
+ }
59
+ return edits;
60
+ }
61
+ /**
62
+ * Do these two instances hold the same CONTENT — everything the component owns,
63
+ * nothing the placement owns? See {@link diffLinkedInstances} for the rules and
64
+ * why each exclusion is there.
65
+ */
66
+ export function linkedInstanceContentEquals(a, b) {
67
+ if (a.kind !== b.kind)
68
+ return false;
69
+ return sameContent(instanceContent(a), instanceContent(b));
70
+ }
71
+ function instanceContent(instance) {
72
+ if (instance.kind === 'module') {
73
+ // `id`/`instanceId` are the placement's; the `componentId` echo in `props` is
74
+ // a link marker rather than content (see `resolveModulePlacement`).
75
+ return {
76
+ type: instance.node.type,
77
+ props: withoutComponentIdProp(instance.node.props ?? {}),
78
+ style: instance.node.style ?? {},
79
+ };
80
+ }
81
+ if (instance.kind === 'section') {
82
+ const { id: _placementId, componentId: _link, ...content } = instance.node;
83
+ return content;
84
+ }
85
+ const { id: _placementId, componentId: _link, experimentId: _thisPagesRun, winnerId: _thisPagesWinner, ...content } = instance.node;
86
+ return content;
87
+ }
88
+ /** Key order is not content: two prop maps written in different orders agree. */
89
+ function canonical(value) {
90
+ if (Array.isArray(value))
91
+ return value.map(canonical);
92
+ if (value !== null && typeof value === 'object') {
93
+ const source = value;
94
+ const out = {};
95
+ for (const key of Object.keys(source).sort()) {
96
+ // An explicitly-undefined key and a missing one are the same absence; only
97
+ // one of the two survives a jsonb round trip, so they must compare equal.
98
+ if (source[key] === undefined)
99
+ continue;
100
+ out[key] = canonical(source[key]);
101
+ }
102
+ return out;
103
+ }
104
+ return value;
105
+ }
106
+ function sameContent(a, b) {
107
+ return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
108
+ }
109
+ function withoutComponentIdProp(props) {
110
+ if (!('componentId' in props))
111
+ return props;
112
+ const { componentId: _echo, ...rest } = props;
113
+ return rest;
114
+ }
115
+ function withoutLink(node) {
116
+ const { componentId: _dropped, ...rest } = node;
117
+ return rest;
118
+ }
119
+ /**
120
+ * Turn an edited instance into the component definition it should become, plus
121
+ * the clean instance the page keeps. Returns null when that cannot be done
122
+ * honestly (see the module case).
123
+ *
124
+ * THE INVERSE OF PLACEMENT, kind by kind:
125
+ *
126
+ * - **module** — the node holds only OVERRIDES (a clean instance holds nothing
127
+ * but the link), so the definition is what this placement currently RENDERS:
128
+ * `resolveModulePlacement(node, resolved)`, the very function the renderer and
129
+ * the canvas preview call. That needs the current definition, so an
130
+ * unresolvable component returns null rather than shipping the overrides alone
131
+ * — which would silently wipe every prop the author never touched. The caller
132
+ * offers only "this page only" in that case.
133
+ * - **section** — the node holds the WHOLE content (its inline copy is the
134
+ * definition's, as edited), so the definition is that content with the link
135
+ * dropped and the PLACEMENT'S SCOPING undone (`unapplySectionPlacement`, the
136
+ * exact inverse of the `applySectionPlacement` the copy was materialised
137
+ * with). That needs the current definition too — for its root id, and because
138
+ * a definition the editor could not read is one whose copy it cannot vouch
139
+ * for — so an unresolvable component returns null here as well.
140
+ * - **variantContainer** — as a section, minus the experiment binding: the
141
+ * definition keeps whatever binding it was SAVED with and never absorbs this
142
+ * placement's. `buildConnectedVariantContainerNode` drops the definition's
143
+ * binding when creating a placement precisely so a component cannot enrol a
144
+ * second page in the first page's experiment; the inverse has to hold, or
145
+ * "update all" would do exactly that in reverse. Arms, audiences, weights and
146
+ * `mode` ARE the component's, and travel. An unresolvable container source
147
+ * returns null rather than emitting a definition with NO binding at all,
148
+ * which would silently clear the component's own experiment (T4 review, I3).
149
+ *
150
+ * Nested links are materialised first (`flattenComponentLinks`) because the API
151
+ * refuses a definition that links to another component — the same reason
152
+ * "save as component" flattens. The instance's OWN link is dropped before
153
+ * flattening runs, or the flattening would materialise it and hand back the
154
+ * STORED definition, throwing away the very edit being saved.
155
+ *
156
+ * IDS ARE STABLE ACROSS ROUNDS (kywi-cms#147 T4 review, C1). `cleanInstance` is
157
+ * built by re-applying the NEW definition to this placement rather than by keeping
158
+ * the edited node, so it is byte-identical to what the next editor session would
159
+ * refresh the copy to (`refreshComponentCopies`), and the definition's own
160
+ * ids never accumulate the placement's scope. The compatibility surface documented
161
+ * at `rekeyResolvedVariantContainer` still applies to ids the author's edit
162
+ * introduces or removes, but an update-all that changes only content now leaves
163
+ * every other instance's rendered ids exactly where they were.
164
+ */
165
+ export function buildUpdateAll(edit, options = {}) {
166
+ const { source, resolveLink } = options;
167
+ if (edit.instance.kind === 'module') {
168
+ if (!source || source.kind !== 'module')
169
+ return null;
170
+ const node = edit.instance.node;
171
+ const rendered = resolveModulePlacement(node, source);
172
+ return {
173
+ definition: {
174
+ props: withoutComponentIdProp(rendered.props),
175
+ style: { ...rendered.style },
176
+ },
177
+ cleanInstance: {
178
+ ...node,
179
+ componentId: edit.componentId,
180
+ props: { componentId: edit.componentId },
181
+ style: {},
182
+ },
183
+ };
184
+ }
185
+ if (edit.instance.kind === 'section') {
186
+ if (!source || source.kind !== 'section')
187
+ return null;
188
+ const placement = edit.instance.node;
189
+ const flattened = flattenComponentLinks(withoutLink(placement), resolveLink);
190
+ const definition = unapplySectionPlacement(flattened, source.section.id);
191
+ return {
192
+ definition: definition,
193
+ cleanInstance: {
194
+ ...applySectionPlacement(definition, placement),
195
+ componentId: edit.componentId,
196
+ },
197
+ };
198
+ }
199
+ if (!source || source.kind !== 'variantContainer')
200
+ return null;
201
+ const placement = edit.instance.node;
202
+ const stored = source.container;
203
+ const flattened = flattenComponentLinks(withoutLink(placement), resolveLink);
204
+ const definition = {
205
+ ...unapplyVariantContainerPlacement(flattened, stored.id),
206
+ ...(stored.experimentId !== undefined ? { experimentId: stored.experimentId } : {}),
207
+ ...(stored.winnerId !== undefined ? { winnerId: stored.winnerId } : {}),
208
+ };
209
+ return {
210
+ definition: definition,
211
+ // The placement keeps its own binding — that is the half of the container
212
+ // this page owns, and `applyVariantContainerPlacement` is where that rule
213
+ // lives.
214
+ cleanInstance: {
215
+ ...applyVariantContainerPlacement(definition, placement),
216
+ componentId: edit.componentId,
217
+ },
218
+ };
219
+ }
220
+ // ── Is the page's copy of the component out of date? ─────────────────────────
221
+ /**
222
+ * Would an update-all built from this edit's BASELINE reproduce the component's
223
+ * current definition — or has the definition moved on since this page stored its
224
+ * copy? (kywi-cms#147 T4 review, C1, the belt.)
225
+ *
226
+ * WHY THIS IS NEEDED EVEN WITH THE REFRESH (`refreshComponentCopies` in
227
+ * `layout/component-refs.ts`). That pass brings every
228
+ * copy in line the moment the library loads, which makes staleness impossible for
229
+ * the ordinary session. It cannot help when there IS no definition to refresh
230
+ * from — the library fetch failed, the row was deleted — or when the library
231
+ * arrived after the document was already dirty, where refreshing would clobber the
232
+ * author's edits and is therefore skipped. In those cases the baseline is whatever
233
+ * the page happened to store, and building the definition out of it would revert
234
+ * every newer edit of that component everywhere. So update-all is WITHDRAWN and
235
+ * the owner is told to save locally and re-edit from an up-to-date page.
236
+ *
237
+ * MODULES ARE EXEMPT, and not by oversight: a module placement stores only
238
+ * OVERRIDES, and its definition is built as `resolveModulePlacement(node,
239
+ * currentDefinition)` — the current definition is read on the spot, so untouched
240
+ * props come from it and nothing can be reverted. There is no copy to be stale.
241
+ *
242
+ * The comparison ignores ids at every structural level. An unrefreshed baseline
243
+ * holds the ids its clone was given rather than the ones a fresh materialisation
244
+ * derives, and refusing update-all over an id difference — with content that
245
+ * agrees — would withdraw the feature's whole point on a technicality.
246
+ */
247
+ export function isBaselineStale(edit, source) {
248
+ if (edit.kind === 'module')
249
+ return false;
250
+ if (edit.baseline.kind === 'section') {
251
+ if (source?.kind !== 'section')
252
+ return true;
253
+ return !sameContent(structuralSection(edit.baseline.node), structuralSection(applySectionPlacement(source.section, edit.baseline.node)));
254
+ }
255
+ if (edit.baseline.kind !== 'variantContainer')
256
+ return true;
257
+ if (source?.kind !== 'variantContainer')
258
+ return true;
259
+ return !sameContent(structuralContainer(edit.baseline.node), structuralContainer(applyVariantContainerPlacement(source.container, edit.baseline.node)));
260
+ }
261
+ /** Drop the keys that identify a node rather than describe it. */
262
+ function withoutIdentity(node) {
263
+ const { id: _placementId, instanceId: _instanceId, componentId: _link, ...rest } = node;
264
+ return rest;
265
+ }
266
+ function structuralSection(section) {
267
+ return {
268
+ ...withoutIdentity(section),
269
+ columns: (section.columns ?? []).map(column => ({
270
+ ...withoutIdentity(column),
271
+ nodes: (column.nodes ?? []).map(node => withoutIdentity(node)),
272
+ })),
273
+ };
274
+ }
275
+ function structuralContainer(container) {
276
+ const { experimentId: _thisPagesRun, winnerId: _thisPagesWinner, ...rest } = container;
277
+ return {
278
+ ...withoutIdentity(rest),
279
+ defaultSections: (container.defaultSections ?? []).map(structuralSection),
280
+ variants: (container.variants ?? []).map(variant => ({
281
+ ...withoutIdentity(variant),
282
+ sections: (variant.sections ?? []).map(structuralSection),
283
+ })),
284
+ };
285
+ }
286
+ // ── "Save to this page only" ─────────────────────────────────────────────────
287
+ /**
288
+ * The node the page stores when the author keeps the edit here: an independent
289
+ * copy of WHAT THEY SEE, with the link gone.
290
+ *
291
+ * HOW THIS DIFFERS FROM THE RAIL'S "Detach from component", and why it must.
292
+ * Rail-detach materialises the component's CURRENT definition into the node
293
+ * (`detachSectionFromComponent` → `applySectionPlacement`) — the right answer
294
+ * there, because that node is showing the definition and detaching must not
295
+ * change the page. Here the node is NOT showing the definition: the author has
296
+ * edited it, and their edit is the whole point of the save. Materialising the
297
+ * stored definition would discard it. So:
298
+ *
299
+ * - **section / container** — keep the node's current subtree, drop the link.
300
+ * Nothing is flattened: a link that was nested inside this instance becomes a
301
+ * real, visible placement of its own the moment the outer link goes (that is
302
+ * exactly what `collectComponentRefs` starts reporting), so the index, the
303
+ * usage counts and the renderer all pick it up. Flattening it would silently
304
+ * turn a live link into a copy.
305
+ * - **module** — the node holds only overrides, so what it renders is the
306
+ * definition merged with them: `materializeModuleNode`, the canonical
307
+ * materialisation the renderer and the server's detach-all share. The local
308
+ * edits survive because the merge puts them on top. The `componentId` echo in
309
+ * `props` is stripped: it is a link marker, and there is no link left. (An
310
+ * unresolvable component degrades to the node's own props alone — again
311
+ * exactly what such an instance renders today.)
312
+ */
313
+ export function detachInstanceKeepingEdits(edit, source) {
314
+ if (edit.instance.kind === 'module') {
315
+ const materialised = materializeModuleNode(edit.instance.node, source ?? { kind: 'unresolvable' }, 'drop');
316
+ return { ...materialised, props: withoutComponentIdProp(materialised.props ?? {}) };
317
+ }
318
+ return withoutLink(edit.instance.node);
319
+ }
320
+ // ── Applying the answers to the document ─────────────────────────────────────
321
+ /**
322
+ * Replace the named placements in a document, keyed by
323
+ * {@link componentRefKey} — the identity everything else in this feature uses.
324
+ *
325
+ * Goes through `rewriteComponentInstances` (the canonical traversal) rather than
326
+ * walking the tree here, so the fork can only ever rewrite nodes the instance
327
+ * index and the usage counts agree are placements — including the ones sitting in
328
+ * page variants and A/B experiment regions, which the reducer's REPLACE_* actions
329
+ * cannot reach (they address the ACTIVE regions only). That is why the fork
330
+ * applies one whole document rather than a series of node swaps.
331
+ */
332
+ export function applyInstanceReplacements(layout, replacements) {
333
+ if (replacements.size === 0)
334
+ return layout;
335
+ const take = (ref) => replacements.get(componentRefKey(ref));
336
+ return rewriteComponentInstances(layout, {
337
+ module: (node, ref) => take(ref) ?? node,
338
+ section: (section, ref) => take(ref) ?? section,
339
+ variantContainer: (container, ref) => take(ref) ?? container,
340
+ });
341
+ }
342
+ //# sourceMappingURL=component-fork.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-fork.js","sourceRoot":"","sources":["../../../src/admin/layout-editor/component-fork.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { EditorState, EditorAction } from './editor-state.js';
3
- import type { InlineTextProp } from './inline-text.js';
3
+ import type { InlineTextProp, InlineTextBindingTarget } from './inline-text.js';
4
+ import type { ComponentAffordances } from './linked-component.js';
4
5
  /** Resolve a connected component node to its live definition for canvas preview. */
5
6
  type ComponentPreviewResolver = (componentId: string) => {
6
7
  props: Record<string, unknown>;
@@ -23,15 +24,19 @@ export interface InlineEditController {
23
24
  resetId: string | null;
24
25
  /** Bumped each time an edit session ends. */
25
26
  nonce: number;
26
- /** The module type's primary text prop, or null when it has none. */
27
- resolveTextProp: (moduleType: string) => InlineTextProp | null;
27
+ /**
28
+ * The element + prop a gesture inside one module's wrapper binds, or null when
29
+ * there is nothing safe to bind (kywi-cms#145 — see `resolveInlineBinding`).
30
+ */
31
+ resolveBinding: (moduleType: string, wrapper: HTMLElement, target: Element | null) => InlineTextBindingTarget | null;
28
32
  onActivate: (moduleId: string) => void;
29
- onCommit: (moduleId: string, host: HTMLElement) => void;
33
+ onCommit: (moduleId: string, host: HTMLElement, prop: InlineTextProp) => void;
30
34
  onCancel: () => void;
31
35
  /**
32
- * Double-click fallback for a module with no primary text prop (kywi-cms#118).
33
- * Selecting is the caller's job (via the module's own `onSelect`); this only
34
- * moves focus into the already-open props rail once that selection commits.
36
+ * Double-click fallback for a gesture with nothing safe to bind
37
+ * (kywi-cms#118, kywi-cms#145). Selecting is the caller's job (via the
38
+ * module's own `onSelect`); this only moves focus into the already-open props
39
+ * rail once that selection commits.
35
40
  */
36
41
  focusPropsRail: () => void;
37
42
  }
@@ -55,8 +60,24 @@ interface EditorCanvasProps {
55
60
  * chips, handles) float above it instead of boxing it into an artboard.
56
61
  */
57
62
  inPlace?: boolean;
63
+ /**
64
+ * Whether the front editor's left module-palette rail is expanded
65
+ * (kywi-cms#132). The rail is fixed to the viewport edge and floats over the
66
+ * page's left margin, so a full-width section's floating chrome renders
67
+ * underneath it and cannot be clicked — the chrome moves out of its way, and
68
+ * this is the only channel that tells it to. Meaningful in in-place mode
69
+ * only: the admin artboard's palette is an in-flow column.
70
+ */
71
+ paletteOpen?: boolean;
58
72
  /** Inline text editing controller, only meaningful in in-place mode. */
59
73
  inlineEdit?: InlineEditController | null;
74
+ /**
75
+ * Reusable-component chrome (kywi-cms#147): "Save as component" actions on
76
+ * section / module / container chrome, and the name for a linked instance's
77
+ * pill. Supplied by whichever shell owns the component library and the save
78
+ * dialog; omitted, the canvas simply renders no component chrome.
79
+ */
80
+ componentAffordances?: ComponentAffordances;
60
81
  }
61
82
  /**
62
83
  * Memoized (kywi-cms#142): the canvas re-renders on every editor-state