@dumiflex/wp-kit 0.5.0 → 0.6.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 +32 -0
- package/README.md +5 -0
- package/package.json +4 -3
- package/src/Avatar.vue +1 -1
- package/src/Banner.vue +4 -4
- package/src/Button.vue +73 -44
- package/src/Checkbox.vue +201 -63
- package/src/Chip.vue +5 -2
- package/src/CodeBlock.vue +1 -1
- package/src/ColorField.vue +60 -6
- package/src/Combobox.vue +10 -14
- package/src/ConfirmModal.vue +12 -22
- package/src/ContextMenu.vue +11 -30
- package/src/CopyButton.vue +1 -1
- package/src/DataTable.vue +0 -2
- package/src/DatePicker.vue +1 -1
- package/src/Disclosure.vue +2 -1
- package/src/Drawer.vue +2 -2
- package/src/DropZone.vue +2 -2
- package/src/HsvPicker.vue +2 -2
- package/src/IconPicker.vue +1 -1
- package/src/ImageGrid.vue +2 -2
- package/src/Lightbox.vue +2 -2
- package/src/Modal.vue +109 -44
- package/src/ModalShell.vue +26 -5
- package/src/Pagination.vue +4 -4
- package/src/Popover.vue +68 -12
- package/src/RadioGroup.vue +5 -4
- package/src/SearchInput.vue +1 -1
- package/src/Select.vue +53 -158
- package/src/Skeleton.vue +1 -1
- package/src/Slider.vue +166 -55
- package/src/SortableList.vue +1 -1
- package/src/TagInput.vue +1 -1
- package/src/Timeline.vue +2 -2
- package/src/WorkingLine.vue +320 -0
- package/src/WpIconButton.vue +3 -2
- package/src/WpImageZoom.vue +7 -7
- package/src/WpSegmented.vue +3 -1
- package/src/WpSwitch.vue +1 -1
- package/src/WpTabs.vue +1 -1
- package/src/_modal-motion.css +1 -1
- package/src/button.css +8 -6
- package/src/floating.ts +5 -0
- package/src/hsv.ts +1 -1
- package/src/menu.css +51 -0
- package/src/motion.css +73 -0
- package/src/slider-math.ts +45 -1
- package/src/theme/looks.css +6 -19
- package/src/theme/tokens.css +12 -1108
- package/src/tokens.ts +8 -3
- package/src/tooltip.css +8 -5
- package/src/useDialogFocus.ts +11 -2
- package/src/CloseButton.vue +0 -69
- package/src/ColorPicker.vue +0 -201
- package/src/ConfirmDialog.vue +0 -165
- package/src/RangeSlider.vue +0 -183
- package/src/Toggle.vue +0 -36
- package/src/WpButton.vue +0 -75
- package/src/WpCheck.vue +0 -104
- package/src/_skeleton.css +0 -50
package/src/Select.vue
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, nextTick,
|
|
2
|
+
import { computed, nextTick, ref, useId, watch } from "vue";
|
|
3
3
|
import Icon, { ICON_SM } from "./Icon.vue";
|
|
4
|
+
import Popover from "./Popover.vue";
|
|
4
5
|
|
|
5
6
|
// Declared in `./select-types` so plain `.ts` helpers can build option lists
|
|
6
7
|
// (a `.ts` importer only sees the `*.vue` ambient shim). Re-exported here so
|
|
7
8
|
// existing `from "./Select.vue"` imports keep resolving.
|
|
8
9
|
import type { SelectOption } from "./select-types";
|
|
9
|
-
import { useFloatingChild } from "./floating-layer";
|
|
10
10
|
export type { SelectOption };
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
@@ -31,21 +31,16 @@ const emit = defineEmits<{
|
|
|
31
31
|
(e: "change", v: string | number | null): void;
|
|
32
32
|
}>();
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const listId = `wp-select-list-${useId()}`;
|
|
35
35
|
const btnRef = ref<HTMLButtonElement | null>(null);
|
|
36
36
|
const open = ref(false);
|
|
37
|
-
const flip = ref(false);
|
|
38
37
|
const active = ref(0);
|
|
39
38
|
/** Type-to-filter query, built from printable keystrokes while the menu
|
|
40
39
|
* is open. Reset every time the menu opens or closes. */
|
|
41
40
|
const query = ref("");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
useFloatingChild(() => menuRef.value);
|
|
46
|
-
|
|
47
|
-
/** Fixed-position coordinates for the teleported menu. */
|
|
48
|
-
const menuStyle = ref<Record<string, string>>({});
|
|
41
|
+
// The list floats in a kit Popover (variant "menu"): it places, flips, fits
|
|
42
|
+
// and follows the trigger, closes on a press outside or Escape, and counts as
|
|
43
|
+
// part of a Popover the Select sits in. The keys stay on the trigger.
|
|
49
44
|
|
|
50
45
|
const selected = computed(() => props.options.find((o) => o.value === props.modelValue) ?? null);
|
|
51
46
|
|
|
@@ -107,108 +102,18 @@ const composedAriaLabel = computed<string | undefined>(() => {
|
|
|
107
102
|
return props.ariaLabel;
|
|
108
103
|
});
|
|
109
104
|
|
|
110
|
-
function onDocClick(e: MouseEvent) {
|
|
111
|
-
const t = e.target as Node | null;
|
|
112
|
-
if (!t) return;
|
|
113
|
-
// Close when the press is outside the trigger AND outside the menu. The menu
|
|
114
|
-
// is teleported to <body>, so it is NOT inside `wrapRef` — checking only the
|
|
115
|
-
// trigger meant a mousedown on the menu's own scrollbar counted as "outside"
|
|
116
|
-
// and dismissed it. The wheel worked, the scrollbar did not.
|
|
117
|
-
if (wrapRef.value?.contains(t)) return;
|
|
118
|
-
if (menuRef.value?.contains(t)) return;
|
|
119
|
-
open.value = false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
onMounted(() => {
|
|
123
|
-
document.addEventListener("mousedown", onDocClick, true);
|
|
124
|
-
});
|
|
125
|
-
onBeforeUnmount(() => {
|
|
126
|
-
document.removeEventListener("mousedown", onDocClick, true);
|
|
127
|
-
window.removeEventListener("scroll", followOnScroll, true);
|
|
128
|
-
window.removeEventListener("resize", reposition);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
/** Any scroll in the page moves the menu with its trigger (like every kit
|
|
132
|
-
* overlay), instead of closing it or leaving it where the trigger used to
|
|
133
|
-
* be. Only once the trigger has scrolled right out of view does it close. */
|
|
134
|
-
function followOnScroll(e: Event) {
|
|
135
|
-
// The menu's own list scroll changes nothing about where it sits.
|
|
136
|
-
const t = e.target as Element | null;
|
|
137
|
-
if (t && typeof t.closest === "function" && t.closest(".wp-select__menu")) return;
|
|
138
|
-
if (!open.value || !btnRef.value) return;
|
|
139
|
-
const r = btnRef.value.getBoundingClientRect();
|
|
140
|
-
if (r.bottom < 0 || r.top > window.innerHeight || r.right < 0 || r.left > window.innerWidth) {
|
|
141
|
-
open.value = false;
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
computeMenuStyle();
|
|
145
|
-
}
|
|
146
|
-
function reposition() {
|
|
147
|
-
if (!open.value || !btnRef.value) return;
|
|
148
|
-
computeMenuStyle();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function computeMenuStyle() {
|
|
152
|
-
if (!btnRef.value) return;
|
|
153
|
-
const rect = btnRef.value.getBoundingClientRect();
|
|
154
|
-
// Ideal menu height = enough room for every option (~36px each + 12px
|
|
155
|
-
// padding), capped at 240px so a huge library doesn't blanket the
|
|
156
|
-
// viewport. The actual rendered height also gets capped by available
|
|
157
|
-
// screen space below — see `effectiveMaxHeight` — so the menu never
|
|
158
|
-
// bleeds past the viewport edge.
|
|
159
|
-
const idealHeight = Math.min(240, props.options.length * 36 + 12);
|
|
160
|
-
const gap = 4; // spacing between trigger and menu
|
|
161
|
-
const margin = 8; // breathing room from the viewport edge
|
|
162
|
-
const spaceBelow = window.innerHeight - rect.bottom - gap - margin;
|
|
163
|
-
const spaceAbove = rect.top - gap - margin;
|
|
164
|
-
|
|
165
|
-
// Flip above only when there isn't enough room below AND there's
|
|
166
|
-
// more room above than below — otherwise the menu still grows under
|
|
167
|
-
// the trigger and `effectiveMaxHeight` clamps it to whatever fits.
|
|
168
|
-
flip.value = spaceBelow < idealHeight && spaceAbove > spaceBelow;
|
|
169
|
-
|
|
170
|
-
const available = flip.value ? spaceAbove : spaceBelow;
|
|
171
|
-
// Floor at 80px so a tightly-cramped row (e.g. inside a modal near the
|
|
172
|
-
// viewport edge) still surfaces a usable scrolling list rather than
|
|
173
|
-
// collapsing to a 1-row sliver.
|
|
174
|
-
const effectiveMaxHeight = Math.max(80, Math.min(idealHeight, available));
|
|
175
|
-
|
|
176
|
-
menuStyle.value = flip.value
|
|
177
|
-
? {
|
|
178
|
-
position: "fixed",
|
|
179
|
-
bottom: (window.innerHeight - rect.top + gap) + "px",
|
|
180
|
-
top: "auto",
|
|
181
|
-
left: rect.left + "px",
|
|
182
|
-
minWidth: rect.width + "px",
|
|
183
|
-
maxHeight: effectiveMaxHeight + "px",
|
|
184
|
-
}
|
|
185
|
-
: {
|
|
186
|
-
position: "fixed",
|
|
187
|
-
top: (rect.bottom + gap) + "px",
|
|
188
|
-
bottom: "auto",
|
|
189
|
-
left: rect.left + "px",
|
|
190
|
-
minWidth: rect.width + "px",
|
|
191
|
-
maxHeight: effectiveMaxHeight + "px",
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
105
|
watch(open, (v) => {
|
|
196
|
-
if (!v) {
|
|
197
|
-
query.value = "";
|
|
198
|
-
window.removeEventListener("scroll", followOnScroll, true);
|
|
199
|
-
window.removeEventListener("resize", reposition);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (!btnRef.value) return;
|
|
203
106
|
query.value = "";
|
|
204
|
-
|
|
107
|
+
if (!v) return;
|
|
205
108
|
// Highlight currently-selected option (or first).
|
|
206
109
|
const idx = props.options.findIndex((o) => o.value === props.modelValue);
|
|
207
110
|
active.value = idx >= 0 ? idx : 0;
|
|
208
|
-
window.addEventListener("scroll", followOnScroll, true);
|
|
209
|
-
window.addEventListener("resize", reposition);
|
|
210
111
|
});
|
|
211
112
|
|
|
113
|
+
function onOpen(v: boolean) {
|
|
114
|
+
open.value = v;
|
|
115
|
+
}
|
|
116
|
+
|
|
212
117
|
function toggle() {
|
|
213
118
|
if (props.disabled) return;
|
|
214
119
|
open.value = !open.value;
|
|
@@ -246,6 +151,9 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
246
151
|
} else if (e.key === "ArrowUp") {
|
|
247
152
|
e.preventDefault();
|
|
248
153
|
if (list.length) active.value = (active.value - 1 + list.length) % list.length;
|
|
154
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
if (list.length) active.value = e.key === "Home" ? 0 : list.length - 1;
|
|
249
157
|
} else if (e.key === "Enter") {
|
|
250
158
|
e.preventDefault();
|
|
251
159
|
const opt = list[active.value];
|
|
@@ -264,7 +172,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
264
172
|
</script>
|
|
265
173
|
|
|
266
174
|
<template>
|
|
267
|
-
<div
|
|
175
|
+
<div class="wp-select-wrap">
|
|
268
176
|
<button
|
|
269
177
|
ref="btnRef"
|
|
270
178
|
type="button"
|
|
@@ -273,6 +181,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
273
181
|
:aria-label="composedAriaLabel"
|
|
274
182
|
:aria-expanded="open"
|
|
275
183
|
:aria-haspopup="'listbox'"
|
|
184
|
+
:aria-controls="open ? listId : undefined"
|
|
276
185
|
:aria-invalid="error || undefined"
|
|
277
186
|
data-test="select-trigger"
|
|
278
187
|
@click="toggle"
|
|
@@ -332,15 +241,27 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
332
241
|
<Icon class="wp-select__chevron" name="chevron-down" />
|
|
333
242
|
</button>
|
|
334
243
|
|
|
335
|
-
<!--
|
|
336
|
-
|
|
337
|
-
|
|
244
|
+
<!-- The list floats on <body> in a menu Popover, so it escapes any
|
|
245
|
+
overflow:hidden ancestor and clears every modal. -->
|
|
246
|
+
<!-- Popover's autofocus is turned OFF: focus stays on the trigger, which has the keys. -->
|
|
247
|
+
<!-- eslint-disable vuejs-accessibility/no-autofocus -->
|
|
248
|
+
<Popover
|
|
249
|
+
:open="open && !disabled"
|
|
250
|
+
:anchor="btnRef"
|
|
251
|
+
variant="menu"
|
|
252
|
+
role="group"
|
|
253
|
+
match-width
|
|
254
|
+
:max-height="240"
|
|
255
|
+
:autofocus="false"
|
|
256
|
+
:label="ariaLabel ?? placeholder"
|
|
257
|
+
@update:open="onOpen"
|
|
258
|
+
>
|
|
259
|
+
<!-- eslint-enable vuejs-accessibility/no-autofocus -->
|
|
338
260
|
<ul
|
|
339
|
-
|
|
340
|
-
ref="menuRef"
|
|
261
|
+
:id="listId"
|
|
341
262
|
class="wp-select__menu"
|
|
342
263
|
role="listbox"
|
|
343
|
-
:
|
|
264
|
+
:aria-label="ariaLabel ?? placeholder"
|
|
344
265
|
data-test="select-menu"
|
|
345
266
|
>
|
|
346
267
|
<li
|
|
@@ -364,7 +285,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
364
285
|
<li
|
|
365
286
|
v-for="(opt, i) in filtered"
|
|
366
287
|
:key="String(opt.value)"
|
|
367
|
-
class="wp-select__option"
|
|
288
|
+
class="wp-select__option wp-menu__item"
|
|
368
289
|
role="option"
|
|
369
290
|
tabindex="-1"
|
|
370
291
|
:aria-selected="opt.value === modelValue"
|
|
@@ -407,7 +328,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
407
328
|
style="padding:6px 12px;opacity:0.55;"
|
|
408
329
|
>No matches</li>
|
|
409
330
|
</ul>
|
|
410
|
-
</
|
|
331
|
+
</Popover>
|
|
411
332
|
</div>
|
|
412
333
|
</template>
|
|
413
334
|
|
|
@@ -415,14 +336,11 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
415
336
|
Self-contained styles so <Select> works everywhere it's used — including the
|
|
416
337
|
ComfyUI CANVAS widget bundle, which does NOT load the manager-global
|
|
417
338
|
tokens.css (that's why an unstyled Select rendered behind the modal there).
|
|
418
|
-
Class names stay wp-select* (global-namespaced, no collision)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
back to literals for the few the
|
|
424
|
-
canvas theme doesn't define (--wp-input-h[-sm], --wp-focus-ring). In the SPA
|
|
425
|
-
these duplicate tokens.css with identical rules — harmless.
|
|
339
|
+
Class names stay wp-select* (global-namespaced, no collision). The list's
|
|
340
|
+
look is the shared menu sheet (menu.css, scoped, at the end), whose layer
|
|
341
|
+
(10020) clears every modal overlay (those run up to 10010). Vars fall back
|
|
342
|
+
to literals for the few the canvas theme doesn't define (--wp-input-h[-sm],
|
|
343
|
+
--wp-focus-ring).
|
|
426
344
|
-->
|
|
427
345
|
<style>
|
|
428
346
|
.wp-select-wrap { position: relative; display: inline-block; width: 100%; }
|
|
@@ -465,7 +383,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
465
383
|
display: flex; align-items: center; gap: 6px;
|
|
466
384
|
flex: 1; min-width: 0; overflow: hidden;
|
|
467
385
|
}
|
|
468
|
-
.wp-select__dot { width: 8px; height: 8px; border-radius: 50
|
|
386
|
+
.wp-select__dot { width: 8px; height: 8px; border-radius: min(50%, var(--wp-radius-cap, 999px)); flex-shrink: 0; display: inline-block; }
|
|
469
387
|
/* Empty stand-in that holds the icon/dot column open for options without one.
|
|
470
388
|
* Width is set inline from `markerWidth`, since it depends on what the list
|
|
471
389
|
* actually contains. */
|
|
@@ -510,7 +428,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
510
428
|
font-size: 10px;
|
|
511
429
|
color: var(--wp-text-dim);
|
|
512
430
|
}
|
|
513
|
-
.wp-select__chevron { color: var(--wp-text-dim); font-size:
|
|
431
|
+
.wp-select__chevron { color: var(--wp-text-dim); font-size: 12px; margin-left: 6px; flex-shrink: 0; }
|
|
514
432
|
/* Clear ✕: a plain glyph kept well clear of the chevron (a 1px rule and
|
|
515
433
|
* 8px either side) so it is not mis-clicked, and shown only while the
|
|
516
434
|
* field is hovered or focused. The ::after gives it a 20px click target. */
|
|
@@ -535,44 +453,20 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
535
453
|
@media (hover: none) { .wp-select__clear { opacity: 1; } }
|
|
536
454
|
.wp-spacer { flex: 1 1 auto; }
|
|
537
455
|
|
|
538
|
-
/*
|
|
539
|
-
*
|
|
456
|
+
/* The list. Its panel (background, border, corners, padding, shadow, layer)
|
|
457
|
+
* and its rows' padding, corners, text and states are the shared menu look:
|
|
458
|
+
* Popover's `variant="menu"` and `menu.css` below. */
|
|
540
459
|
.wp-select__menu {
|
|
541
460
|
/* One line box for an option label, in px so a leading dot/icon can be
|
|
542
461
|
* centred against it without the em-resolves-against-its-own-font-size trap
|
|
543
|
-
* (the icon runs at 12px
|
|
544
|
-
*
|
|
545
|
-
|
|
546
|
-
--wp-select-line-h: 18.75px;
|
|
547
|
-
background: var(--wp-bg-3);
|
|
548
|
-
border: 1px solid var(--wp-border-strong);
|
|
549
|
-
border-radius: min(7px, var(--wp-radius-cap, 999px));
|
|
550
|
-
box-shadow: var(--wp-shadow-md);
|
|
551
|
-
z-index: 10020;
|
|
552
|
-
padding: 4px;
|
|
553
|
-
max-height: 240px;
|
|
554
|
-
overflow: auto;
|
|
462
|
+
* (the icon runs at 12px and so does the label; `1.5em` on the icon would
|
|
463
|
+
* still measure against a different box). */
|
|
464
|
+
--wp-select-line-h: 18px;
|
|
555
465
|
list-style: none;
|
|
556
466
|
margin: 0;
|
|
467
|
+
padding: 0;
|
|
557
468
|
}
|
|
558
|
-
.wp-select__option {
|
|
559
|
-
display: flex; align-items: center; gap: 6px;
|
|
560
|
-
padding: 6px 8px;
|
|
561
|
-
border-radius: min(6px, var(--wp-radius-cap, 999px));
|
|
562
|
-
color: var(--wp-text-muted);
|
|
563
|
-
cursor: pointer;
|
|
564
|
-
font-size: 12.5px;
|
|
565
|
-
user-select: none;
|
|
566
|
-
}
|
|
567
|
-
.wp-select__option:hover { background: var(--wp-bg-4); color: var(--wp-text); }
|
|
568
|
-
.wp-select__option[data-active="true"] {
|
|
569
|
-
background: color-mix(in oklab, var(--wp-accent-500) 20%, transparent);
|
|
570
|
-
color: var(--wp-text);
|
|
571
|
-
}
|
|
572
|
-
.wp-select__option[data-selected="true"] {
|
|
573
|
-
background: color-mix(in oklab, var(--wp-accent-500) 18%, transparent);
|
|
574
|
-
color: var(--wp-text);
|
|
575
|
-
}
|
|
469
|
+
.wp-select__option { display: flex; gap: 6px; }
|
|
576
470
|
/* Option rows WRAP (to two lines) rather than ellipsis-clipping at the first.
|
|
577
471
|
* The menu is up to 620px wide and these values run long — a constraint
|
|
578
472
|
* exception source is a whole phrase plus a nested `@ref` — so clipping them
|
|
@@ -640,5 +534,6 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
640
534
|
white-space: normal !important;
|
|
641
535
|
overflow-wrap: anywhere;
|
|
642
536
|
}
|
|
643
|
-
.wp-select__menu { max-width: min(620px, 92vw); }
|
|
644
537
|
</style>
|
|
538
|
+
|
|
539
|
+
<style scoped src="./menu.css"></style>
|
package/src/Skeleton.vue
CHANGED
|
@@ -80,7 +80,7 @@ const lineWidth = (i: number): string | undefined =>
|
|
|
80
80
|
.wp-skeleton__line { width: 100%; height: 0.75em; }
|
|
81
81
|
.wp-skeleton__line--hint { height: 0.6em; opacity: 0.7; }
|
|
82
82
|
.wp-skeleton__block { width: 100%; height: 64px; border-radius: var(--wp-radius); }
|
|
83
|
-
.wp-skeleton__circle { flex: none; width: 28px; height: 28px; border-radius: 50
|
|
83
|
+
.wp-skeleton__circle { flex: none; width: 28px; height: 28px; border-radius: min(50%, var(--wp-radius-cap, 999px)); }
|
|
84
84
|
.wp-skeleton__tile { flex: none; width: 28px; height: 28px; border-radius: var(--wp-radius-sm); }
|
|
85
85
|
.wp-skeleton__chips { display: flex; flex-wrap: wrap; gap: var(--wp-space-2); }
|
|
86
86
|
/* A Chip's height (xs, 19px); pass height for a sm (24px) filter row. */
|