@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.
- package/AGENT-PATTERNS.md +15 -13
- package/README.md +67 -0
- package/dist/admin/layout-editor/column-overlay.d.ts.map +1 -1
- package/dist/admin/layout-editor/column-overlay.js +7 -1
- package/dist/admin/layout-editor/column-overlay.js.map +1 -1
- package/dist/admin/layout-editor/drop-indicator.d.ts +18 -1
- package/dist/admin/layout-editor/drop-indicator.d.ts.map +1 -1
- package/dist/admin/layout-editor/drop-indicator.js +23 -3
- package/dist/admin/layout-editor/drop-indicator.js.map +1 -1
- package/dist/admin/layout-editor/editor-canvas.d.ts +31 -8
- package/dist/admin/layout-editor/editor-canvas.d.ts.map +1 -1
- package/dist/admin/layout-editor/editor-canvas.js +131 -48
- package/dist/admin/layout-editor/editor-canvas.js.map +1 -1
- package/dist/admin/layout-editor/editor-state.d.ts.map +1 -1
- package/dist/admin/layout-editor/editor-state.js +19 -3
- package/dist/admin/layout-editor/editor-state.js.map +1 -1
- package/dist/admin/layout-editor/editor.css +106 -8
- package/dist/admin/layout-editor/inline-text.d.ts +56 -4
- package/dist/admin/layout-editor/inline-text.d.ts.map +1 -1
- package/dist/admin/layout-editor/inline-text.js +134 -11
- package/dist/admin/layout-editor/inline-text.js.map +1 -1
- package/dist/admin/layout-editor/layout-editor.d.ts.map +1 -1
- package/dist/admin/layout-editor/layout-editor.js +1 -1
- package/dist/admin/layout-editor/layout-editor.js.map +1 -1
- package/dist/admin/layout-editor/module-overlay.d.ts +26 -13
- package/dist/admin/layout-editor/module-overlay.d.ts.map +1 -1
- package/dist/admin/layout-editor/module-overlay.js +206 -22
- package/dist/admin/layout-editor/module-overlay.js.map +1 -1
- package/dist/admin/layout-editor/overlay-shell.d.ts.map +1 -1
- package/dist/admin/layout-editor/overlay-shell.js +39 -12
- package/dist/admin/layout-editor/overlay-shell.js.map +1 -1
- package/dist/admin/layout-editor/props-panel.js +1 -1
- package/dist/admin/layout-editor/props-panel.js.map +1 -1
- package/dist/admin/layout-editor/section-overlay.d.ts +8 -1
- package/dist/admin/layout-editor/section-overlay.d.ts.map +1 -1
- package/dist/admin/layout-editor/section-overlay.js +8 -8
- package/dist/admin/layout-editor/section-overlay.js.map +1 -1
- package/dist/admin/layout-editor/section-variant-container-config.js +5 -5
- package/dist/admin/layout-editor/section-variant-container-config.js.map +1 -1
- package/dist/admin/layout-editor/use-chrome-flip.d.ts +111 -11
- package/dist/admin/layout-editor/use-chrome-flip.d.ts.map +1 -1
- package/dist/admin/layout-editor/use-chrome-flip.js +233 -33
- package/dist/admin/layout-editor/use-chrome-flip.js.map +1 -1
- package/dist/admin/layout-editor/use-editor-dnd.d.ts +44 -1
- package/dist/admin/layout-editor/use-editor-dnd.d.ts.map +1 -1
- package/dist/admin/layout-editor/use-editor-dnd.js +191 -3
- package/dist/admin/layout-editor/use-editor-dnd.js.map +1 -1
- package/dist/admin/styles/admin.css +22 -1
- package/dist/admin/surfaces/layouts/layout-editor-client.d.ts.map +1 -1
- package/dist/admin/surfaces/layouts/layout-editor-client.js +12 -1
- package/dist/admin/surfaces/layouts/layout-editor-client.js.map +1 -1
- package/dist/api/content-routes.d.ts.map +1 -1
- package/dist/api/content-routes.js +34 -7
- package/dist/api/content-routes.js.map +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +7 -73
- package/dist/api/index.js.map +1 -1
- package/dist/cache/content-invalidation.d.ts +28 -0
- package/dist/cache/content-invalidation.d.ts.map +1 -0
- package/dist/cache/content-invalidation.js +72 -0
- package/dist/cache/content-invalidation.js.map +1 -0
- package/dist/db/content-restore.d.ts +5 -1
- package/dist/db/content-restore.d.ts.map +1 -1
- package/dist/db/content-restore.js +9 -2
- package/dist/db/content-restore.js.map +1 -1
- package/dist/layout/comments-module.d.ts.map +1 -1
- package/dist/layout/comments-module.js +6 -2
- package/dist/layout/comments-module.js.map +1 -1
- package/dist/layout/module-render-context.d.ts +32 -0
- package/dist/layout/module-render-context.d.ts.map +1 -1
- package/dist/layout/module-render-context.js +32 -1
- package/dist/layout/module-render-context.js.map +1 -1
- package/dist/layout/modules-extended.d.ts.map +1 -1
- package/dist/layout/modules-extended.js +21 -11
- package/dist/layout/modules-extended.js.map +1 -1
- package/dist/layout/modules.d.ts.map +1 -1
- package/dist/layout/modules.js +15 -10
- package/dist/layout/modules.js.map +1 -1
- package/dist/scope/inline-editing.d.ts.map +1 -1
- package/dist/scope/inline-editing.js +10 -0
- package/dist/scope/inline-editing.js.map +1 -1
- package/dist/scope/sticky-offset.d.ts +185 -0
- package/dist/scope/sticky-offset.d.ts.map +1 -0
- package/dist/scope/sticky-offset.js +432 -0
- package/dist/scope/sticky-offset.js.map +1 -0
- package/dist/site/styles.css +41 -3
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { readStickyOffsetPx } from '../../scope/sticky-offset.js';
|
|
3
4
|
/**
|
|
4
5
|
* Height of the editor's fixed top bar, mirroring `--kywi-overlay-bar-height`
|
|
5
6
|
* in editor.css (the page itself is offset by that token via
|
|
6
|
-
* `.kywi-frontend-edit--editing { padding-top: … }
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* `.kywi-frontend-edit--editing { padding-top: … }`, and the site's own
|
|
8
|
+
* top-anchored elements by `sticky-offset.ts`). It has to be a number here —
|
|
9
|
+
* this is arithmetic against a `getBoundingClientRect()`, not a layout value
|
|
10
|
+
* CSS could resolve — so all of those must be changed together.
|
|
9
11
|
*/
|
|
10
12
|
const BAR_HEIGHT_PX = 46;
|
|
11
13
|
/** One chrome row: the height a floating section/container bar occupies. */
|
|
@@ -13,8 +15,68 @@ const CHROME_ROW_PX = 40;
|
|
|
13
15
|
/**
|
|
14
16
|
* An owner whose top edge sits above this line has no room for a chrome row
|
|
15
17
|
* between the fixed bar and itself, so its chrome has to move.
|
|
18
|
+
*
|
|
19
|
+
* The floor, not the whole story: a page whose own header is `position: sticky;
|
|
20
|
+
* top: 0` has a SECOND band under the editor's bar, because the sticky-offset
|
|
21
|
+
* detector re-pins that header just below it (kywi-cms#146). Clearing only the
|
|
22
|
+
* editor's bar then lands the chrome inside the site's nav — measured on the
|
|
23
|
+
* reference app at a clamped chrome of y 60–92 against a nav band of 46–111.
|
|
24
|
+
* So the live strip is this plus whatever the detector published; see
|
|
25
|
+
* {@link topStripPx}.
|
|
16
26
|
*/
|
|
17
27
|
export const BAR_STRIP_PX = BAR_HEIGHT_PX + CHROME_ROW_PX;
|
|
28
|
+
/**
|
|
29
|
+
* Width of the open module-palette rail, used when `--kywi-overlay-rail-left`
|
|
30
|
+
* cannot be read (server render, a host that lazy-loads editor.css after the
|
|
31
|
+
* first measurement, jsdom). Mirrors `--kywi-editor-panel-width-left` in
|
|
32
|
+
* editor.css, which the rail token resolves to.
|
|
33
|
+
*
|
|
34
|
+
* The rail is `box-sizing: border-box` (it carries `.kywi-overlay-chrome`, see
|
|
35
|
+
* the box-sizing rule at the top of editor.css), so this number is the rail's
|
|
36
|
+
* FULL on-screen footprint — its 1px right border included. That is what makes
|
|
37
|
+
* "read the token" and "measure the rail" the same answer, and it is why the
|
|
38
|
+
* token is the source here: it is stable while the rail's own width transition
|
|
39
|
+
* is still running, which a rect read during a palette toggle is not.
|
|
40
|
+
*/
|
|
41
|
+
export const RAIL_WIDTH_FALLBACK_PX = 232;
|
|
42
|
+
/**
|
|
43
|
+
* The narrowest strip of owner worth pushing the bar into. A chrome bar cannot
|
|
44
|
+
* be pushed so far right that its LEFT edge leaves its owner: it is absolutely
|
|
45
|
+
* positioned INSIDE the owner, and a bar that starts past a narrow band's right
|
|
46
|
+
* edge points at nothing. So on an owner too narrow to both clear the rail and
|
|
47
|
+
* still leave this much room, the offset stops here and the bar stays (partly)
|
|
48
|
+
* behind the rail — which the owner fixes with one click on the palette toggle.
|
|
49
|
+
*
|
|
50
|
+
* What this actually guarantees, precisely: the bar's left edge stays inside the
|
|
51
|
+
* owner with at least 200px of the owner to its right — NOT that the whole bar
|
|
52
|
+
* fits. The in-place bar renders its own controls plus Personalize and A/B, so a
|
|
53
|
+
* fully-actioned one is nearer 300px wide and can still overhang the owner's
|
|
54
|
+
* right edge; the chrome carries no max-width to stop it. That overhang is
|
|
55
|
+
* cosmetic (the buttons remain over the page, clickable, and visibly attached to
|
|
56
|
+
* their band) and deliberate: the alternative is refusing to move the bar out
|
|
57
|
+
* from under the rail at all, which is the bug this cap exists inside of.
|
|
58
|
+
*/
|
|
59
|
+
export const CHROME_MIN_WIDTH_PX = 200;
|
|
60
|
+
/**
|
|
61
|
+
* Current width of the left rail, from the token the rail itself is sized by,
|
|
62
|
+
* so a host that re-themes `--kywi-editor-panel-width-left` moves the chrome
|
|
63
|
+
* with the rail instead of leaving it behind a wider one.
|
|
64
|
+
*
|
|
65
|
+
* `getPropertyValue` returns the custom property's COMPUTED value, i.e. with
|
|
66
|
+
* its own `var()` reference already substituted — so this reads "232px", not
|
|
67
|
+
* "var(--kywi-editor-panel-width-left)". Either way a value this cannot parse
|
|
68
|
+
* falls back rather than producing NaN arithmetic.
|
|
69
|
+
*/
|
|
70
|
+
function readRailWidthPx() {
|
|
71
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
72
|
+
return RAIL_WIDTH_FALLBACK_PX;
|
|
73
|
+
}
|
|
74
|
+
const raw = window
|
|
75
|
+
.getComputedStyle(document.documentElement)
|
|
76
|
+
.getPropertyValue('--kywi-overlay-rail-left');
|
|
77
|
+
const parsed = Number.parseFloat(raw);
|
|
78
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : RAIL_WIDTH_FALLBACK_PX;
|
|
79
|
+
}
|
|
18
80
|
/**
|
|
19
81
|
* In-place chrome floats ABOVE its owner (`transform: translateY(-100%)`, see
|
|
20
82
|
* `.kywi-ip-section-chrome` / `.kywi-ip-vc-chrome` in editor.css). For an owner
|
|
@@ -24,10 +86,39 @@ export const BAR_STRIP_PX = BAR_HEIGHT_PX + CHROME_ROW_PX;
|
|
|
24
86
|
* an edge-adjacent element's chrome can never cover the rails (kywi-cms#118).
|
|
25
87
|
* The fix is therefore positional (kywi-cms#122).
|
|
26
88
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
89
|
+
* The same argument runs sideways for the open module palette (kywi-cms#132).
|
|
90
|
+
* That rail is fixed to the left viewport edge, 232px wide, and out-ranks
|
|
91
|
+
* chrome by the same deliberate tier — so a FULL-WIDTH section, whose chrome
|
|
92
|
+
* renders at the section's own `left: 0`, has its grip, +Col, Personalize, A/B
|
|
93
|
+
* and × buttons sitting under the palette, unclickable. z-index cannot rescue
|
|
94
|
+
* that either (the rail must stay on top), and unlike an arbitrary site header
|
|
95
|
+
* the rail's footprint is a value the editor owns, so the fix is again
|
|
96
|
+
* positional: push the chrome right until it clears the rail.
|
|
97
|
+
*
|
|
98
|
+
* The vertical strip is not always just the bar, though. On a site whose header
|
|
99
|
+
* is `position: sticky; top: 0` — the common theme pattern — the editor re-pins
|
|
100
|
+
* that header directly under its own bar for the session (sticky-offset.ts,
|
|
101
|
+
* kywi-cms#146), so there is a SECOND band to clear. Clearing only the bar puts
|
|
102
|
+
* the chrome inside the site's nav: measured on the reference app at a clamped
|
|
103
|
+
* chrome of y 60–92 against a nav band of 46–111. The detector publishes that
|
|
104
|
+
* band's height and the clamp adds it, which is a no-op (0) on a page with no
|
|
105
|
+
* top-anchored chrome — i.e. every page the #122 geometry was written against.
|
|
106
|
+
*
|
|
107
|
+
* Returns both offsets from ONE rect read, in px:
|
|
108
|
+
* - `clampPx` — distance to pin the chrome DOWN from its owner's top edge so
|
|
109
|
+
* it lands just under the fixed bar (and the site's offset nav, if any), or
|
|
110
|
+
* null when the owner has room above it and the ordinary above-position
|
|
111
|
+
* applies. The caller turns that into `--kywi-ip-chrome-clamp` +
|
|
112
|
+
* `.kywi-ip-chrome--clamped`.
|
|
113
|
+
* - `railAvoidPx` — distance to push the chrome RIGHT so it clears the open
|
|
114
|
+
* palette rail, or null when the palette is closed or the owner already
|
|
115
|
+
* starts to the right of it. The caller turns that into
|
|
116
|
+
* `--kywi-ip-chrome-left` + `.kywi-ip-chrome--rail-avoid`.
|
|
117
|
+
*
|
|
118
|
+
* One hook rather than two: both offsets are read off the same
|
|
119
|
+
* `getBoundingClientRect()`, which is also the rule the drag work landed on
|
|
120
|
+
* (kywi-cms#136) — chrome geometry comes from rects taken in one frame, never
|
|
121
|
+
* from separately-sampled measurements that can disagree by a scroll.
|
|
31
122
|
*
|
|
32
123
|
* Pinning inside the owner rather than hanging the bar off the owner's BOTTOM
|
|
33
124
|
* edge (the first shape this fix took): a hero taller than the viewport put its
|
|
@@ -36,49 +127,106 @@ export const BAR_STRIP_PX = BAR_HEIGHT_PX + CHROME_ROW_PX;
|
|
|
36
127
|
* app. Clamping keeps the bar one row under the editor's own bar for as long as
|
|
37
128
|
* any of its owner is on screen, which is the whole point of the fix.
|
|
38
129
|
*
|
|
39
|
-
* Re-measured on mount
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
130
|
+
* Re-measured on mount, on every scroll/resize, and whenever the owner's own box
|
|
131
|
+
* changes — a `ResizeObserver`, for the layout moves no window event reports
|
|
132
|
+
* (see the effect). The owner's top edge moves relative to the bar as the reader
|
|
133
|
+
* scrolls, and while clamped the offset has to track it pixel for pixel. That is
|
|
134
|
+
* a style write per scroll for the one or two chromes near the top strip; every
|
|
135
|
+
* other chrome returns a stable value (null, or the bottom clamp below) and
|
|
136
|
+
* React bails out without touching the DOM at all.
|
|
45
137
|
*
|
|
46
138
|
* @param ownerRef the element the chrome is positioned against (the section or
|
|
47
139
|
* variant-container root). A null ref never clamps.
|
|
48
140
|
* @param enabled pass false for surfaces with no floating chrome — the admin
|
|
49
141
|
* artboard has its own header and in-flow toolbars, so it must neither clamp
|
|
50
142
|
* nor pay for the listeners.
|
|
143
|
+
* @param paletteOpen whether the left module-palette rail is currently expanded.
|
|
144
|
+
* In-place only; the admin Layout tab's palette is an in-flow column that
|
|
145
|
+
* displaces the canvas rather than floating over it, so nothing there has a
|
|
146
|
+
* rail to avoid. Part of the effect's deps, so toggling the palette
|
|
147
|
+
* re-measures immediately rather than waiting for the next scroll.
|
|
51
148
|
*/
|
|
52
|
-
export function useChromeFlip(ownerRef, enabled = true) {
|
|
149
|
+
export function useChromeFlip(ownerRef, enabled = true, paletteOpen = false) {
|
|
53
150
|
const [clampOffsetPx, setClampOffsetPx] = React.useState(null);
|
|
151
|
+
const [railAvoidOffsetPx, setRailAvoidOffsetPx] = React.useState(null);
|
|
54
152
|
React.useEffect(() => {
|
|
55
153
|
if (!enabled || typeof window === 'undefined') {
|
|
56
154
|
setClampOffsetPx(null);
|
|
155
|
+
setRailAvoidOffsetPx(null);
|
|
57
156
|
return;
|
|
58
157
|
}
|
|
158
|
+
// Read once per effect run, not per scroll: this is a theme token, so it
|
|
159
|
+
// can only change with a stylesheet, and `getComputedStyle` in a scroll
|
|
160
|
+
// handler is exactly the kind of per-frame style read the clamp's own
|
|
161
|
+
// comment is careful to avoid. A palette toggle re-runs the effect (it is
|
|
162
|
+
// in the deps), which is also when a re-read could matter.
|
|
163
|
+
const railWidthPx = readRailWidthPx();
|
|
59
164
|
const measure = () => {
|
|
60
165
|
const owner = ownerRef.current;
|
|
61
166
|
if (!owner)
|
|
62
167
|
return;
|
|
63
|
-
|
|
64
|
-
|
|
168
|
+
// ONE rect for both axes — see the kywi-cms#136 note above.
|
|
169
|
+
const { top, height, left, width } = owner.getBoundingClientRect();
|
|
170
|
+
// ── Vertical: clear the editor's fixed top bar (kywi-cms#122) and the
|
|
171
|
+
// site's offset sticky band on top of it (kywi-cms#146) ─────────────
|
|
172
|
+
//
|
|
173
|
+
// Read per measurement, unlike the rail width above: that is a theme
|
|
174
|
+
// token which can only change with a stylesheet, whereas this tracks a
|
|
175
|
+
// live element whose height changes when its nav wraps. Cheap enough for
|
|
176
|
+
// a scroll handler because `readStickyOffsetPx` reads back the INLINE
|
|
177
|
+
// custom property the detector wrote — a CSSOM read, not a
|
|
178
|
+
// `getComputedStyle` that could force a style recalculation. 0 (the whole
|
|
179
|
+
// pre-#146 behaviour) whenever the page has no offset top band.
|
|
180
|
+
const stickyPx = readStickyOffsetPx();
|
|
181
|
+
// Where a clamped bar has to land: under the editor's bar AND under the
|
|
182
|
+
// site's nav, which the detector re-pinned directly beneath it.
|
|
183
|
+
const stripBottomPx = BAR_HEIGHT_PX + stickyPx;
|
|
184
|
+
if (top >= BAR_STRIP_PX + stickyPx) {
|
|
65
185
|
setClampOffsetPx(null);
|
|
66
|
-
return;
|
|
67
186
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
187
|
+
else {
|
|
188
|
+
// How far down from the owner's top edge the bar has to sit to clear the
|
|
189
|
+
// fixed bar. Never negative: an owner already below the bar keeps its
|
|
190
|
+
// chrome flush with its own top edge rather than being pulled up into it.
|
|
191
|
+
const wanted = Math.max(0, stripBottomPx - top);
|
|
192
|
+
// …but never past the owner's own bottom edge either. Once the owner has
|
|
193
|
+
// scrolled far enough that the bar would otherwise hang below it, the bar
|
|
194
|
+
// rides out with its owner instead of sticking to the top of the screen
|
|
195
|
+
// over whatever the reader is actually looking at. This also caps the
|
|
196
|
+
// per-scroll churn: for anything well above the fold the offset stops
|
|
197
|
+
// changing, and the style writes stop with it.
|
|
198
|
+
const maxInsideOwner = Math.max(0, height - CHROME_ROW_PX);
|
|
199
|
+
// Rounded so sub-pixel rects (browser zoom, fractional layouts) don't
|
|
200
|
+
// re-render on every frame of a scroll that hasn't moved a whole pixel.
|
|
201
|
+
setClampOffsetPx(Math.round(Math.min(wanted, maxInsideOwner)));
|
|
202
|
+
}
|
|
203
|
+
// ── Horizontal: clear the open palette rail (kywi-cms#132) ────────────
|
|
204
|
+
if (!paletteOpen || left >= railWidthPx) {
|
|
205
|
+
// A closed palette, or an owner (a centred container, an inset band)
|
|
206
|
+
// that already starts to the right of the rail — the ordinary
|
|
207
|
+
// left-edge position is reachable, so leave it alone.
|
|
208
|
+
setRailAvoidOffsetPx(null);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// Full-width owners are the case in the issue: left 0, so the chrome
|
|
212
|
+
// needs the rail's whole width. A partly-covered owner needs only the
|
|
213
|
+
// overlap.
|
|
214
|
+
const wanted = railWidthPx - left;
|
|
215
|
+
// Capped so the bar's left edge stays inside the owner it belongs to.
|
|
216
|
+
// A band narrower than the rail plus a usable strip keeps whatever room
|
|
217
|
+
// it has; pushing further would start the bar past the band's own right
|
|
218
|
+
// edge, over the section beside it.
|
|
219
|
+
const maxInsideOwner = Math.max(0, width - CHROME_MIN_WIDTH_PX);
|
|
220
|
+
// Rounded AWAY from each obstacle, not to the nearest pixel. A
|
|
221
|
+
// fractional owner left (zoom, a centred layout on an odd viewport)
|
|
222
|
+
// rounded to nearest can land the bar half a pixel back INSIDE the rail
|
|
223
|
+
// — owner left 124.6 + round(107.4) = 231.6, against a rail that ends at
|
|
224
|
+
// 232 — which is both visibly clipped and a real hit-test failure at the
|
|
225
|
+
// bar's left edge. `ceil` on the push clears the rail; `floor` on the
|
|
226
|
+
// cap keeps the guarantee above (≥200px of owner to the bar's right)
|
|
227
|
+
// rather than overshooting it by the same sub-pixel.
|
|
228
|
+
setRailAvoidOffsetPx(Math.min(Math.ceil(wanted), Math.floor(maxInsideOwner)));
|
|
229
|
+
}
|
|
82
230
|
};
|
|
83
231
|
measure();
|
|
84
232
|
// `capture: true` so a scroll inside any nested scroll container on the
|
|
@@ -87,11 +235,63 @@ export function useChromeFlip(ownerRef, enabled = true) {
|
|
|
87
235
|
const listenerOptions = { passive: true, capture: true };
|
|
88
236
|
window.addEventListener('scroll', measure, listenerOptions);
|
|
89
237
|
window.addEventListener('resize', measure, listenerOptions);
|
|
238
|
+
// Scroll and resize are window events, and the owner can move under BOTH
|
|
239
|
+
// measurements without either firing. The live case is the responsive
|
|
240
|
+
// preview: switching Desktop → Mobile re-caps the page's max-width
|
|
241
|
+
// (`.kywi-ip-page--constrained`, overlay-shell), so every band's left edge
|
|
242
|
+
// and width jump inwards while the window is exactly the size it was and
|
|
243
|
+
// nothing scrolled. Nor is there a prop change to fall back on: the offsets
|
|
244
|
+
// are state inside this hook, so the canvas's React.memo has nothing to
|
|
245
|
+
// re-render on. Measured on the reference app at 1280 with the palette out:
|
|
246
|
+
// a band at x=124 takes a 108px push, and switching to Mobile moves it to
|
|
247
|
+
// x=452.5 — the bar stayed at 560.5, a third of the band's width adrift,
|
|
248
|
+
// until something scrolled. Observing the owner heals both axes; the same
|
|
249
|
+
// layout change is what moves its top edge relative to the fixed bar.
|
|
250
|
+
//
|
|
251
|
+
// Feature-detected rather than assumed: happy-dom ships an inert stub and
|
|
252
|
+
// older hosts have none, and a chrome that only re-measures on scroll is
|
|
253
|
+
// exactly the behaviour this shipped with, so a missing observer degrades
|
|
254
|
+
// instead of throwing.
|
|
255
|
+
const ResizeObserverCtor = window
|
|
256
|
+
.ResizeObserver;
|
|
257
|
+
let observer = null;
|
|
258
|
+
if (typeof ResizeObserverCtor === 'function') {
|
|
259
|
+
observer = new ResizeObserverCtor(measure);
|
|
260
|
+
const owner = ownerRef.current;
|
|
261
|
+
if (owner)
|
|
262
|
+
observer.observe(owner);
|
|
263
|
+
}
|
|
90
264
|
return () => {
|
|
91
265
|
window.removeEventListener('scroll', measure, listenerOptions);
|
|
92
266
|
window.removeEventListener('resize', measure, listenerOptions);
|
|
267
|
+
observer?.disconnect();
|
|
93
268
|
};
|
|
94
|
-
}, [ownerRef, enabled]);
|
|
95
|
-
return clampOffsetPx;
|
|
269
|
+
}, [ownerRef, enabled, paletteOpen]);
|
|
270
|
+
return React.useMemo(() => ({ clampPx: clampOffsetPx, railAvoidPx: railAvoidOffsetPx }), [clampOffsetPx, railAvoidOffsetPx]);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The modifier classes the measured offsets ask for, in the order the CSS
|
|
274
|
+
* expects them. Shared by both consumers (`SectionOverlay` and
|
|
275
|
+
* `VariantContainerBlock`) so a third one cannot half-implement the contract.
|
|
276
|
+
*/
|
|
277
|
+
export function chromeOffsetClasses(offsets) {
|
|
278
|
+
const classes = [];
|
|
279
|
+
if (offsets.clampPx !== null)
|
|
280
|
+
classes.push('kywi-ip-chrome--clamped');
|
|
281
|
+
if (offsets.railAvoidPx !== null)
|
|
282
|
+
classes.push('kywi-ip-chrome--rail-avoid');
|
|
283
|
+
return classes;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The custom properties those classes read, or undefined when neither offset
|
|
287
|
+
* applies (so the element keeps no `style` attribute at all in the common case).
|
|
288
|
+
*/
|
|
289
|
+
export function chromeOffsetStyle(offsets) {
|
|
290
|
+
if (offsets.clampPx === null && offsets.railAvoidPx === null)
|
|
291
|
+
return undefined;
|
|
292
|
+
return {
|
|
293
|
+
...(offsets.clampPx !== null ? { '--kywi-ip-chrome-clamp': `${offsets.clampPx}px` } : {}),
|
|
294
|
+
...(offsets.railAvoidPx !== null ? { '--kywi-ip-chrome-left': `${offsets.railAvoidPx}px` } : {}),
|
|
295
|
+
};
|
|
96
296
|
}
|
|
97
297
|
//# sourceMappingURL=use-chrome-flip.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chrome-flip.js","sourceRoot":"","sources":["../../../src/admin/layout-editor/use-chrome-flip.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AACZ,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB
|
|
1
|
+
{"version":3,"file":"use-chrome-flip.js","sourceRoot":"","sources":["../../../src/admin/layout-editor/use-chrome-flip.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AACZ,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AAEjE;;;;;;;GAOG;AACH,MAAM,aAAa,GAAG,EAAE,CAAA;AAExB,4EAA4E;AAC5E,MAAM,aAAa,GAAG,EAAE,CAAA;AAExB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,GAAG,aAAa,CAAA;AAEzD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAA;AAEzC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAA;AAiBtC;;;;;;;;;GASG;AACH,SAAS,eAAe;IACtB,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACrE,OAAO,sBAAsB,CAAA;IAC/B,CAAC;IACD,MAAM,GAAG,GAAG,MAAM;SACf,gBAAgB,CAAC,QAAQ,CAAC,eAAe,CAAC;SAC1C,gBAAgB,CAAC,0BAA0B,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;IACrC,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,sBAAsB,CAAA;AAChF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,MAAM,UAAU,aAAa,CAC3B,QAA6C,EAC7C,OAAO,GAAG,IAAI,EACd,WAAW,GAAG,KAAK;IAEnB,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAC7E,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAErF,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,OAAO,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAC9C,gBAAgB,CAAC,IAAI,CAAC,CAAA;YACtB,oBAAoB,CAAC,IAAI,CAAC,CAAA;YAC1B,OAAM;QACR,CAAC;QAED,yEAAyE;QACzE,wEAAwE;QACxE,sEAAsE;QACtE,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,WAAW,GAAG,eAAe,EAAE,CAAA;QAErC,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAA;YAC9B,IAAI,CAAC,KAAK;gBAAE,OAAM;YAClB,4DAA4D;YAC5D,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,qBAAqB,EAAE,CAAA;YAElE,uEAAuE;YACvE,yEAAyE;YACzE,EAAE;YACF,qEAAqE;YACrE,uEAAuE;YACvE,yEAAyE;YACzE,sEAAsE;YACtE,2DAA2D;YAC3D,0EAA0E;YAC1E,gEAAgE;YAChE,MAAM,QAAQ,GAAG,kBAAkB,EAAE,CAAA;YACrC,wEAAwE;YACxE,gEAAgE;YAChE,MAAM,aAAa,GAAG,aAAa,GAAG,QAAQ,CAAA;YAC9C,IAAI,GAAG,IAAI,YAAY,GAAG,QAAQ,EAAE,CAAC;gBACnC,gBAAgB,CAAC,IAAI,CAAC,CAAA;YACxB,CAAC;iBAAM,CAAC;gBACN,yEAAyE;gBACzE,sEAAsE;gBACtE,0EAA0E;gBAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,GAAG,GAAG,CAAC,CAAA;gBAC/C,yEAAyE;gBACzE,0EAA0E;gBAC1E,wEAAwE;gBACxE,sEAAsE;gBACtE,sEAAsE;gBACtE,+CAA+C;gBAC/C,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,CAAA;gBAC1D,sEAAsE;gBACtE,wEAAwE;gBACxE,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAA;YAChE,CAAC;YAED,yEAAyE;YACzE,IAAI,CAAC,WAAW,IAAI,IAAI,IAAI,WAAW,EAAE,CAAC;gBACxC,qEAAqE;gBACrE,8DAA8D;gBAC9D,sDAAsD;gBACtD,oBAAoB,CAAC,IAAI,CAAC,CAAA;YAC5B,CAAC;iBAAM,CAAC;gBACN,qEAAqE;gBACrE,sEAAsE;gBACtE,WAAW;gBACX,MAAM,MAAM,GAAG,WAAW,GAAG,IAAI,CAAA;gBACjC,sEAAsE;gBACtE,wEAAwE;gBACxE,wEAAwE;gBACxE,oCAAoC;gBACpC,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,mBAAmB,CAAC,CAAA;gBAC/D,+DAA+D;gBAC/D,oEAAoE;gBACpE,wEAAwE;gBACxE,yEAAyE;gBACzE,yEAAyE;gBACzE,sEAAsE;gBACtE,qEAAqE;gBACrE,qDAAqD;gBACrD,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAA;YAC/E,CAAC;QACH,CAAC,CAAA;QAED,OAAO,EAAE,CAAA;QACT,wEAAwE;QACxE,yEAAyE;QACzE,0DAA0D;QAC1D,MAAM,eAAe,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAW,CAAA;QACjE,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,eAAe,CAAC,CAAA;QAC3D,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,eAAe,CAAC,CAAA;QAE3D,yEAAyE;QACzE,sEAAsE;QACtE,mEAAmE;QACnE,2EAA2E;QAC3E,yEAAyE;QACzE,4EAA4E;QAC5E,wEAAwE;QACxE,4EAA4E;QAC5E,0EAA0E;QAC1E,yEAAyE;QACzE,0EAA0E;QAC1E,sEAAsE;QACtE,EAAE;QACF,0EAA0E;QAC1E,yEAAyE;QACzE,0EAA0E;QAC1E,uBAAuB;QACvB,MAAM,kBAAkB,GAAI,MAA8D;aACvF,cAAc,CAAA;QACjB,IAAI,QAAQ,GAA0B,IAAI,CAAA;QAC1C,IAAI,OAAO,kBAAkB,KAAK,UAAU,EAAE,CAAC;YAC7C,QAAQ,GAAG,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAA;YAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAA;YAC9B,IAAI,KAAK;gBAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QACpC,CAAC;QAED,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,eAAe,CAAC,CAAA;YAC9D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,eAAe,CAAC,CAAA;YAC9D,QAAQ,EAAE,UAAU,EAAE,CAAA;QACxB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAA;IAEpC,OAAO,KAAK,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC,EAClE,CAAC,aAAa,EAAE,iBAAiB,CAAC,CACnC,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAsB;IACxD,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;IACrE,IAAI,OAAO,CAAC,WAAW,KAAK,IAAI;QAAE,OAAO,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAA;IAC5E,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsB;IACtD,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,WAAW,KAAK,IAAI;QAAE,OAAO,SAAS,CAAA;IAC9E,OAAO;QACL,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,wBAAwB,EAAE,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzF,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1E,CAAA;AAC1B,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useSensors } from '@dnd-kit/core';
|
|
3
|
-
import type { CollisionDetection, DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
|
3
|
+
import type { Announcements, CollisionDetection, DragEndEvent, DragMoveEvent, DragStartEvent, KeyboardCoordinateGetter } from '@dnd-kit/core';
|
|
4
4
|
import type { ModuleRegistry } from '../../layout/registry.js';
|
|
5
5
|
import type { EditorAction } from './editor-state.js';
|
|
6
6
|
import type { ActiveDrag, DropTarget } from './drag-feedback.js';
|
|
@@ -31,6 +31,49 @@ export interface EditorDnd {
|
|
|
31
31
|
handleDragMove: (event: DragMoveEvent) => void;
|
|
32
32
|
handleDragEnd: (event: DragEndEvent) => void;
|
|
33
33
|
handleDragCancel: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Spread onto `<DndContext accessibility={...}>` in both shells so a
|
|
36
|
+
* keyboard drag (kywi-cms#139) narrates itself the same way a pointer drag
|
|
37
|
+
* shows itself: what's picked up, where it is, and what happened at drop or
|
|
38
|
+
* cancel. dnd-kit renders these into its own visually-hidden live region —
|
|
39
|
+
* no `container` override, unlike audience-list.tsx's — because neither
|
|
40
|
+
* shell nests DndContext inside an element (like `<table>`) that rejects a
|
|
41
|
+
* bare `<div>` child.
|
|
42
|
+
*/
|
|
43
|
+
accessibility: {
|
|
44
|
+
announcements: Announcements;
|
|
45
|
+
};
|
|
34
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* kywi-cms#139: `sortableKeyboardCoordinates` — the repo's standing
|
|
49
|
+
* coordinateGetter for every other keyboard drag (audience-list.tsx,
|
|
50
|
+
* site-tree.tsx, site-tree-table.tsx, menu-editor.tsx) — turned out to be
|
|
51
|
+
* silently inert for a palette item. Verified by hand: Tab to a palette item,
|
|
52
|
+
* Space to pick it up, arrow keys did *nothing* — no drop indicator, no
|
|
53
|
+
* screen-reader "over" announcement, and Space dropped it "not over a drop
|
|
54
|
+
* target" every time.
|
|
55
|
+
*
|
|
56
|
+
* The cause is in dnd-kit's own implementation, not this repo: it resolves
|
|
57
|
+
* the dragged item's OWN slot via `droppableContainers.get(active.id)`, to
|
|
58
|
+
* offset the move against where the item already sits in a shared sortable
|
|
59
|
+
* list. Every other caller drags a `useSortable` row — which, being sortable,
|
|
60
|
+
* is also its own droppable, so that lookup succeeds. A palette item
|
|
61
|
+
* (module-palette.tsx) is a plain `useDraggable` with no droppable
|
|
62
|
+
* counterpart of its own; the lookup misses, an internal guard silently
|
|
63
|
+
* fails, and the getter returns nothing — dnd-kit never even calls
|
|
64
|
+
* `onMove`, so no re-render, no collision recompute, no announcement.
|
|
65
|
+
*
|
|
66
|
+
* Delegate to the real thing whenever the active item IS also a droppable —
|
|
67
|
+
* a canvas reorder (module-overlay.tsx's `useSortable`) behaves exactly as
|
|
68
|
+
* it did before this existed. Only when that lookup misses (a palette item)
|
|
69
|
+
* does this take over, with the same direction-filter-then-closestCorners
|
|
70
|
+
* dnd-kit's own version uses, landing the drag on the target droppable's
|
|
71
|
+
* top-left corner rather than computing a same-list offset that doesn't
|
|
72
|
+
* apply here. That is enough: `resolveDragEndAction`/`resolvePaletteDropSlot`
|
|
73
|
+
* read the drop from `over.data`, not from the translate's exact pixels, so
|
|
74
|
+
* a keyboard move only has to make `over` become the right droppable — which
|
|
75
|
+
* this does — not reproduce dnd-kit's reorder-preview polish.
|
|
76
|
+
*/
|
|
77
|
+
export declare const paletteAwareKeyboardCoordinates: KeyboardCoordinateGetter;
|
|
35
78
|
export declare function useEditorDnd({ registry, dispatch }: EditorDndOptions): EditorDnd;
|
|
36
79
|
//# sourceMappingURL=use-editor-dnd.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-editor-dnd.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/use-editor-dnd.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"use-editor-dnd.d.ts","sourceRoot":"","sources":["../../../src/admin/layout-editor/use-editor-dnd.ts"],"names":[],"mappings":"AACA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAChF,OAAO,EAIL,UAAU,EAIX,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAEV,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,cAAc,EAEd,wBAAwB,EAEzB,MAAM,eAAe,CAAA;AAEtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AASrD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEhE;;;;;;GAMG;AAEH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAA;CACvC;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IACtC,kBAAkB,EAAE,kBAAkB,CAAA;IACtC,kEAAkE;IAClE,UAAU,EAAE,UAAU,GAAG,IAAI,CAAA;IAC7B,6EAA6E;IAC7E,UAAU,EAAE,UAAU,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAA;IAChD,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAC9C,aAAa,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B;;;;;;;;OAQG;IACH,aAAa,EAAE;QAAE,aAAa,EAAE,aAAa,CAAA;KAAE,CAAA;CAChD;AA0ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,+BAA+B,EAAE,wBAwD7C,CAAA;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,gBAAgB,GAAG,SAAS,CAuOhF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { PointerSensor, KeyboardSensor, useSensors, useSensor, } from '@dnd-kit/core';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { PointerSensor, KeyboardSensor, KeyboardCode, useSensors, useSensor, closestCorners, getFirstCollision, } from '@dnd-kit/core';
|
|
4
|
+
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
|
4
5
|
import { createDwellTimer, editorCollisionDetection, resolveDragEndAction, resolvePaletteDropSlot, } from './drag-utils.js';
|
|
5
6
|
/**
|
|
6
7
|
* How long a drag has to hold one slot before the thin line grows into a
|
|
@@ -17,6 +18,149 @@ const DWELL_MS = 600;
|
|
|
17
18
|
function zoneKey(columnId, index) {
|
|
18
19
|
return `${columnId}:${index}`;
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The screen-reader subject of a drag: the registry label, for both a
|
|
23
|
+
* palette item and a canvas reorder — `ModuleOverlay`'s `useSortable` data
|
|
24
|
+
* carries `moduleType` too (kywi-cms#139 review), so both sources resolve the
|
|
25
|
+
* same way now. The generic fallback only fires if drag data is ever missing
|
|
26
|
+
* `moduleType` altogether (there is no such caller today; kept for safety).
|
|
27
|
+
*/
|
|
28
|
+
function describeSubject(data, labelFor) {
|
|
29
|
+
if (data?.moduleType)
|
|
30
|
+
return labelFor(data.moduleType);
|
|
31
|
+
return 'the module';
|
|
32
|
+
}
|
|
33
|
+
/** Upper-cases the first character, for `describeSubject`'s output at the
|
|
34
|
+
* start of a sentence (`onDragOver`/`onDragEnd`) — a registry label is
|
|
35
|
+
* already capitalized ("Heading"), so this only changes anything for the
|
|
36
|
+
* generic "the module" fallback ("The module …"). */
|
|
37
|
+
function capitalize(text) {
|
|
38
|
+
return text.length ? text[0].toUpperCase() + text.slice(1) : text;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Where a drag is over, or would drop, in words — position only, never the
|
|
42
|
+
* column's own id. Production column ids are `crypto.randomUUID()`, so
|
|
43
|
+
* speaking one (`column 3f2a9c1e-4b7d-4a1e-9c2f-8ab0d5e61a77`) would read 36
|
|
44
|
+
* hex-and-dash characters aloud on every arrow press (kywi-cms#139 review;
|
|
45
|
+
* caught late because the original tests' `col-1`/`col-2` fixtures read fine
|
|
46
|
+
* spoken and masked it). The announcement callbacks only receive `{ active,
|
|
47
|
+
* over }` (dnd-kit's `Accessibility.Arguments`) — no droppable list, no
|
|
48
|
+
* ordinal — so "column 2 of 3" isn't available without threading new layout
|
|
49
|
+
* state into the hook just to answer a screen reader once per keypress; the
|
|
50
|
+
* per-column position already on `over.data` (see `EditorDragDatum`) is.
|
|
51
|
+
* That index is deliberately the *declared* slot, not the pointer-corrected
|
|
52
|
+
* one `resolveDragEndAction` computes for a mouse drop: a keyboard drag has
|
|
53
|
+
* no pointer at all, so the declared slot is the only honest answer to
|
|
54
|
+
* "where does the screen reader say this is" that a keyboard user's own
|
|
55
|
+
* arrow presses actually produced.
|
|
56
|
+
*/
|
|
57
|
+
function describeLocation(over) {
|
|
58
|
+
const data = over?.data.current;
|
|
59
|
+
if (!data?.columnId)
|
|
60
|
+
return null;
|
|
61
|
+
const index = data.index ?? 0;
|
|
62
|
+
return `this column, position ${index + 1}`;
|
|
63
|
+
}
|
|
64
|
+
/** Arrow-key codes `sortableKeyboardCoordinates` (and this function) act on. */
|
|
65
|
+
const KEYBOARD_DIRECTIONS = [
|
|
66
|
+
KeyboardCode.Down,
|
|
67
|
+
KeyboardCode.Up,
|
|
68
|
+
KeyboardCode.Left,
|
|
69
|
+
KeyboardCode.Right,
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* kywi-cms#139: `sortableKeyboardCoordinates` — the repo's standing
|
|
73
|
+
* coordinateGetter for every other keyboard drag (audience-list.tsx,
|
|
74
|
+
* site-tree.tsx, site-tree-table.tsx, menu-editor.tsx) — turned out to be
|
|
75
|
+
* silently inert for a palette item. Verified by hand: Tab to a palette item,
|
|
76
|
+
* Space to pick it up, arrow keys did *nothing* — no drop indicator, no
|
|
77
|
+
* screen-reader "over" announcement, and Space dropped it "not over a drop
|
|
78
|
+
* target" every time.
|
|
79
|
+
*
|
|
80
|
+
* The cause is in dnd-kit's own implementation, not this repo: it resolves
|
|
81
|
+
* the dragged item's OWN slot via `droppableContainers.get(active.id)`, to
|
|
82
|
+
* offset the move against where the item already sits in a shared sortable
|
|
83
|
+
* list. Every other caller drags a `useSortable` row — which, being sortable,
|
|
84
|
+
* is also its own droppable, so that lookup succeeds. A palette item
|
|
85
|
+
* (module-palette.tsx) is a plain `useDraggable` with no droppable
|
|
86
|
+
* counterpart of its own; the lookup misses, an internal guard silently
|
|
87
|
+
* fails, and the getter returns nothing — dnd-kit never even calls
|
|
88
|
+
* `onMove`, so no re-render, no collision recompute, no announcement.
|
|
89
|
+
*
|
|
90
|
+
* Delegate to the real thing whenever the active item IS also a droppable —
|
|
91
|
+
* a canvas reorder (module-overlay.tsx's `useSortable`) behaves exactly as
|
|
92
|
+
* it did before this existed. Only when that lookup misses (a palette item)
|
|
93
|
+
* does this take over, with the same direction-filter-then-closestCorners
|
|
94
|
+
* dnd-kit's own version uses, landing the drag on the target droppable's
|
|
95
|
+
* top-left corner rather than computing a same-list offset that doesn't
|
|
96
|
+
* apply here. That is enough: `resolveDragEndAction`/`resolvePaletteDropSlot`
|
|
97
|
+
* read the drop from `over.data`, not from the translate's exact pixels, so
|
|
98
|
+
* a keyboard move only has to make `over` become the right droppable — which
|
|
99
|
+
* this does — not reproduce dnd-kit's reorder-preview polish.
|
|
100
|
+
*/
|
|
101
|
+
export const paletteAwareKeyboardCoordinates = (event, args) => {
|
|
102
|
+
const { context } = args;
|
|
103
|
+
if (context.droppableContainers.get(args.active)) {
|
|
104
|
+
return sortableKeyboardCoordinates(event, args);
|
|
105
|
+
}
|
|
106
|
+
if (!KEYBOARD_DIRECTIONS.includes(event.code))
|
|
107
|
+
return undefined;
|
|
108
|
+
// Hoisted here (dnd-kit's own getter does the same): an arrow press that
|
|
109
|
+
// resolves no candidate below still must not fall through to the browser's
|
|
110
|
+
// native page-scroll mid-drag — that would shift `scrollAdjustedTranslate`
|
|
111
|
+
// and could move `over` without the owner having pressed anything meant to
|
|
112
|
+
// move it (kywi-cms#139 review).
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
const { active, collisionRect, droppableRects, droppableContainers, over } = context;
|
|
115
|
+
if (!active || !collisionRect)
|
|
116
|
+
return undefined;
|
|
117
|
+
const candidates = [];
|
|
118
|
+
droppableContainers.getEnabled().forEach((entry) => {
|
|
119
|
+
if (!entry || entry.disabled)
|
|
120
|
+
return;
|
|
121
|
+
const rect = droppableRects.get(entry.id);
|
|
122
|
+
if (!rect)
|
|
123
|
+
return;
|
|
124
|
+
switch (event.code) {
|
|
125
|
+
case KeyboardCode.Down:
|
|
126
|
+
if (collisionRect.top < rect.top)
|
|
127
|
+
candidates.push(entry);
|
|
128
|
+
break;
|
|
129
|
+
case KeyboardCode.Up:
|
|
130
|
+
if (collisionRect.top > rect.top)
|
|
131
|
+
candidates.push(entry);
|
|
132
|
+
break;
|
|
133
|
+
case KeyboardCode.Left:
|
|
134
|
+
if (collisionRect.left > rect.left)
|
|
135
|
+
candidates.push(entry);
|
|
136
|
+
break;
|
|
137
|
+
case KeyboardCode.Right:
|
|
138
|
+
if (collisionRect.left < rect.left)
|
|
139
|
+
candidates.push(entry);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const collisions = closestCorners({
|
|
144
|
+
active,
|
|
145
|
+
collisionRect,
|
|
146
|
+
droppableRects,
|
|
147
|
+
droppableContainers: candidates,
|
|
148
|
+
pointerCoordinates: null,
|
|
149
|
+
});
|
|
150
|
+
let closestId = getFirstCollision(collisions, 'id');
|
|
151
|
+
// Same "don't re-announce where we already are" rule as dnd-kit's own
|
|
152
|
+
// version: if the nearest candidate is the droppable we're already over,
|
|
153
|
+
// and there is a second one, prefer that — otherwise repeated presses in
|
|
154
|
+
// the same direction would never move past the first neighbour.
|
|
155
|
+
if (closestId === over?.id && collisions.length > 1)
|
|
156
|
+
closestId = collisions[1]?.id ?? null;
|
|
157
|
+
if (closestId == null)
|
|
158
|
+
return undefined;
|
|
159
|
+
const targetRect = droppableRects.get(closestId);
|
|
160
|
+
if (!targetRect)
|
|
161
|
+
return undefined;
|
|
162
|
+
return { x: targetRect.left, y: targetRect.top };
|
|
163
|
+
};
|
|
20
164
|
export function useEditorDnd({ registry, dispatch }) {
|
|
21
165
|
const sensors = useSensors(
|
|
22
166
|
// kywi-cms#134: without a distance constraint a bare CLICK on a palette
|
|
@@ -24,7 +168,14 @@ export function useEditorDnd({ registry, dispatch }) {
|
|
|
24
168
|
// resolves a drop target under the cursor and the editor silently inserts
|
|
25
169
|
// a module the user never dragged anywhere. Requiring 5px of travel makes
|
|
26
170
|
// a click a no-op and leaves a real drag untouched.
|
|
27
|
-
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
171
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
172
|
+
// kywi-cms#139: without a coordinateGetter, dnd-kit's KeyboardSensor has no
|
|
173
|
+
// way to translate an arrow-key press into a position at all — Tab could
|
|
174
|
+
// focus a palette item and Space could "pick it up", but arrows did
|
|
175
|
+
// nothing and the drag could never move. See `paletteAwareKeyboardCoordinates`
|
|
176
|
+
// above for why the repo's usual `sortableKeyboardCoordinates` doesn't
|
|
177
|
+
// reach a palette item on its own.
|
|
178
|
+
useSensor(KeyboardSensor, { coordinateGetter: paletteAwareKeyboardCoordinates }));
|
|
28
179
|
/**
|
|
29
180
|
* The pointer's live viewport position, in the same coordinate space
|
|
30
181
|
* `getBoundingClientRect()` reports. dnd-kit computes exactly this value on
|
|
@@ -85,6 +236,42 @@ export function useEditorDnd({ registry, dispatch }) {
|
|
|
85
236
|
* arrived in the same tick would see a stale (null) label.
|
|
86
237
|
*/
|
|
87
238
|
const labelFor = useCallback((moduleType) => registry?.get(moduleType)?.label ?? moduleType, [registry]);
|
|
239
|
+
// ── Screen-reader announcements (kywi-cms#139) ────────────────────────────
|
|
240
|
+
// A keyboard drag is otherwise silent: sighted mouse users see the ghost and
|
|
241
|
+
// drop indicator this hook already publishes above, but a screen-reader
|
|
242
|
+
// user picking a palette item up with Space and moving it with arrow keys
|
|
243
|
+
// gets no feedback that anything happened at all. dnd-kit renders whatever
|
|
244
|
+
// these return into its own visually-hidden live region.
|
|
245
|
+
const announcements = useMemo(() => ({
|
|
246
|
+
onDragStart({ active }) {
|
|
247
|
+
const data = active.data.current;
|
|
248
|
+
return `Picked up ${describeSubject(data, labelFor)}.`;
|
|
249
|
+
},
|
|
250
|
+
onDragOver({ active, over }) {
|
|
251
|
+
const data = active.data.current;
|
|
252
|
+
const subject = capitalize(describeSubject(data, labelFor));
|
|
253
|
+
const location = describeLocation(over);
|
|
254
|
+
return location
|
|
255
|
+
? `${subject} is over ${location}.`
|
|
256
|
+
: `${subject} is no longer over a drop target.`;
|
|
257
|
+
},
|
|
258
|
+
onDragEnd({ active, over }) {
|
|
259
|
+
const data = active.data.current;
|
|
260
|
+
const subject = capitalize(describeSubject(data, labelFor));
|
|
261
|
+
const location = describeLocation(over);
|
|
262
|
+
return location
|
|
263
|
+
? `${subject} was dropped over ${location}.`
|
|
264
|
+
: `${subject} was dropped, but not over a drop target.`;
|
|
265
|
+
},
|
|
266
|
+
onDragCancel({ active }) {
|
|
267
|
+
const data = active.data.current;
|
|
268
|
+
return `${capitalize(describeSubject(data, labelFor))} drag was cancelled.`;
|
|
269
|
+
},
|
|
270
|
+
}), [labelFor]);
|
|
271
|
+
// Stable identity like `announcements` itself: DndContext takes this whole
|
|
272
|
+
// object as a prop, and dnd-kit's own Accessibility component re-subscribes
|
|
273
|
+
// its live region whenever it changes.
|
|
274
|
+
const accessibility = useMemo(() => ({ announcements }), [announcements]);
|
|
88
275
|
/** Stand the whole feedback layer down — no ghost, no indicator, no timer. */
|
|
89
276
|
const clearFeedback = useCallback(() => {
|
|
90
277
|
dwell.reset();
|
|
@@ -180,6 +367,7 @@ export function useEditorDnd({ registry, dispatch }) {
|
|
|
180
367
|
handleDragMove,
|
|
181
368
|
handleDragEnd,
|
|
182
369
|
handleDragCancel,
|
|
370
|
+
accessibility,
|
|
183
371
|
};
|
|
184
372
|
}
|
|
185
373
|
//# sourceMappingURL=use-editor-dnd.js.map
|