@dumiflex/wp-kit 0.3.0 → 0.5.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 (130) hide show
  1. package/CHANGELOG.md +92 -51
  2. package/LICENSE +674 -674
  3. package/README.md +73 -76
  4. package/package.json +78 -77
  5. package/src/Avatar.vue +83 -83
  6. package/src/Badge.vue +97 -97
  7. package/src/Banner.vue +118 -118
  8. package/src/BookmarkButton.vue +27 -27
  9. package/src/BundleMeta.vue +51 -51
  10. package/src/Button.vue +103 -103
  11. package/src/Card.vue +68 -68
  12. package/src/Checkbox.vue +109 -107
  13. package/src/Chip.vue +163 -97
  14. package/src/ChoiceCards.vue +133 -133
  15. package/src/CloseButton.vue +69 -69
  16. package/src/CodeBlock.vue +146 -119
  17. package/src/ColorField.vue +188 -206
  18. package/src/ColorPicker.vue +201 -201
  19. package/src/Combobox.vue +331 -334
  20. package/src/ConfirmDialog.vue +165 -163
  21. package/src/ConfirmModal.vue +147 -147
  22. package/src/ConstraintMatrixPreview.vue +708 -708
  23. package/src/ContextMenu.vue +386 -379
  24. package/src/CopyButton.vue +156 -156
  25. package/src/DataTable.vue +293 -292
  26. package/src/DatePicker.vue +234 -234
  27. package/src/DescriptionList.vue +73 -73
  28. package/src/DiffView.vue +68 -68
  29. package/src/Disclosure.vue +166 -166
  30. package/src/DragHandle.vue +49 -49
  31. package/src/Drawer.vue +131 -131
  32. package/src/DropZone.vue +196 -196
  33. package/src/DropdownMenu.vue +72 -72
  34. package/src/EmptyState.vue +111 -111
  35. package/src/Field.vue +28 -28
  36. package/src/HsvPicker.vue +208 -208
  37. package/src/Icon.vue +39 -39
  38. package/src/IconPicker.vue +209 -209
  39. package/src/ImageGrid.vue +127 -125
  40. package/src/InlineEdit.vue +183 -183
  41. package/src/Input.vue +99 -99
  42. package/src/Kbd.vue +56 -56
  43. package/src/Lightbox.vue +176 -173
  44. package/src/ListFilter.vue +85 -85
  45. package/src/ListRow.vue +204 -204
  46. package/src/LoadingMark.vue +217 -161
  47. package/src/Logo.vue +60 -60
  48. package/src/Modal.vue +78 -76
  49. package/src/ModalShell.vue +109 -107
  50. package/src/Pagination.vue +106 -106
  51. package/src/Popover.vue +180 -171
  52. package/src/PostCard.vue +210 -211
  53. package/src/ProgressBar.vue +90 -90
  54. package/src/RadioGroup.vue +177 -177
  55. package/src/RangeSlider.vue +183 -183
  56. package/src/RelativeDate.vue +35 -35
  57. package/src/ResizeHandle.vue +143 -143
  58. package/src/SearchInput.vue +192 -199
  59. package/src/Select.vue +644 -641
  60. package/src/Separator.vue +54 -54
  61. package/src/Skeleton.vue +98 -82
  62. package/src/Slider.vue +198 -198
  63. package/src/SortableList.vue +260 -260
  64. package/src/SplitPane.vue +68 -68
  65. package/src/StarButton.vue +40 -40
  66. package/src/Stepper.vue +141 -141
  67. package/src/SupportLinks.vue +93 -93
  68. package/src/TagInput.vue +297 -300
  69. package/src/Textarea.vue +57 -57
  70. package/src/Timeline.vue +92 -92
  71. package/src/ToastCard.vue +119 -119
  72. package/src/ToastHost.vue +40 -40
  73. package/src/Toggle.vue +36 -36
  74. package/src/TokenizedValue.vue +310 -310
  75. package/src/Toolbar.vue +110 -110
  76. package/src/VirtualList.vue +87 -87
  77. package/src/VisuallyHidden.vue +29 -29
  78. package/src/WpButton.vue +75 -75
  79. package/src/WpCheck.vue +104 -102
  80. package/src/WpIconButton.vue +107 -107
  81. package/src/WpImageZoom.vue +544 -544
  82. package/src/WpNumber.vue +178 -183
  83. package/src/WpSegmented.vue +175 -175
  84. package/src/WpSelect.vue +93 -89
  85. package/src/WpSwitch.vue +163 -163
  86. package/src/WpTabs.vue +188 -188
  87. package/src/_modal-motion.css +33 -33
  88. package/src/_skeleton.css +50 -50
  89. package/src/axis-color.ts +42 -42
  90. package/src/button.css +169 -169
  91. package/src/calendar.ts +59 -59
  92. package/src/clipboard.ts +50 -50
  93. package/src/colour-presets.ts +12 -12
  94. package/src/diff.ts +51 -51
  95. package/src/drop-zone.ts +41 -41
  96. package/src/field.css +50 -0
  97. package/src/flip.ts +237 -237
  98. package/src/floating-layer.ts +48 -0
  99. package/src/floating.ts +99 -99
  100. package/src/highlight.ts +199 -0
  101. package/src/hsv.ts +100 -100
  102. package/src/icon-catalog.ts +52 -52
  103. package/src/image-zoom.ts +93 -93
  104. package/src/kind-colour.ts +28 -0
  105. package/src/loading-mark-motion.ts +249 -0
  106. package/src/logo-paths.ts +21 -21
  107. package/src/matrix-axis.ts +91 -91
  108. package/src/pagination.ts +27 -27
  109. package/src/platform-keys.ts +29 -29
  110. package/src/rich-tokenize.ts +357 -357
  111. package/src/select-types.ts +32 -32
  112. package/src/slider-math.ts +45 -45
  113. package/src/sortable.ts +22 -22
  114. package/src/theme/fonts/OFL-Fraunces.txt +93 -93
  115. package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
  116. package/src/theme/fonts/OFL-Manrope.txt +93 -93
  117. package/src/theme/looks.css +428 -428
  118. package/src/theme/palettes.css +254 -254
  119. package/src/theme/tokens.css +2896 -2894
  120. package/src/tokens.ts +56 -56
  121. package/src/tooltip.css +43 -43
  122. package/src/tooltip.ts +146 -146
  123. package/src/useDialogFocus.ts +73 -73
  124. package/src/useGrowableField.ts +357 -357
  125. package/src/useModalKeyShield.ts +116 -116
  126. package/src/useToast.ts +48 -48
  127. package/src/wp-logo.svg +14 -14
  128. package/src/ChipToggle.vue +0 -92
  129. package/src/KindChip.vue +0 -53
  130. package/src/RatingBadge.vue +0 -42
package/src/flip.ts CHANGED
@@ -1,237 +1,237 @@
1
- /**
2
- * Manual FLIP-move helper + reduce-motion gate. Shared across every
3
- * row-list surface in the extension — moved from
4
- * `src/components/context/bundles/flip.ts` (which was bundle-coupled
5
- * by directory but not by code) so InjectorRow + future row UIs can
6
- * reuse the same animation primitives instead of reinventing them.
7
- *
8
- * Used by ContextWidget's drop / picker-add / trash-remove paths to
9
- * animate row reordering, plus cross-container fade-slide and bundle
10
- * collapse/expand. Pure functions — no Vue imports, unit-testable.
11
- *
12
- * Reduce-motion contract: every helper checks `shouldAnimate()` which
13
- * returns false when `.wp-a11y-no-motion` is on `document.body`. CSS
14
- * transitions/animations are handled by a separate blanket rule in
15
- * src/components/shared/a11y.css; this module handles the JS side.
16
- *
17
- * Motion constants mirror the CSS tokens in
18
- * src/components/shared/theme.css (--wp-motion-*). Keep these in sync.
19
- *
20
- * Class-name customization: every animation helper accepts an
21
- * `opts.classes` field so different consumers can use their own
22
- * BEM-style modifier names (e.g. ModuleRow uses `wp-module--*`,
23
- * InjectorRow uses `wp-inj-row--*`). Defaults preserve the original
24
- * Context/Bundle behavior so ContextWidget keeps working without
25
- * any callsite change.
26
- */
27
-
28
- export const MOTION_COLLAPSE_MS = 450;
29
- export const MOTION_FLIP_MS = 280;
30
- export const MOTION_FADE_MS = 180;
31
- export const MOTION_PULSE_MS = 420;
32
- export const MOTION_STAGGER_MS = 60;
33
- export const MOTION_CURVE_FLIP = "cubic-bezier(0.22, 1, 0.36, 1)";
34
-
35
- type FlipOptions = { duration?: number; ease?: string };
36
-
37
- /** Per-consumer class name vocabulary. Each animation helper applies
38
- * these classes to mark rows mid-transition; the consumer's CSS
39
- * defines what the classes look like. Defaults to the original
40
- * ContextWidget/ModuleRow names for back-compat. */
41
- export interface AnimationClasses {
42
- arriving: string;
43
- arrived: string;
44
- leaving: string;
45
- flash: string;
46
- }
47
-
48
- const DEFAULT_CLASSES: AnimationClasses = {
49
- arriving: "wp-module--arriving",
50
- arrived: "wp-module--arrived",
51
- leaving: "wp-module--leaving",
52
- flash: "wp-module--flash",
53
- };
54
-
55
- /** Resolve user-supplied classes against the defaults so callers can
56
- * override only the names that differ. Internal helper. */
57
- function resolveClasses(partial?: Partial<AnimationClasses>): AnimationClasses {
58
- if (!partial) return DEFAULT_CLASSES;
59
- return { ...DEFAULT_CLASSES, ...partial };
60
- }
61
-
62
- /** Whether motion should animate. False when user has reduce-motion ON. */
63
- export function shouldAnimate(): boolean {
64
- return !document.body.classList.contains("wp-a11y-no-motion");
65
- }
66
-
67
- /** Capture each child's bounding rect, keyed by the caller's key function. */
68
- export function captureRects(
69
- container: HTMLElement,
70
- key: (el: HTMLElement) => string | null,
71
- ): Map<string, DOMRect> {
72
- const out = new Map<string, DOMRect>();
73
- for (const child of Array.from(container.children)) {
74
- const el = child as HTMLElement;
75
- const k = key(el);
76
- if (k != null) out.set(k, el.getBoundingClientRect());
77
- }
78
- return out;
79
- }
80
-
81
- /**
82
- * After a DOM mutation, apply inverse-transform to children whose
83
- * position changed, then transition back to identity. Skip silently
84
- * when reduce-motion is on.
85
- */
86
- export function applyFlip(
87
- container: HTMLElement,
88
- before: Map<string, DOMRect>,
89
- key: (el: HTMLElement) => string | null,
90
- opt: FlipOptions = {},
91
- ): void {
92
- if (!shouldAnimate()) return;
93
- const dur = opt.duration ?? MOTION_FLIP_MS;
94
- const ease = opt.ease ?? MOTION_CURVE_FLIP;
95
- for (const child of Array.from(container.children)) {
96
- const el = child as HTMLElement;
97
- const k = key(el);
98
- if (k == null) continue;
99
- const beforeRect = before.get(k);
100
- if (!beforeRect) continue;
101
- const afterRect = el.getBoundingClientRect();
102
- const dx = beforeRect.left - afterRect.left;
103
- const dy = beforeRect.top - afterRect.top;
104
- if (dx === 0 && dy === 0) continue;
105
- el.style.transition = "none";
106
- el.style.transform = `translate(${dx}px, ${dy}px)`;
107
- requestAnimationFrame(() => {
108
- el.style.transition = `transform ${dur}ms ${ease}`;
109
- el.style.transform = "translate(0, 0)";
110
- });
111
- }
112
- }
113
-
114
- /**
115
- * Mutate state then animate the newly-inserted element (matched by uid)
116
- * with --arriving → --arrived class transition. The arriving class is
117
- * applied after the mutation + a microtask so the browser registers
118
- * the "from" state; arrived flips the next frame so the transition fires.
119
- * No-op (mutate-only) when reduce-motion is on.
120
- */
121
- export async function withEnterAnimation(
122
- uid: string,
123
- container: HTMLElement,
124
- mutate: () => void,
125
- opts?: { classes?: Partial<AnimationClasses> },
126
- ): Promise<void> {
127
- mutate();
128
- if (!shouldAnimate()) return;
129
- const cls = resolveClasses(opts?.classes);
130
- // Wait one rAF so Vue commits the mutation to DOM before we query.
131
- // A bare `await Promise.resolve()` schedules a microtask that can
132
- // race Vue's own microtask-based reactivity scheduler — rAF runs
133
- // after the microtask queue drains and before paint.
134
- await new Promise<void>(r => requestAnimationFrame(() => r()));
135
- const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
136
- if (!el) return;
137
- el.classList.add(cls.arriving);
138
- await new Promise<void>(r => requestAnimationFrame(() => r()));
139
- el.classList.add(cls.arrived);
140
- const cleanup = (): void => {
141
- el.classList.remove(cls.arriving, cls.arrived);
142
- el.removeEventListener("transitionend", cleanup);
143
- };
144
- el.addEventListener("transitionend", cleanup, { once: true });
145
- }
146
-
147
- /**
148
- * Animate a batch of newly-inserted elements (matched by uid array)
149
- * with --arriving → --arrived class lifecycle. Caller has already
150
- * mutated state and is responsible for waiting nextTick if needed —
151
- * this helper waits one rAF before applying classes so Vue's commit
152
- * has landed.
153
- *
154
- * Two-pass design: apply all --arriving classes first (so every row
155
- * is at the from-state simultaneously), then apply all --arrived
156
- * classes on the next frame (so they all transition together). Avoids
157
- * a staircase effect where rows animate sequentially.
158
- *
159
- * No-op when reduce-motion is on.
160
- */
161
- export async function animateEnterBatch(
162
- uids: readonly (string | null | undefined)[],
163
- container: HTMLElement,
164
- opts?: { classes?: Partial<AnimationClasses> },
165
- ): Promise<void> {
166
- if (!shouldAnimate()) return;
167
- const cls = resolveClasses(opts?.classes);
168
- await new Promise<void>(r => requestAnimationFrame(() => r()));
169
- const elements: HTMLElement[] = [];
170
- for (const uid of uids) {
171
- if (!uid) continue;
172
- const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
173
- if (el) elements.push(el);
174
- }
175
- if (elements.length === 0) return;
176
- for (const el of elements) el.classList.add(cls.arriving);
177
- await new Promise<void>(r => requestAnimationFrame(() => r()));
178
- for (const el of elements) {
179
- el.classList.add(cls.arrived);
180
- const cleanup = (): void => {
181
- el.classList.remove(cls.arriving, cls.arrived);
182
- el.removeEventListener("transitionend", cleanup);
183
- };
184
- el.addEventListener("transitionend", cleanup, { once: true });
185
- }
186
- }
187
-
188
- /**
189
- * Flash a batch of rows with the existing wp-module--flash green ring
190
- * keyframe (1500ms) — used to indicate library-side mutations (refresh,
191
- * reset, fork). Staggered so bulk refresh cascades. No-op when reduce-
192
- * motion is on.
193
- */
194
- const FLASH_DURATION_MS = MOTION_PULSE_MS; // 420ms — matches drop pulse timing
195
- export async function flashRows(
196
- uids: readonly (string | null | undefined)[],
197
- container: HTMLElement,
198
- staggerMs: number = MOTION_STAGGER_MS,
199
- opts?: { classes?: Partial<AnimationClasses> },
200
- ): Promise<void> {
201
- if (!shouldAnimate()) return;
202
- const cls = resolveClasses(opts?.classes);
203
- await new Promise<void>(r => requestAnimationFrame(() => r()));
204
- for (let i = 0; i < uids.length; i++) {
205
- const uid = uids[i];
206
- if (!uid) continue;
207
- const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
208
- if (!el) continue;
209
- const delay = i * staggerMs;
210
- setTimeout(() => {
211
- el.classList.add(cls.flash);
212
- setTimeout(() => el.classList.remove(cls.flash), FLASH_DURATION_MS);
213
- }, delay);
214
- }
215
- }
216
-
217
- /**
218
- * Animate an element (matched by uid) with --leaving class, wait for the
219
- * fade to complete, then run mutate to remove from state. Mutate is
220
- * called immediately when reduce-motion is on (no class added).
221
- */
222
- export async function withLeaveAnimation(
223
- uid: string,
224
- container: HTMLElement,
225
- mutate: () => void,
226
- opts?: { classes?: Partial<AnimationClasses> },
227
- ): Promise<void> {
228
- if (!shouldAnimate()) {
229
- mutate();
230
- return;
231
- }
232
- const cls = resolveClasses(opts?.classes);
233
- const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
234
- if (el) el.classList.add(cls.leaving);
235
- await new Promise<void>(r => setTimeout(r, MOTION_FADE_MS));
236
- mutate();
237
- }
1
+ /**
2
+ * Manual FLIP-move helper + reduce-motion gate. Shared across every
3
+ * row-list surface in the extension — moved from
4
+ * `src/components/context/bundles/flip.ts` (which was bundle-coupled
5
+ * by directory but not by code) so InjectorRow + future row UIs can
6
+ * reuse the same animation primitives instead of reinventing them.
7
+ *
8
+ * Used by ContextWidget's drop / picker-add / trash-remove paths to
9
+ * animate row reordering, plus cross-container fade-slide and bundle
10
+ * collapse/expand. Pure functions — no Vue imports, unit-testable.
11
+ *
12
+ * Reduce-motion contract: every helper checks `shouldAnimate()` which
13
+ * returns false when `.wp-a11y-no-motion` is on `document.body`. CSS
14
+ * transitions/animations are handled by a separate blanket rule in
15
+ * src/components/shared/a11y.css; this module handles the JS side.
16
+ *
17
+ * Motion constants mirror the CSS tokens in
18
+ * src/components/shared/theme.css (--wp-motion-*). Keep these in sync.
19
+ *
20
+ * Class-name customization: every animation helper accepts an
21
+ * `opts.classes` field so different consumers can use their own
22
+ * BEM-style modifier names (e.g. ModuleRow uses `wp-module--*`,
23
+ * InjectorRow uses `wp-inj-row--*`). Defaults preserve the original
24
+ * Context/Bundle behavior so ContextWidget keeps working without
25
+ * any callsite change.
26
+ */
27
+
28
+ export const MOTION_COLLAPSE_MS = 450;
29
+ export const MOTION_FLIP_MS = 280;
30
+ export const MOTION_FADE_MS = 180;
31
+ export const MOTION_PULSE_MS = 420;
32
+ export const MOTION_STAGGER_MS = 60;
33
+ export const MOTION_CURVE_FLIP = "cubic-bezier(0.22, 1, 0.36, 1)";
34
+
35
+ type FlipOptions = { duration?: number; ease?: string };
36
+
37
+ /** Per-consumer class name vocabulary. Each animation helper applies
38
+ * these classes to mark rows mid-transition; the consumer's CSS
39
+ * defines what the classes look like. Defaults to the original
40
+ * ContextWidget/ModuleRow names for back-compat. */
41
+ export interface AnimationClasses {
42
+ arriving: string;
43
+ arrived: string;
44
+ leaving: string;
45
+ flash: string;
46
+ }
47
+
48
+ const DEFAULT_CLASSES: AnimationClasses = {
49
+ arriving: "wp-module--arriving",
50
+ arrived: "wp-module--arrived",
51
+ leaving: "wp-module--leaving",
52
+ flash: "wp-module--flash",
53
+ };
54
+
55
+ /** Resolve user-supplied classes against the defaults so callers can
56
+ * override only the names that differ. Internal helper. */
57
+ function resolveClasses(partial?: Partial<AnimationClasses>): AnimationClasses {
58
+ if (!partial) return DEFAULT_CLASSES;
59
+ return { ...DEFAULT_CLASSES, ...partial };
60
+ }
61
+
62
+ /** Whether motion should animate. False when user has reduce-motion ON. */
63
+ export function shouldAnimate(): boolean {
64
+ return !document.body.classList.contains("wp-a11y-no-motion");
65
+ }
66
+
67
+ /** Capture each child's bounding rect, keyed by the caller's key function. */
68
+ export function captureRects(
69
+ container: HTMLElement,
70
+ key: (el: HTMLElement) => string | null,
71
+ ): Map<string, DOMRect> {
72
+ const out = new Map<string, DOMRect>();
73
+ for (const child of Array.from(container.children)) {
74
+ const el = child as HTMLElement;
75
+ const k = key(el);
76
+ if (k != null) out.set(k, el.getBoundingClientRect());
77
+ }
78
+ return out;
79
+ }
80
+
81
+ /**
82
+ * After a DOM mutation, apply inverse-transform to children whose
83
+ * position changed, then transition back to identity. Skip silently
84
+ * when reduce-motion is on.
85
+ */
86
+ export function applyFlip(
87
+ container: HTMLElement,
88
+ before: Map<string, DOMRect>,
89
+ key: (el: HTMLElement) => string | null,
90
+ opt: FlipOptions = {},
91
+ ): void {
92
+ if (!shouldAnimate()) return;
93
+ const dur = opt.duration ?? MOTION_FLIP_MS;
94
+ const ease = opt.ease ?? MOTION_CURVE_FLIP;
95
+ for (const child of Array.from(container.children)) {
96
+ const el = child as HTMLElement;
97
+ const k = key(el);
98
+ if (k == null) continue;
99
+ const beforeRect = before.get(k);
100
+ if (!beforeRect) continue;
101
+ const afterRect = el.getBoundingClientRect();
102
+ const dx = beforeRect.left - afterRect.left;
103
+ const dy = beforeRect.top - afterRect.top;
104
+ if (dx === 0 && dy === 0) continue;
105
+ el.style.transition = "none";
106
+ el.style.transform = `translate(${dx}px, ${dy}px)`;
107
+ requestAnimationFrame(() => {
108
+ el.style.transition = `transform ${dur}ms ${ease}`;
109
+ el.style.transform = "translate(0, 0)";
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Mutate state then animate the newly-inserted element (matched by uid)
116
+ * with --arriving → --arrived class transition. The arriving class is
117
+ * applied after the mutation + a microtask so the browser registers
118
+ * the "from" state; arrived flips the next frame so the transition fires.
119
+ * No-op (mutate-only) when reduce-motion is on.
120
+ */
121
+ export async function withEnterAnimation(
122
+ uid: string,
123
+ container: HTMLElement,
124
+ mutate: () => void,
125
+ opts?: { classes?: Partial<AnimationClasses> },
126
+ ): Promise<void> {
127
+ mutate();
128
+ if (!shouldAnimate()) return;
129
+ const cls = resolveClasses(opts?.classes);
130
+ // Wait one rAF so Vue commits the mutation to DOM before we query.
131
+ // A bare `await Promise.resolve()` schedules a microtask that can
132
+ // race Vue's own microtask-based reactivity scheduler — rAF runs
133
+ // after the microtask queue drains and before paint.
134
+ await new Promise<void>(r => requestAnimationFrame(() => r()));
135
+ const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
136
+ if (!el) return;
137
+ el.classList.add(cls.arriving);
138
+ await new Promise<void>(r => requestAnimationFrame(() => r()));
139
+ el.classList.add(cls.arrived);
140
+ const cleanup = (): void => {
141
+ el.classList.remove(cls.arriving, cls.arrived);
142
+ el.removeEventListener("transitionend", cleanup);
143
+ };
144
+ el.addEventListener("transitionend", cleanup, { once: true });
145
+ }
146
+
147
+ /**
148
+ * Animate a batch of newly-inserted elements (matched by uid array)
149
+ * with --arriving → --arrived class lifecycle. Caller has already
150
+ * mutated state and is responsible for waiting nextTick if needed —
151
+ * this helper waits one rAF before applying classes so Vue's commit
152
+ * has landed.
153
+ *
154
+ * Two-pass design: apply all --arriving classes first (so every row
155
+ * is at the from-state simultaneously), then apply all --arrived
156
+ * classes on the next frame (so they all transition together). Avoids
157
+ * a staircase effect where rows animate sequentially.
158
+ *
159
+ * No-op when reduce-motion is on.
160
+ */
161
+ export async function animateEnterBatch(
162
+ uids: readonly (string | null | undefined)[],
163
+ container: HTMLElement,
164
+ opts?: { classes?: Partial<AnimationClasses> },
165
+ ): Promise<void> {
166
+ if (!shouldAnimate()) return;
167
+ const cls = resolveClasses(opts?.classes);
168
+ await new Promise<void>(r => requestAnimationFrame(() => r()));
169
+ const elements: HTMLElement[] = [];
170
+ for (const uid of uids) {
171
+ if (!uid) continue;
172
+ const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
173
+ if (el) elements.push(el);
174
+ }
175
+ if (elements.length === 0) return;
176
+ for (const el of elements) el.classList.add(cls.arriving);
177
+ await new Promise<void>(r => requestAnimationFrame(() => r()));
178
+ for (const el of elements) {
179
+ el.classList.add(cls.arrived);
180
+ const cleanup = (): void => {
181
+ el.classList.remove(cls.arriving, cls.arrived);
182
+ el.removeEventListener("transitionend", cleanup);
183
+ };
184
+ el.addEventListener("transitionend", cleanup, { once: true });
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Flash a batch of rows with the existing wp-module--flash green ring
190
+ * keyframe (1500ms) — used to indicate library-side mutations (refresh,
191
+ * reset, fork). Staggered so bulk refresh cascades. No-op when reduce-
192
+ * motion is on.
193
+ */
194
+ const FLASH_DURATION_MS = MOTION_PULSE_MS; // 420ms — matches drop pulse timing
195
+ export async function flashRows(
196
+ uids: readonly (string | null | undefined)[],
197
+ container: HTMLElement,
198
+ staggerMs: number = MOTION_STAGGER_MS,
199
+ opts?: { classes?: Partial<AnimationClasses> },
200
+ ): Promise<void> {
201
+ if (!shouldAnimate()) return;
202
+ const cls = resolveClasses(opts?.classes);
203
+ await new Promise<void>(r => requestAnimationFrame(() => r()));
204
+ for (let i = 0; i < uids.length; i++) {
205
+ const uid = uids[i];
206
+ if (!uid) continue;
207
+ const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
208
+ if (!el) continue;
209
+ const delay = i * staggerMs;
210
+ setTimeout(() => {
211
+ el.classList.add(cls.flash);
212
+ setTimeout(() => el.classList.remove(cls.flash), FLASH_DURATION_MS);
213
+ }, delay);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Animate an element (matched by uid) with --leaving class, wait for the
219
+ * fade to complete, then run mutate to remove from state. Mutate is
220
+ * called immediately when reduce-motion is on (no class added).
221
+ */
222
+ export async function withLeaveAnimation(
223
+ uid: string,
224
+ container: HTMLElement,
225
+ mutate: () => void,
226
+ opts?: { classes?: Partial<AnimationClasses> },
227
+ ): Promise<void> {
228
+ if (!shouldAnimate()) {
229
+ mutate();
230
+ return;
231
+ }
232
+ const cls = resolveClasses(opts?.classes);
233
+ const el = container.querySelector<HTMLElement>(`[data-uid="${uid}"]`);
234
+ if (el) el.classList.add(cls.leaving);
235
+ await new Promise<void>(r => setTimeout(r, MOTION_FADE_MS));
236
+ mutate();
237
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Lets a floating piece (a Select's list, a menu, a nested Popover) that is
3
+ * opened from inside a Popover count as part of it. Those pieces teleport to
4
+ * <body>, outside the Popover's panel, so without this a click on one is a
5
+ * click outside the Popover (which closes it) and the Popover's Escape
6
+ * handler, listening first, closes it before the inner list can.
7
+ *
8
+ * A Popover calls `provideFloatingLayer()`; a floating piece calls
9
+ * `useFloatingChild(() => el)` with its floating element (null while shut).
10
+ */
11
+ import { inject, onBeforeUnmount, provide, type InjectionKey } from "vue";
12
+
13
+ type Getter = () => HTMLElement | null;
14
+
15
+ interface FloatingLayer {
16
+ add(get: Getter): () => void;
17
+ }
18
+
19
+ const FLOATING_LAYER: InjectionKey<FloatingLayer> = Symbol("wp-floating-layer");
20
+
21
+ export interface OwnedFloating {
22
+ /** True when the event path runs through an open child. */
23
+ owns(path: readonly EventTarget[]): boolean;
24
+ /** True while any child has its floating element open. */
25
+ childOpen(): boolean;
26
+ }
27
+
28
+ export function provideFloatingLayer(): OwnedFloating {
29
+ const children = new Set<Getter>();
30
+ provide(FLOATING_LAYER, {
31
+ add(get) {
32
+ children.add(get);
33
+ return () => { children.delete(get); };
34
+ },
35
+ });
36
+ const open = (): HTMLElement[] => [...children].map((g) => g()).filter((el): el is HTMLElement => el !== null);
37
+ return {
38
+ owns: (path) => open().some((el) => path.includes(el)),
39
+ childOpen: () => open().length > 0,
40
+ };
41
+ }
42
+
43
+ export function useFloatingChild(get: Getter): void {
44
+ const layer = inject(FLOATING_LAYER, null);
45
+ if (!layer) return;
46
+ const remove = layer.add(get);
47
+ onBeforeUnmount(remove);
48
+ }