@kywi-software/core 0.9.0 → 0.11.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 (87) hide show
  1. package/AGENT-PATTERNS.md +15 -13
  2. package/README.md +67 -0
  3. package/dist/admin/layout-editor/column-overlay.d.ts.map +1 -1
  4. package/dist/admin/layout-editor/column-overlay.js +7 -1
  5. package/dist/admin/layout-editor/column-overlay.js.map +1 -1
  6. package/dist/admin/layout-editor/drop-indicator.d.ts +18 -1
  7. package/dist/admin/layout-editor/drop-indicator.d.ts.map +1 -1
  8. package/dist/admin/layout-editor/drop-indicator.js +23 -3
  9. package/dist/admin/layout-editor/drop-indicator.js.map +1 -1
  10. package/dist/admin/layout-editor/editor-canvas.d.ts +31 -8
  11. package/dist/admin/layout-editor/editor-canvas.d.ts.map +1 -1
  12. package/dist/admin/layout-editor/editor-canvas.js +131 -48
  13. package/dist/admin/layout-editor/editor-canvas.js.map +1 -1
  14. package/dist/admin/layout-editor/editor-state.d.ts.map +1 -1
  15. package/dist/admin/layout-editor/editor-state.js +19 -3
  16. package/dist/admin/layout-editor/editor-state.js.map +1 -1
  17. package/dist/admin/layout-editor/editor.css +106 -8
  18. package/dist/admin/layout-editor/inline-text.d.ts +56 -4
  19. package/dist/admin/layout-editor/inline-text.d.ts.map +1 -1
  20. package/dist/admin/layout-editor/inline-text.js +134 -11
  21. package/dist/admin/layout-editor/inline-text.js.map +1 -1
  22. package/dist/admin/layout-editor/layout-editor.d.ts.map +1 -1
  23. package/dist/admin/layout-editor/layout-editor.js +1 -1
  24. package/dist/admin/layout-editor/layout-editor.js.map +1 -1
  25. package/dist/admin/layout-editor/module-overlay.d.ts +26 -13
  26. package/dist/admin/layout-editor/module-overlay.d.ts.map +1 -1
  27. package/dist/admin/layout-editor/module-overlay.js +206 -22
  28. package/dist/admin/layout-editor/module-overlay.js.map +1 -1
  29. package/dist/admin/layout-editor/overlay-shell.d.ts.map +1 -1
  30. package/dist/admin/layout-editor/overlay-shell.js +39 -12
  31. package/dist/admin/layout-editor/overlay-shell.js.map +1 -1
  32. package/dist/admin/layout-editor/props-panel.js +1 -1
  33. package/dist/admin/layout-editor/props-panel.js.map +1 -1
  34. package/dist/admin/layout-editor/section-overlay.d.ts +8 -1
  35. package/dist/admin/layout-editor/section-overlay.d.ts.map +1 -1
  36. package/dist/admin/layout-editor/section-overlay.js +8 -8
  37. package/dist/admin/layout-editor/section-overlay.js.map +1 -1
  38. package/dist/admin/layout-editor/section-variant-container-config.js +5 -5
  39. package/dist/admin/layout-editor/section-variant-container-config.js.map +1 -1
  40. package/dist/admin/layout-editor/use-chrome-flip.d.ts +111 -11
  41. package/dist/admin/layout-editor/use-chrome-flip.d.ts.map +1 -1
  42. package/dist/admin/layout-editor/use-chrome-flip.js +233 -33
  43. package/dist/admin/layout-editor/use-chrome-flip.js.map +1 -1
  44. package/dist/admin/layout-editor/use-editor-dnd.d.ts +44 -1
  45. package/dist/admin/layout-editor/use-editor-dnd.d.ts.map +1 -1
  46. package/dist/admin/layout-editor/use-editor-dnd.js +191 -3
  47. package/dist/admin/layout-editor/use-editor-dnd.js.map +1 -1
  48. package/dist/admin/styles/admin.css +22 -1
  49. package/dist/admin/surfaces/layouts/layout-editor-client.d.ts.map +1 -1
  50. package/dist/admin/surfaces/layouts/layout-editor-client.js +12 -1
  51. package/dist/admin/surfaces/layouts/layout-editor-client.js.map +1 -1
  52. package/dist/api/content-routes.d.ts.map +1 -1
  53. package/dist/api/content-routes.js +34 -7
  54. package/dist/api/content-routes.js.map +1 -1
  55. package/dist/api/index.d.ts.map +1 -1
  56. package/dist/api/index.js +7 -73
  57. package/dist/api/index.js.map +1 -1
  58. package/dist/cache/content-invalidation.d.ts +28 -0
  59. package/dist/cache/content-invalidation.d.ts.map +1 -0
  60. package/dist/cache/content-invalidation.js +72 -0
  61. package/dist/cache/content-invalidation.js.map +1 -0
  62. package/dist/db/content-restore.d.ts +5 -1
  63. package/dist/db/content-restore.d.ts.map +1 -1
  64. package/dist/db/content-restore.js +9 -2
  65. package/dist/db/content-restore.js.map +1 -1
  66. package/dist/layout/comments-module.d.ts.map +1 -1
  67. package/dist/layout/comments-module.js +6 -2
  68. package/dist/layout/comments-module.js.map +1 -1
  69. package/dist/layout/module-render-context.d.ts +32 -0
  70. package/dist/layout/module-render-context.d.ts.map +1 -1
  71. package/dist/layout/module-render-context.js +32 -1
  72. package/dist/layout/module-render-context.js.map +1 -1
  73. package/dist/layout/modules-extended.d.ts.map +1 -1
  74. package/dist/layout/modules-extended.js +21 -11
  75. package/dist/layout/modules-extended.js.map +1 -1
  76. package/dist/layout/modules.d.ts.map +1 -1
  77. package/dist/layout/modules.js +15 -10
  78. package/dist/layout/modules.js.map +1 -1
  79. package/dist/scope/inline-editing.d.ts.map +1 -1
  80. package/dist/scope/inline-editing.js +10 -0
  81. package/dist/scope/inline-editing.js.map +1 -1
  82. package/dist/scope/sticky-offset.d.ts +185 -0
  83. package/dist/scope/sticky-offset.d.ts.map +1 -0
  84. package/dist/scope/sticky-offset.js +432 -0
  85. package/dist/scope/sticky-offset.js.map +1 -0
  86. package/dist/site/styles.css +41 -3
  87. package/package.json +1 -1
package/AGENT-PATTERNS.md CHANGED
@@ -441,20 +441,22 @@ interchangeable.
441
441
  page is complete with JavaScript off. This is the default for anything the
442
442
  server can know — rule-based audiences (UTM, referrer, the `kywi_audience`
443
443
  pin) and A/B experiments. Since 0.6.3 it also renders in the layout editor as
444
- a badged block with an **arm switcher**, each arm editable with the ordinary
445
- section/column/module tools, so server-resolution no longer costs the owner
446
- their editing surface. Since kywi-cms#119, the owner doesn't need an agent to
447
- hand-author this shape at all: the section chrome in the admin layout
448
- editor and in the in-place front-of-site overlay alike offers
449
- **Personalize this section** / **A/B test** actions that wrap an ordinary
450
- section into a section-level variant container on the spot, seeding the
451
- default arm from the section's current content and opening straight into its
452
- config (audience/experiment picker included). An agent only needs to reach
453
- for `update_layout` when scripting bulk changes or building a container the
454
- UI can't reach.
444
+ a badged block with a **variant switcher** (internal editor code
445
+ `activeArms`, the `VariantArm` type, `data-arm-id` still says "arm"; the
446
+ persisted layout field is `variants`), each variant editable with the
447
+ ordinary section/column/module tools, so server-resolution no longer costs
448
+ the owner their editing surface. Since kywi-cms#119, the owner doesn't need
449
+ an agent to hand-author this shape at all: the section chrome in the
450
+ admin layout editor and in the in-place front-of-site overlay alike
451
+ offers **Personalize this section** / **A/B test** actions that wrap an
452
+ ordinary section into a section-level variant container on the spot,
453
+ seeding the default variant from the section's current content and
454
+ opening straight into its config (audience/experiment picker included).
455
+ An agent only needs to reach for `update_layout` when scripting bulk
456
+ changes or building a container the UI can't reach.
455
457
  - **Module-level** — the `variantContainer` *module*, placed in a column; its
456
- arms are HTML strings (`defaultContent`, `variants: [{audienceId, label,
457
- content}]`). Every arm ships in the HTML (default visible, the rest
458
+ variants are HTML strings (`defaultContent`, `variants: [{audienceId, label,
459
+ content}]`). Every variant ships in the HTML (default visible, the rest
458
460
  `display:none`) and the browser runtime reveals the matching one. Use it
459
461
  **only for signals the server cannot read at first paint** — in practice, a
460
462
  self-ID answer held in `localStorage`.
package/README.md CHANGED
@@ -166,6 +166,73 @@ text, button …) types straight on the page (committed on blur through the same
166
166
  deselects, and a persistent **Done** leaves — confirming first if there are
167
167
  unsaved changes.
168
168
 
169
+ ### Sticky site headers and the editor's bars
170
+
171
+ Two fixed bars can appear at the top of a public page for a signed-in editor:
172
+ the **browse-mode toolbar** (`.kywi-edit-toolbar`, 44px) and the **edit-mode
173
+ bar** (`.kywi-overlay-toolbar`, 46px). Each publishes its height as a token on
174
+ `:root`, so a theme never has to hard-code either number:
175
+
176
+ | Token | Bar | Default |
177
+ | --- | --- | --- |
178
+ | `--kywi-browse-bar-height` | browse-mode toolbar | `44px` |
179
+ | `--kywi-overlay-bar-height` | edit-mode bar | `46px` |
180
+
181
+ Both are declared on `:root` by **`./site/styles.css`**, which every public page
182
+ loads — and again, identically, by the layout editor's `editor.css`. The second
183
+ sheet alone is not enough to rely on: a generated app pulls the admin design
184
+ system in behind `next/dynamic`, so `editor.css` is absent from the page until
185
+ someone opens the overlay editor, and browse mode would resolve neither token.
186
+ Give every `var()` on these tokens the matching px fallback anyway (`44px` /
187
+ `46px`) — a `var()` whose token is undefined falls back to the property's
188
+ *initial* value, and `top: auto` un-sticks an element outright rather than
189
+ merely mis-offsetting it.
190
+
191
+ While a bar is up, the document is offset by its height, which is enough for a
192
+ header in normal flow. It is **not** enough for `position: sticky; top: 0` (or
193
+ `position: fixed; top: 0`): document padding moves an element's flow position,
194
+ not the line it pins to once it sticks — so a sticky nav would pin to viewport
195
+ y=0 underneath the bar as soon as the reader scrolled. The editor therefore
196
+ detects top-anchored elements when a bar mounts and patches each one's inline
197
+ `top` to the bar's token for the session, restoring the original exactly on the
198
+ way out (`packages/core/src/scope/sticky-offset.ts`). Nothing is required of a
199
+ theme for this to work.
200
+
201
+ Two things are worth knowing if you are writing one:
202
+
203
+ - **A sticky element that mounts after the editor opens is not offset.** The
204
+ scan runs once, when the bar mounts; a cookie bar on a timer or a menu that is
205
+ `display: none` at that moment keeps its own `top`. Anchor those yourself off
206
+ the class the bar puts on `<html>` — `kywi-frontend-edit--browsing` or
207
+ `kywi-frontend-edit--editing`, which are also how you special-case anything
208
+ else for an editing session:
209
+
210
+ ```css
211
+ .my-late-sticky { top: 0; }
212
+ .kywi-frontend-edit--browsing .my-late-sticky { top: var(--kywi-browse-bar-height, 44px); }
213
+ .kywi-frontend-edit--editing .my-late-sticky { top: var(--kywi-overlay-bar-height, 46px); }
214
+ ```
215
+
216
+ A second tier keeps its own offset on top of the bar:
217
+
218
+ ```css
219
+ .my-late-subnav { top: 64px; }
220
+ .kywi-frontend-edit--browsing .my-late-subnav { top: calc(var(--kywi-browse-bar-height, 44px) + 64px); }
221
+ .kywi-frontend-edit--editing .my-late-subnav { top: calc(var(--kywi-overlay-bar-height, 46px) + 64px); }
222
+ ```
223
+ - **A whole sticky STACK moves, not just its first tier.** Anything with a
224
+ computed `top` within 4px of zero is offset by one bar height; anything
225
+ between there and 200px — a sub-nav parked under the header at `top: 64px`,
226
+ the docs-site pattern — is offset by the bar *plus its own original value*, so
227
+ the gaps a theme built survive the session. Moving only the first tier would
228
+ make the header cover the sub-nav, an overlap the offset itself would have
229
+ created.
230
+ - **Further down than that, and inside a scroll pane, is left alone.** A sticky
231
+ filter panel at `top: 320px` is anchored to page content rather than to the
232
+ top of the viewport, and a sticky table head inside an `overflow: auto` pane
233
+ pins to that pane, not to the window — the bar never covers either, so neither
234
+ moves.
235
+
169
236
  A host must therefore NOT wrap the editor in `.kywi-admin-shell`: that class is
170
237
  the admin design system's base + reset and would re-typeset the page being
171
238
  edited. Pass the page's own body-wrapper class as `pageClassName` instead, so
@@ -1 +1 @@
1
- {"version":3,"file":"column-overlay.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/column-overlay.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAe,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CAyBhH"}
1
+ {"version":3,"file":"column-overlay.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/column-overlay.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAe,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CA+BhH"}
@@ -2,7 +2,13 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  export function ColumnOverlay({ columnId, width, inPlace = false, rootRef, dropId, children }) {
4
4
  if (inPlace) {
5
- return (_jsx("div", { ref: rootRef, className: `kywi-column kywi-col-${width} kywi-ip-column`, "data-kywi-column": "", "data-kywi-col-width": width, "data-column-id": columnId, ...(dropId ? { 'data-kywi-column-drop': dropId } : {}), children: children }));
5
+ return (_jsx("div", { ref: rootRef, className: `kywi-column kywi-col-${width} kywi-ip-column`,
6
+ // kywi-cms#138: the drop indicator is an absolutely-positioned overlay
7
+ // inside this column, so the column has to be its containing block —
8
+ // the site's own `.kywi-column` carries no positioning. Inline and
9
+ // editor-only: this component never renders outside the editor, so it
10
+ // cannot leak into the published page.
11
+ style: { position: 'relative' }, "data-kywi-column": "", "data-kywi-col-width": width, "data-column-id": columnId, ...(dropId ? { 'data-kywi-column-drop': dropId } : {}), children: children }));
6
12
  }
7
13
  return (_jsxs("div", { className: "kywi-editor-column", style: { flex: width }, "data-column-id": columnId, children: [_jsxs("div", { className: "kywi-editor-column-label", children: ["col-", width] }), children] }));
8
14
  }
@@ -1 +1 @@
1
- {"version":3,"file":"column-overlay.js","sourceRoot":"","sources":["../../../src/admin/layout-editor/column-overlay.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AA+BZ,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAsB;IAC/G,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,cACE,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,wBAAwB,KAAK,iBAAiB,sBACxC,EAAE,yBACE,KAAK,oBACV,QAAQ,KACpB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,YAEtD,QAAQ,GACL,CACP,CAAA;IACH,CAAC;IACD,OAAO,CACL,eACE,SAAS,EAAC,oBAAoB,EAC9B,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,oBACN,QAAQ,aAExB,eAAK,SAAS,EAAC,0BAA0B,qBAAM,KAAK,IAAO,EAC1D,QAAQ,IACL,CACP,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"column-overlay.js","sourceRoot":"","sources":["../../../src/admin/layout-editor/column-overlay.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AA+BZ,MAAM,UAAU,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAsB;IAC/G,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,cACE,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,wBAAwB,KAAK,iBAAiB;YACzD,uEAAuE;YACvE,qEAAqE;YACrE,mEAAmE;YACnE,sEAAsE;YACtE,uCAAuC;YACvC,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,sBACd,EAAE,yBACE,KAAK,oBACV,QAAQ,KACpB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,YAEtD,QAAQ,GACL,CACP,CAAA;IACH,CAAC;IACD,OAAO,CACL,eACE,SAAS,EAAC,oBAAoB,EAC9B,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,oBACN,QAAQ,aAExB,eAAK,SAAS,EAAC,0BAA0B,qBAAM,KAAK,IAAO,EAC1D,QAAQ,IACL,CACP,CAAA;AACH,CAAC"}
@@ -2,7 +2,24 @@ import type { DropFeedbackState } from './drag-utils.js';
2
2
  interface DropIndicatorProps {
3
3
  state: DropFeedbackState;
4
4
  moduleType?: string;
5
+ /**
6
+ * Y of the slot boundary this marks, in the column's own coordinate space
7
+ * (kywi-cms#138).
8
+ *
9
+ * The indicator is OVERLAY chrome: absolutely positioned inside the column,
10
+ * because drawn in the flow it pushed every module below the boundary down by
11
+ * its own height — measured at +19px as a line and +78px as the placeholder —
12
+ * the instant the drag projected a slot. The module the owner was aiming at
13
+ * slid out from under the cursor mid-drag, so aiming at the bottom of a
14
+ * module landed the drop one slot early. Positioned, it costs the column
15
+ * nothing.
16
+ *
17
+ * `null` while the boundary has not been measured yet (the first render of a
18
+ * new projection, before the layout effect that reads the slots runs) — that
19
+ * frame draws at the top of the column and is corrected before paint.
20
+ */
21
+ top: number | null;
5
22
  }
6
- export declare function DropIndicator({ state, moduleType }: DropIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
23
+ export declare function DropIndicator({ state, moduleType, top }: DropIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
7
24
  export {};
8
25
  //# sourceMappingURL=drop-indicator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drop-indicator.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/drop-indicator.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,iBAAiB,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,kBAAkB,kDAetE"}
1
+ {"version":3,"file":"drop-indicator.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/drop-indicator.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AASxD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,iBAAiB,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;;;;;;;;;OAeG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB;AAED,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,kBAAkB,kDAgC3E"}
@@ -1,11 +1,31 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- export function DropIndicator({ state, moduleType }) {
3
+ /**
4
+ * Line thickness, mirrored from `.kywi-editor-drop-line`'s `height` in
5
+ * editor.css so the line can be CENTRED on the boundary it marks. The two must
6
+ * agree; a line drawn half a pixel off is not worth a measurement per frame.
7
+ */
8
+ const LINE_HEIGHT = 3;
9
+ export function DropIndicator({ state, moduleType, top }) {
4
10
  if (state === 'idle')
5
11
  return null;
12
+ const boundary = top ?? 0;
13
+ const overlay = {
14
+ position: 'absolute',
15
+ left: 0,
16
+ right: 0,
17
+ pointerEvents: 'none',
18
+ // Above the modules and their own chrome (type chip / action row, z-index 3
19
+ // in editor.css) and below the column resize handle (6) — this is canvas
20
+ // chrome, not part of the floating-chrome tiers that start at 998.
21
+ zIndex: 5,
22
+ };
6
23
  if (state === 'line') {
7
- return _jsx("div", { className: "kywi-editor-drop-line" });
24
+ return (_jsx("div", { className: "kywi-editor-drop-line", style: { ...overlay, top: boundary - LINE_HEIGHT / 2 } }));
8
25
  }
9
- return (_jsxs("div", { className: "kywi-editor-drop-placeholder", children: [moduleType && _jsxs("span", { children: [moduleType, " will be placed here"] }), _jsx("div", { style: { fontSize: '10px', opacity: 0.7, marginTop: 2, textTransform: 'none', fontWeight: 500 }, children: "Release to drop" })] }));
26
+ // Top edge ON the boundary, extending downwards: it overlaps the module that
27
+ // would be displaced, which is the honest picture — that is the room the
28
+ // dropped module takes and where the displaced one goes.
29
+ return (_jsxs("div", { className: "kywi-editor-drop-placeholder", style: { ...overlay, top: boundary }, children: [moduleType && _jsxs("span", { children: [moduleType, " will be placed here"] }), _jsx("div", { style: { fontSize: '10px', opacity: 0.7, marginTop: 2, textTransform: 'none', fontWeight: 500 }, children: "Release to drop" })] }));
10
30
  }
11
31
  //# sourceMappingURL=drop-indicator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"drop-indicator.js","sourceRoot":"","sources":["../../../src/admin/layout-editor/drop-indicator.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AASZ,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAsB;IACrE,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAEjC,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QACrB,OAAO,cAAK,SAAS,EAAC,uBAAuB,GAAG,CAAA;IAClD,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,8BAA8B,aAC1C,UAAU,IAAI,2BAAO,UAAU,4BAA4B,EAC5D,cAAK,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,gCAE9F,IACF,CACP,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"drop-indicator.js","sourceRoot":"","sources":["../../../src/admin/layout-editor/drop-indicator.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAIZ;;;;GAIG;AACH,MAAM,WAAW,GAAG,CAAC,CAAA;AAwBrB,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAsB;IAC1E,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAEjC,MAAM,QAAQ,GAAG,GAAG,IAAI,CAAC,CAAA;IACzB,MAAM,OAAO,GAAwB;QACnC,QAAQ,EAAE,UAAU;QACpB,IAAI,EAAE,CAAC;QACP,KAAK,EAAE,CAAC;QACR,aAAa,EAAE,MAAM;QACrB,4EAA4E;QAC5E,yEAAyE;QACzE,mEAAmE;QACnE,MAAM,EAAE,CAAC;KACV,CAAA;IAED,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QACrB,OAAO,CACL,cAAK,SAAS,EAAC,uBAAuB,EAAC,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,WAAW,GAAG,CAAC,EAAE,GAAI,CAClG,CAAA;IACH,CAAC;IAED,6EAA6E;IAC7E,yEAAyE;IACzE,yDAAyD;IACzD,OAAO,CACL,eAAK,SAAS,EAAC,8BAA8B,EAAC,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,aAC/E,UAAU,IAAI,2BAAO,UAAU,4BAA4B,EAC5D,cAAK,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,gCAE9F,IACF,CACP,CAAA;AACH,CAAC"}
@@ -1,6 +1,6 @@
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
4
  /** Resolve a connected component node to its live definition for canvas preview. */
5
5
  type ComponentPreviewResolver = (componentId: string) => {
6
6
  props: Record<string, unknown>;
@@ -23,15 +23,19 @@ export interface InlineEditController {
23
23
  resetId: string | null;
24
24
  /** Bumped each time an edit session ends. */
25
25
  nonce: number;
26
- /** The module type's primary text prop, or null when it has none. */
27
- resolveTextProp: (moduleType: string) => InlineTextProp | null;
26
+ /**
27
+ * The element + prop a gesture inside one module's wrapper binds, or null when
28
+ * there is nothing safe to bind (kywi-cms#145 — see `resolveInlineBinding`).
29
+ */
30
+ resolveBinding: (moduleType: string, wrapper: HTMLElement, target: Element | null) => InlineTextBindingTarget | null;
28
31
  onActivate: (moduleId: string) => void;
29
- onCommit: (moduleId: string, host: HTMLElement) => void;
32
+ onCommit: (moduleId: string, host: HTMLElement, prop: InlineTextProp) => void;
30
33
  onCancel: () => void;
31
34
  /**
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.
35
+ * Double-click fallback for a gesture with nothing safe to bind
36
+ * (kywi-cms#118, kywi-cms#145). Selecting is the caller's job (via the
37
+ * module's own `onSelect`); this only moves focus into the already-open props
38
+ * rail once that selection commits.
35
39
  */
36
40
  focusPropsRail: () => void;
37
41
  }
@@ -55,9 +59,28 @@ interface EditorCanvasProps {
55
59
  * chips, handles) float above it instead of boxing it into an artboard.
56
60
  */
57
61
  inPlace?: boolean;
62
+ /**
63
+ * Whether the front editor's left module-palette rail is expanded
64
+ * (kywi-cms#132). The rail is fixed to the viewport edge and floats over the
65
+ * page's left margin, so a full-width section's floating chrome renders
66
+ * underneath it and cannot be clicked — the chrome moves out of its way, and
67
+ * this is the only channel that tells it to. Meaningful in in-place mode
68
+ * only: the admin artboard's palette is an in-flow column.
69
+ */
70
+ paletteOpen?: boolean;
58
71
  /** Inline text editing controller, only meaningful in in-place mode. */
59
72
  inlineEdit?: InlineEditController | null;
60
73
  }
61
- export declare function EditorCanvas({ state, dispatch, moduleComponents, onInsertSection, resolveComponent, overlayHandles, inPlace, inlineEdit, }: EditorCanvasProps): import("react/jsx-runtime").JSX.Element;
74
+ /**
75
+ * Memoized (kywi-cms#142): the canvas re-renders on every editor-state
76
+ * change, including drag feedback published elsewhere in the tree
77
+ * (DropTargetContext, consumed by DroppableColumn below `EditorCanvas`'s own
78
+ * props) that doesn't change any of ITS props. `React.memo`'s default
79
+ * shallow-equal prop comparison skips re-rendering the whole canvas subtree
80
+ * for those, since `state`/`dispatch`/`moduleComponents` etc. are stable
81
+ * references from the shells (LayoutEditor / OverlayShell) across a
82
+ * drop-target update.
83
+ */
84
+ export declare const EditorCanvas: React.NamedExoticComponent<EditorCanvasProps>;
62
85
  export {};
63
86
  //# sourceMappingURL=editor-canvas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"editor-canvas.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/editor-canvas.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAkBlE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAKtD,oFAAoF;AACpF,KAAK,wBAAwB,GAAG,CAC9B,WAAW,EAAE,MAAM,KAChB;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAA;AAEjG;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,cAAc,GAAG,IAAI,CAAA;IAC9D,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,CAAA;IACvD,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;IACH,cAAc,EAAE,MAAM,IAAI,CAAA;CAC3B;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAA;IACtC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC,CAAA;IACzH,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,wBAAwB,CAAA;IAC3C,sFAAsF;IACtF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,wEAAwE;IACxE,UAAU,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAA;CACzC;AAykBD,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAsB,EACtB,OAAe,EACf,UAAU,GACX,EAAE,iBAAiB,2CAyFnB"}
1
+ {"version":3,"file":"editor-canvas.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/editor-canvas.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAkBlE,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAK/E,oFAAoF;AACpF,KAAK,wBAAwB,GAAG,CAC9B,WAAW,EAAE,MAAM,KAChB;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAA;AAEjG;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,cAAc,EAAE,CACd,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,OAAO,GAAG,IAAI,KACnB,uBAAuB,GAAG,IAAI,CAAA;IACnC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;IAC7E,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,IAAI,CAAA;CAC3B;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAA;IACtC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC,CAAA;IACzH,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,wBAAwB,CAAA;IAC3C,sFAAsF;IACtF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,wEAAwE;IACxE,UAAU,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAA;CACzC;AA8pBD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,+CAqGvB,CAAA"}
@@ -15,7 +15,7 @@ import { ColumnResizeHandle } from './column-resize-handle.js';
15
15
  import { ModuleOverlay } from './module-overlay.js';
16
16
  import { DropIndicator } from './drop-indicator.js';
17
17
  import { DropTargetContext } from './drag-feedback.js';
18
- import { useChromeFlip } from './use-chrome-flip.js';
18
+ import { useChromeFlip, chromeOffsetClasses, chromeOffsetStyle } from './use-chrome-flip.js';
19
19
  import { ModuleEditorContext } from '../../layout/module-render-context.js';
20
20
  /**
21
21
  * Render a node's live preview in the canvas. A connected component node
@@ -63,23 +63,68 @@ function getRegions(state) {
63
63
  return state.layout.regions;
64
64
  }
65
65
  /**
66
- * Build the inline-editing binding for one module, or null when the module has
67
- * no primary text prop (or the canvas isn't in in-place mode).
66
+ * Build the inline-editing binding for one module, or null when the canvas
67
+ * isn't in in-place mode.
68
+ *
69
+ * Deliberately NOT gated on the module having an editable prop any more
70
+ * (kywi-cms#145): what a gesture can edit depends on where in the module it
71
+ * landed, so the decision moved to `resolve`, per gesture. A module with nothing
72
+ * bindable resolves to null every time and its double-click falls through to
73
+ * `onInlineFallback`.
68
74
  */
69
75
  function inlineTextBindingFor(nodeId, nodeType, inPlace, inlineEdit) {
70
76
  if (!inPlace || !inlineEdit)
71
77
  return null;
72
- const prop = inlineEdit.resolveTextProp(nodeType);
73
- if (!prop)
74
- return null;
75
78
  return {
76
79
  active: inlineEdit.editingId === nodeId,
77
- html: prop.html,
80
+ resolve: (wrapper, target) => inlineEdit.resolveBinding(nodeType, wrapper, target),
78
81
  onActivate: () => inlineEdit.onActivate(nodeId),
79
- onCommit: (host) => inlineEdit.onCommit(nodeId, host),
82
+ onCommit: (host, prop) => inlineEdit.onCommit(nodeId, host, prop),
80
83
  onCancel: () => inlineEdit.onCancel(),
81
84
  };
82
85
  }
86
+ /**
87
+ * Y of the slot boundary `index` marks, in the column's own coordinate space —
88
+ * or null when there is nothing to measure against (kywi-cms#138).
89
+ *
90
+ * `offsetTop`/`offsetHeight` rather than `getBoundingClientRect()`: the column
91
+ * is the slots' offset parent (both shells position it, see `DroppableColumn`
92
+ * below), so these are already expressed in exactly the space an absolutely
93
+ * positioned child of that column is placed in — no scroll or viewport maths in
94
+ * between, and nothing to re-derive when the page scrolls mid-drag.
95
+ *
96
+ * Direct children only — deliberately narrower than `resolveColumnDropIndex`,
97
+ * which reads `querySelectorAll('[data-kywi-module-slot]')` and so counts
98
+ * descendants at any depth. The two agree today because only `ModuleOverlay`
99
+ * emits `data-kywi-module-slot` and a column never nests another column, so
100
+ * every slot inside a column IS a direct child. If a module ever renders its
101
+ * own column (or anything else emits that attribute), the two would diverge —
102
+ * the indicator would measure against one set of slots while the drop resolves
103
+ * against another — and this is the place that has to change with it.
104
+ *
105
+ * Only ever called for a PALETTE drag (`useEditorDnd` projects a drop target
106
+ * for nothing else), which is what makes the measurement trustworthy: during a
107
+ * canvas reorder the dragged slot carries a live dnd-kit transform and reports
108
+ * where it has been dragged to rather than where it sits.
109
+ */
110
+ function slotBoundaryOffset(columnEl, index) {
111
+ if (!columnEl)
112
+ return null;
113
+ const slots = [];
114
+ // `Array.from`, not `for…of` over the collection: the package's `lib` is
115
+ // ["ES2022", "DOM"] without "DOM.Iterable".
116
+ for (const child of Array.from(columnEl.children)) {
117
+ if (child.hasAttribute('data-kywi-module-slot'))
118
+ slots.push(child);
119
+ }
120
+ // An empty column has one slot boundary and it is the top of the column.
121
+ const last = slots[slots.length - 1];
122
+ if (!last)
123
+ return 0;
124
+ if (index >= slots.length)
125
+ return last.offsetTop + last.offsetHeight;
126
+ return slots[index].offsetTop;
127
+ }
83
128
  // ── DroppableColumn ───────────────────────────────────────────────────────────
84
129
  function DroppableColumn({ column, state, dispatch, moduleComponents, resolveComponent, inPlace = false, inlineEdit, }) {
85
130
  // kywi-cms#20: this droppable represents the column's own (empty) space —
@@ -104,31 +149,57 @@ function DroppableColumn({ column, state, dispatch, moduleComponents, resolveCom
104
149
  // draws nothing — which is the point: the owner sees a single destination.
105
150
  const dropTarget = React.useContext(DropTargetContext);
106
151
  const target = dropTarget?.columnId === column.id ? dropTarget : null;
152
+ // kywi-cms#138: the drop element doubles as the indicator's positioning
153
+ // context, so the indicator has to be measured against the same element it is
154
+ // placed inside. `setNodeRef` is stable (dnd-kit builds it once per hook), so
155
+ // this callback is too — a changing ref callback would hand dnd-kit a `null`
156
+ // node on every render, mid-drag included.
157
+ const columnElRef = React.useRef(null);
158
+ const attachColumnRef = React.useCallback((el) => {
159
+ columnElRef.current = el;
160
+ setNodeRef(el);
161
+ }, [setNodeRef]);
162
+ // Measured, not guessed: the boundary's y in the column's own coordinates.
163
+ // `useLayoutEffect` so the correction lands before the browser paints — the
164
+ // first frame of a new projection renders at the top of the column and is
165
+ // never seen there.
166
+ const [indicatorTop, setIndicatorTop] = React.useState(null);
167
+ const targetIndex = target ? target.index : null;
168
+ React.useLayoutEffect(() => {
169
+ setIndicatorTop(targetIndex === null ? null : slotBoundaryOffset(columnElRef.current, targetIndex));
170
+ }, [targetIndex, column.nodes.length]);
107
171
  /**
108
- * The indicator for one slot boundary: before the module at `index`, or after
109
- * the last module when `index === column.nodes.length`. Rendering it in the
110
- * flow (rather than as an absolute overlay) is what makes it honest — the
111
- * modules below really do move down, so the placeholder shows the room the
112
- * dropped module will take.
172
+ * The projected drop boundary, drawn as ONE absolutely-positioned element at
173
+ * the end of the column rather than spliced into the module flow.
174
+ *
175
+ * In the flow it was self-defeating: the indicator's own height pushed
176
+ * everything below the boundary down the moment the drag projected it —
177
+ * measured on the e2e fixture, +19px for the line and +78px once it escalated
178
+ * to the placeholder — so the module the owner was aiming at moved out from
179
+ * under the cursor and the projection re-resolved one slot early. As an
180
+ * overlay it promises the same slot without touching the layout it describes.
113
181
  */
114
- const indicatorAt = (index) => target && target.index === index ? (_jsx(DropIndicator, { state: target.feedback, ...(target.label ? { moduleType: target.label } : {}) })) : null;
115
- const content = (_jsxs(_Fragment, { children: [column.nodes.length === 0 && (_jsx("div", { className: inPlace ? 'kywi-ip-empty-column' : 'kywi-editor-column-empty', children: "Drag a module here" })), _jsxs(SortableContext, { items: moduleIds, strategy: verticalListSortingStrategy, children: [column.nodes.map((node, index) => {
116
- const inlineText = inlineTextBindingFor(node.id, node.type, inPlace, inlineEdit);
117
- const select = () => dispatch({ type: 'SELECT', id: node.id, selectionType: 'module' });
118
- // No primary text prop to bind (kywi-cms#118): double-click would
119
- // otherwise be a silent no-op, so route it to the props rail instead
120
- // — select the module, then focus its first field once that lands.
121
- const onInlineFallback = inPlace && !inlineText && inlineEdit
122
- ? () => {
123
- select();
124
- inlineEdit.focusPropsRail();
125
- }
126
- : undefined;
127
- // Re-key the preview after an edit session so contenteditable's DOM
128
- // leftovers are replaced by a clean render from the stored props.
129
- const previewKey = inlineEdit && inlineEdit.resetId === node.id ? `${node.id}:${inlineEdit.nonce}` : node.id;
130
- return (_jsxs(React.Fragment, { children: [indicatorAt(index), _jsx(ModuleOverlay, { moduleId: node.id, instanceId: node.instanceId, columnId: column.id, index: index, isSelected: state.selectedId === node.id, typeLabel: node.type, inPlace: inPlace, moduleType: node.type, inlineText: inlineText, ...(onInlineFallback ? { onInlineFallback } : {}), onSelect: select, children: _jsx(ModulePreview, { node: node, moduleComponents: moduleComponents, ...(resolveComponent ? { resolveComponent } : {}) }, previewKey) })] }, node.id));
131
- }), indicatorAt(column.nodes.length)] })] }));
182
+ const indicator = target ? (_jsx(DropIndicator, { state: target.feedback, top: indicatorTop, ...(target.label ? { moduleType: target.label } : {}) })) : null;
183
+ const content = (_jsxs(_Fragment, { children: [column.nodes.length === 0 && (_jsx("div", { className: inPlace ? 'kywi-ip-empty-column' : 'kywi-editor-column-empty', children: "Drag a module here" })), _jsx(SortableContext, { items: moduleIds, strategy: verticalListSortingStrategy, children: column.nodes.map((node, index) => {
184
+ const inlineText = inlineTextBindingFor(node.id, node.type, inPlace, inlineEdit);
185
+ const select = () => dispatch({ type: 'SELECT', id: node.id, selectionType: 'module' });
186
+ // Nothing safe to bind (kywi-cms#118): double-click would otherwise be
187
+ // a silent no-op, so route it to the props rail instead — select the
188
+ // module, then focus its first field once that lands. Now always
189
+ // supplied, because whether a gesture binds is only known when it
190
+ // happens (kywi-cms#145): the overlay calls this when its own
191
+ // resolution comes back empty.
192
+ const onInlineFallback = inPlace && inlineEdit
193
+ ? () => {
194
+ select();
195
+ inlineEdit.focusPropsRail();
196
+ }
197
+ : undefined;
198
+ // Re-key the preview after an edit session so contenteditable's DOM
199
+ // leftovers are replaced by a clean render from the stored props.
200
+ const previewKey = inlineEdit && inlineEdit.resetId === node.id ? `${node.id}:${inlineEdit.nonce}` : node.id;
201
+ return (_jsx(React.Fragment, { children: _jsx(ModuleOverlay, { moduleId: node.id, instanceId: node.instanceId, columnId: column.id, index: index, isSelected: state.selectedId === node.id, typeLabel: node.type, inPlace: inPlace, moduleType: node.type, inlineText: inlineText, ...(onInlineFallback ? { onInlineFallback } : {}), onSelect: select, children: _jsx(ModulePreview, { node: node, moduleComponents: moduleComponents, ...(resolveComponent ? { resolveComponent } : {}) }, previewKey) }) }, node.id));
202
+ }) }), indicator] }));
132
203
  // In place the column element IS the drop zone: an extra wrapper div would
133
204
  // sit between `.kywi-column` and the module wrappers and break the site's own
134
205
  // child selectors. In the artboard the wrapper also carries a minimum height
@@ -136,8 +207,11 @@ function DroppableColumn({ column, state, dispatch, moduleComponents, resolveCom
136
207
  //
137
208
  // Either way the element carrying `setNodeRef` also carries
138
209
  // `data-kywi-column-drop` (kywi-cms#134) — in place that's the column element
139
- // itself, so the id goes through `ColumnOverlay`'s `dropId`.
140
- return inPlace ? (_jsx(ColumnOverlay, { columnId: column.id, width: column.width, inPlace: true, rootRef: setNodeRef, dropId: column.id, children: content })) : (_jsx(ColumnOverlay, { columnId: column.id, width: column.width, children: _jsx("div", { ref: setNodeRef, "data-kywi-column-drop": column.id, style: { minHeight: 40 }, children: content }) }));
210
+ // itself, so the id goes through `ColumnOverlay`'s `dropId` — and is the drop
211
+ // indicator's positioning context (kywi-cms#138), which is why the artboard
212
+ // wrapper is `position: relative` here and the in-place column is inside
213
+ // `ColumnOverlay`.
214
+ return inPlace ? (_jsx(ColumnOverlay, { columnId: column.id, width: column.width, inPlace: true, rootRef: attachColumnRef, dropId: column.id, children: content })) : (_jsx(ColumnOverlay, { columnId: column.id, width: column.width, children: _jsx("div", { ref: attachColumnRef, "data-kywi-column-drop": column.id, style: { position: 'relative', minHeight: 40 }, children: content }) }));
141
215
  }
142
216
  // ── SectionBlock ──────────────────────────────────────────────────────────────
143
217
  /**
@@ -146,7 +220,7 @@ function DroppableColumn({ column, state, dispatch, moduleComponents, resolveCom
146
220
  * container arm (kywi-cms#92) — arm content is edited with exactly the same
147
221
  * machinery, because the reducer's lookups are arm-aware.
148
222
  */
149
- function SectionBlock({ section, state, dispatch, moduleComponents, resolveComponent, inPlace = false, inArm = false, inlineEdit, }) {
223
+ function SectionBlock({ section, state, dispatch, moduleComponents, resolveComponent, inPlace = false, paletteOpen = false, inArm = false, inlineEdit, }) {
150
224
  // In place, the section carries its OWN resolved style exactly as the public
151
225
  // SectionRenderer applies it (kywi-cms#94) — otherwise a section's background
152
226
  // or padding would vanish the moment the owner started editing, which is the
@@ -172,7 +246,7 @@ function SectionBlock({ section, state, dispatch, moduleComponents, resolveCompo
172
246
  { icon: 'personalize', label: 'Personalize', onClick: wrapInContainer('personalization') },
173
247
  { icon: 'variant', label: 'A/B test', onClick: wrapInContainer('ab_test') },
174
248
  ];
175
- return (_jsx(SectionOverlay, { sectionId: section.id, isSelected: state.selectedId === section.id, inPlace: inPlace, ...(inArm ? {} : { actions: wrapActions }), ...(resolved ? { inPlaceStyle: resolved.base, responsiveCss: resolved.responsiveCss } : {}), onSelect: () => dispatch({ type: 'SELECT', id: section.id, selectionType: 'section' }), onAddColumn: () => dispatch({ type: 'ADD_COLUMN', sectionId: section.id }), onDelete: () => dispatch({ type: 'REMOVE_SECTION', sectionId: section.id }), children: section.columns.map((column, ci) => (_jsxs(React.Fragment, { children: [_jsx(DroppableColumn, { column: column, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) }), !inPlace && ci < section.columns.length - 1 && (_jsx(ColumnResizeHandle, { leftWidth: column.width, rightWidth: section.columns[ci + 1].width, onResize: (lw, rw) => {
249
+ return (_jsx(SectionOverlay, { sectionId: section.id, isSelected: state.selectedId === section.id, inPlace: inPlace, paletteOpen: paletteOpen, ...(inArm ? {} : { actions: wrapActions }), ...(resolved ? { inPlaceStyle: resolved.base, responsiveCss: resolved.responsiveCss } : {}), onSelect: () => dispatch({ type: 'SELECT', id: section.id, selectionType: 'section' }), onAddColumn: () => dispatch({ type: 'ADD_COLUMN', sectionId: section.id }), onDelete: () => dispatch({ type: 'REMOVE_SECTION', sectionId: section.id }), children: section.columns.map((column, ci) => (_jsxs(React.Fragment, { children: [_jsx(DroppableColumn, { column: column, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) }), !inPlace && ci < section.columns.length - 1 && (_jsx(ColumnResizeHandle, { leftWidth: column.width, rightWidth: section.columns[ci + 1].width, onResize: (lw, rw) => {
176
250
  const widths = section.columns.map((c, i) => i === ci ? lw : i === ci + 1 ? rw : c.width);
177
251
  dispatch({ type: 'RESIZE_COLUMNS', sectionId: section.id, widths });
178
252
  } }))] }, column.id))) }));
@@ -192,7 +266,7 @@ function formatWeight(weight) {
192
266
  * way — the canvas is showing whichever arm is selected here.
193
267
  */
194
268
  function ArmTabs({ arms, activeArm, isAbTest, onSelect, }) {
195
- return (_jsx("div", { className: "kywi-editor-vc-arms", role: "tablist", "aria-label": "Variant arms", children: arms.map(arm => (_jsxs("button", { type: "button", role: "tab", "aria-selected": arm.id === activeArm, "data-arm-id": arm.id, className: `kywi-editor-vc-arm-btn${arm.id === activeArm ? ' kywi-editor-vc-arm-btn--active' : ''}`, onClick: (e) => {
269
+ return (_jsx("div", { className: "kywi-editor-vc-arms", role: "tablist", "aria-label": "Variants", children: arms.map(arm => (_jsxs("button", { type: "button", role: "tab", "aria-selected": arm.id === activeArm, "data-arm-id": arm.id, className: `kywi-editor-vc-arm-btn${arm.id === activeArm ? ' kywi-editor-vc-arm-btn--active' : ''}`, onClick: (e) => {
196
270
  e.stopPropagation();
197
271
  onSelect(arm.id);
198
272
  }, children: [arm.label, isAbTest && arm.weight !== null && (_jsx("span", { className: "kywi-editor-vc-arm-weight", children: formatWeight(arm.weight) }))] }, arm.id))) }));
@@ -222,7 +296,7 @@ function ArmBody({ inPlace, activeArm, children, }) {
222
296
  * `.kywi-variant-container` and its badge/arm switcher floats above the band,
223
297
  * so a personalized hero looks exactly like the hero while staying switchable.
224
298
  */
225
- function VariantContainerBlock({ container, state, dispatch, moduleComponents, resolveComponent, inPlace = false, inlineEdit, }) {
299
+ function VariantContainerBlock({ container, state, dispatch, moduleComponents, resolveComponent, inPlace = false, paletteOpen = false, inlineEdit, }) {
226
300
  // Collapsed/expanded is presentation-only and per-block, so it stays local.
227
301
  // Expanded by default: hiding arm content behind a disclosure would re-create
228
302
  // the very "invisible in the editor" problem this block exists to fix.
@@ -244,18 +318,17 @@ function VariantContainerBlock({ container, state, dispatch, moduleComponents, r
244
318
  // In place the badge/arm switcher floats above this band; for a personalized
245
319
  // hero — a band at the very top of the page, the common case — that lands
246
320
  // under the editor's fixed top bar, so it is pinned down inside the band
247
- // instead (kywi-cms#122).
321
+ // instead (kywi-cms#122), and for a full-width band it also lands behind the
322
+ // open module palette, so it is pushed right out of that rail (kywi-cms#132).
248
323
  const containerRef = React.useRef(null);
249
- const chromeClampPx = useChromeFlip(containerRef, inPlace);
324
+ const chromeOffsets = useChromeFlip(containerRef, inPlace, paletteOpen);
250
325
  return (_jsxs("div", { ref: containerRef, className: rootClassName, "data-variant-container-id": container.id, "data-vc-mode": container.mode, ...(inPlace ? { 'data-variant': activeArm } : {}), "data-testid": "editor-variant-container", children: [_jsxs("div", { className: inPlace
251
- ? `kywi-ip-chrome kywi-ip-vc-chrome${chromeClampPx !== null ? ' kywi-ip-chrome--clamped' : ''}`
252
- : 'kywi-editor-vc-toolbar', style: inPlace && chromeClampPx !== null
253
- ? { '--kywi-ip-chrome-clamp': `${chromeClampPx}px` }
254
- : undefined, ...(inPlace ? { 'data-kywi-editor-chrome': '', contentEditable: false } : {}), onClick: (e) => {
326
+ ? ['kywi-ip-chrome', 'kywi-ip-vc-chrome', ...chromeOffsetClasses(chromeOffsets)].join(' ')
327
+ : 'kywi-editor-vc-toolbar', style: inPlace ? chromeOffsetStyle(chromeOffsets) : undefined, ...(inPlace ? { 'data-kywi-editor-chrome': '', contentEditable: false } : {}), onClick: (e) => {
255
328
  // Stop here so the canvas root doesn't immediately deselect.
256
329
  e.stopPropagation();
257
330
  dispatch({ type: 'SELECT', id: container.id, selectionType: 'variantContainer' });
258
- }, children: [_jsx("span", { className: "kywi-editor-vc-badge", children: isAbTest ? 'A/B test' : 'Personalized' }), _jsx("span", { className: "kywi-editor-vc-title", children: isAbTest ? 'A/B tested section' : 'Personalized section' }), _jsxs("span", { className: "kywi-editor-vc-arm-count", children: [arms.length, " arm", arms.length === 1 ? '' : 's'] }), inPlace && expanded && (_jsx(ArmTabs, { arms: arms, activeArm: activeArm, isAbTest: isAbTest, onSelect: selectArm })), _jsx("div", { className: "kywi-editor-vc-toolbar-actions", children: _jsx("button", { type: "button", className: "kywi-editor-section-toolbar-btn", "data-action": "toggle-variant-container", "aria-expanded": expanded, onClick: (e) => { e.stopPropagation(); setExpanded(v => !v); }, children: expanded ? 'Collapse' : 'Expand' }) })] }), expanded && (_jsxs(_Fragment, { children: [!inPlace && (_jsx(ArmTabs, { arms: arms, activeArm: activeArm, isAbTest: isAbTest, onSelect: selectArm })), _jsxs(ArmBody, { inPlace: inPlace, activeArm: activeArm, children: [sections.length === 0 && (_jsx("p", { className: "kywi-editor-vc-arm-empty", children: "This arm renders nothing. Add a section so visitors in it see content." })), sections.map(section => (_jsx(SectionBlock, { section: section, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, inArm: true, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) }, section.id))), _jsx("button", { type: "button", className: inPlace ? 'kywi-ip-add-arm-section' : 'kywi-editor-vc-add-section', "data-action": "add-arm-section", ...(inPlace ? { 'data-kywi-editor-chrome': '' } : {}), onClick: (e) => {
331
+ }, children: [_jsx("span", { className: "kywi-editor-vc-badge", children: isAbTest ? 'A/B test' : 'Personalized' }), _jsx("span", { className: "kywi-editor-vc-title", children: isAbTest ? 'A/B tested section' : 'Personalized section' }), _jsxs("span", { className: "kywi-editor-vc-arm-count", children: [arms.length, " variant", arms.length === 1 ? '' : 's'] }), inPlace && expanded && (_jsx(ArmTabs, { arms: arms, activeArm: activeArm, isAbTest: isAbTest, onSelect: selectArm })), _jsx("div", { className: "kywi-editor-vc-toolbar-actions", children: _jsx("button", { type: "button", className: "kywi-editor-section-toolbar-btn", "data-action": "toggle-variant-container", "aria-expanded": expanded, onClick: (e) => { e.stopPropagation(); setExpanded(v => !v); }, children: expanded ? 'Collapse' : 'Expand' }) })] }), expanded && (_jsxs(_Fragment, { children: [!inPlace && (_jsx(ArmTabs, { arms: arms, activeArm: activeArm, isAbTest: isAbTest, onSelect: selectArm })), _jsxs(ArmBody, { inPlace: inPlace, activeArm: activeArm, children: [sections.length === 0 && (_jsx("p", { className: "kywi-editor-vc-arm-empty", children: "This variant renders nothing. Add a section so visitors in it see content." })), sections.map(section => (_jsx(SectionBlock, { section: section, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, paletteOpen: paletteOpen, inArm: true, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) }, section.id))), _jsx("button", { type: "button", className: inPlace ? 'kywi-ip-add-arm-section' : 'kywi-editor-vc-add-section', "data-action": "add-arm-section", ...(inPlace ? { 'data-kywi-editor-chrome': '' } : {}), onClick: (e) => {
259
332
  e.stopPropagation();
260
333
  dispatch({
261
334
  type: 'ADD_ARM_SECTION',
@@ -263,10 +336,20 @@ function VariantContainerBlock({ container, state, dispatch, moduleComponents, r
263
336
  armId: activeArm,
264
337
  columns: [12],
265
338
  });
266
- }, children: "+ Add section to this arm" })] })] }))] }));
339
+ }, children: "+ Add section to this variant" })] })] }))] }));
267
340
  }
268
341
  // ── EditorCanvas ──────────────────────────────────────────────────────────────
269
- export function EditorCanvas({ state, dispatch, moduleComponents, onInsertSection, resolveComponent, overlayHandles = false, inPlace = false, inlineEdit, }) {
342
+ /**
343
+ * Memoized (kywi-cms#142): the canvas re-renders on every editor-state
344
+ * change, including drag feedback published elsewhere in the tree
345
+ * (DropTargetContext, consumed by DroppableColumn below `EditorCanvas`'s own
346
+ * props) that doesn't change any of ITS props. `React.memo`'s default
347
+ * shallow-equal prop comparison skips re-rendering the whole canvas subtree
348
+ * for those, since `state`/`dispatch`/`moduleComponents` etc. are stable
349
+ * references from the shells (LayoutEditor / OverlayShell) across a
350
+ * drop-target update.
351
+ */
352
+ export const EditorCanvas = React.memo(function EditorCanvas({ state, dispatch, moduleComponents, onInsertSection, resolveComponent, overlayHandles = false, inPlace = false, paletteOpen = false, inlineEdit, }) {
270
353
  const regions = getRegions(state);
271
354
  // In place the canvas is NOT an artboard: it carries no background, padding
272
355
  // or max-width of its own, so the page renders at its real width with its own
@@ -285,6 +368,6 @@ export function EditorCanvas({ state, dispatch, moduleComponents, onInsertSectio
285
368
  return (_jsx(ModuleEditorContext.Provider, { value: true, children: _jsxs("div", { className: canvasClassName, onClick: () => dispatch({ type: 'DESELECT' }), children: [totalNodes === 0 && (_jsxs("div", { className: "kywi-editor-canvas-empty", "data-testid": "editor-canvas-empty", children: [_jsx("p", { className: "kywi-editor-canvas-empty-title", children: "This page currently renders its body content." }), _jsx("p", { className: "kywi-editor-canvas-empty-copy", children: "Add a section to design it with the layout engine." }), _jsx("button", { type: "button", className: "kywi-editor-canvas-empty-cta", onClick: (event) => {
286
369
  event.stopPropagation();
287
370
  onInsertSection(Object.keys(regions)[0] ?? 'main');
288
- }, children: "+ Add Section" })] })), Object.entries(regions).map(([regionName, nodes]) => (_jsxs(RegionOverlay, { name: regionName, inPlace: inPlace, children: [nodes.filter(isRenderableNode).map((node) => (_jsxs(React.Fragment, { children: [!inPlace && _jsx(SectionInsertLine, { onInsert: () => onInsertSection(regionName) }), isLayoutSection(node) ? (_jsx(SectionBlock, { section: node, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) })) : (_jsx(VariantContainerBlock, { container: node, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) }))] }, node.id))), _jsx(SectionInsertLine, { onInsert: () => onInsertSection(regionName), inPlace: inPlace })] }, regionName)))] }) }));
289
- }
371
+ }, children: "+ Add Section" })] })), Object.entries(regions).map(([regionName, nodes]) => (_jsxs(RegionOverlay, { name: regionName, inPlace: inPlace, children: [nodes.filter(isRenderableNode).map((node) => (_jsxs(React.Fragment, { children: [!inPlace && _jsx(SectionInsertLine, { onInsert: () => onInsertSection(regionName) }), isLayoutSection(node) ? (_jsx(SectionBlock, { section: node, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, paletteOpen: paletteOpen, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) })) : (_jsx(VariantContainerBlock, { container: node, state: state, dispatch: dispatch, moduleComponents: moduleComponents, inPlace: inPlace, paletteOpen: paletteOpen, ...(inlineEdit ? { inlineEdit } : {}), ...(resolveComponent ? { resolveComponent } : {}) }))] }, node.id))), _jsx(SectionInsertLine, { onInsert: () => onInsertSection(regionName), inPlace: inPlace })] }, regionName)))] }) }));
372
+ });
290
373
  //# sourceMappingURL=editor-canvas.js.map