@dumiflex/wp-kit 0.3.0 → 0.4.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/CHANGELOG.md +77 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +119 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +292 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +125 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +173 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +175 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +82 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- 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
|
+
}
|