@dumiflex/wp-kit 0.7.1 → 0.9.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 +24 -0
- package/package.json +1 -1
- package/src/Chip.vue +55 -6
- package/src/DataTable.vue +16 -5
- package/src/ImageGrid.vue +32 -5
- package/src/Lightbox.vue +6 -2
- package/src/RadioGroup.vue +26 -6
- package/src/WpSwitch.vue +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0](https://github.com/DumiFlex/wp-kit/compare/v0.8.0...v0.9.0) (2026-11-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* named colour swatches with a swatch slot, and a natural-shape ImageGrid ([4184232](https://github.com/DumiFlex/wp-kit/commit/4184232477b87557298c10c1b7d9310664f6c0a2))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* draw the switch track in one piece at canvas zooms ([#84](https://github.com/DumiFlex/wp-kit/issues/84)) ([9d38d0d](https://github.com/DumiFlex/wp-kit/commit/9d38d0d71dc2a855f72de8925cd774eb05b35d10))
|
|
14
|
+
|
|
15
|
+
## [0.8.0](https://github.com/DumiFlex/wp-kit/compare/v0.7.1...v0.8.0) (2026-11-03)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* Chip stat tiles and DataTable rows that unfold ([8c1cade](https://github.com/DumiFlex/wp-kit/commit/8c1cade94cb5bd1900ca8fca948a4b18fa6ba4f5))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* Lightbox closes on a click anywhere dark, the bar and around the picture included ([#79](https://github.com/DumiFlex/wp-kit/issues/79)) ([a6eb440](https://github.com/DumiFlex/wp-kit/commit/a6eb44053641342a96f795b2c75af82d5417c334))
|
|
26
|
+
|
|
3
27
|
## [0.7.1](https://github.com/DumiFlex/wp-kit/compare/v0.7.0...v0.7.1) (2026-11-03)
|
|
4
28
|
|
|
5
29
|
|
package/package.json
CHANGED
package/src/Chip.vue
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* filters reads at a glance. `count` puts a small number after the label.
|
|
19
19
|
* `size`: `xs` (default, a label in a line of text), `sm` (24px, a filter
|
|
20
20
|
* row) or `md` (32px, next to a md control).
|
|
21
|
+
*
|
|
22
|
+
* `variant="stat"` is the same chip as a tile: the `count` big on top, the
|
|
23
|
+
* label under it and a top edge in `color` (a summary row whose tiles also
|
|
24
|
+
* filter the list below):
|
|
25
|
+
*
|
|
26
|
+
* <Chip variant="stat" :color="roleColour(r)" :count="n" :pressed="on.has(r)" @click="flip(r)">{{ label }}</Chip>
|
|
21
27
|
*/
|
|
22
28
|
import { computed, type Component } from "vue";
|
|
23
29
|
import Icon from "./Icon.vue";
|
|
@@ -45,9 +51,11 @@ interface Props {
|
|
|
45
51
|
/** A status dot before the label, in the chip's colour. */
|
|
46
52
|
dot?: boolean | undefined;
|
|
47
53
|
size?: "xs" | "sm" | "md" | undefined;
|
|
54
|
+
/** `pill` (default) or `stat`: a tile with the count big on top. */
|
|
55
|
+
variant?: "pill" | "stat" | undefined;
|
|
48
56
|
disabled?: boolean | undefined;
|
|
49
57
|
}
|
|
50
|
-
const props = withDefaults(defineProps<Props>(), { tone: "default", as: "span", size: "xs", pressed: undefined });
|
|
58
|
+
const props = withDefaults(defineProps<Props>(), { tone: "default", as: "span", size: "xs", variant: "pill", pressed: undefined });
|
|
51
59
|
|
|
52
60
|
const emit = defineEmits<{
|
|
53
61
|
(e: "remove"): void;
|
|
@@ -56,19 +64,21 @@ const emit = defineEmits<{
|
|
|
56
64
|
const toggle = computed(() => props.pressed !== undefined);
|
|
57
65
|
const tag = computed(() => (toggle.value ? "button" : props.as));
|
|
58
66
|
const pressable = computed(() => tag.value !== "span");
|
|
67
|
+
const stat = computed(() => props.variant === "stat");
|
|
59
68
|
const classes = computed(() => [
|
|
60
69
|
"wp-chip",
|
|
61
70
|
`wp-chip--${props.size}`,
|
|
71
|
+
stat.value && "wp-chip--stat",
|
|
62
72
|
pressable.value && "wp-chip--action",
|
|
63
73
|
toggle.value && "wp-chip--toggle",
|
|
64
74
|
toggle.value && props.color && "wp-chip--tinted",
|
|
65
|
-
!toggle.value && (props.color ? "wp-chip--color" : props.tone !== "default" && `wp-chip--${props.tone}`),
|
|
75
|
+
!toggle.value && !stat.value && (props.color ? "wp-chip--color" : props.tone !== "default" && `wp-chip--${props.tone}`),
|
|
66
76
|
]);
|
|
67
77
|
|
|
68
78
|
const colorStyle = computed(() => {
|
|
69
79
|
if (!props.color) return undefined;
|
|
70
80
|
const c = props.color;
|
|
71
|
-
if (toggle.value) return { "--wp-chip-c": c };
|
|
81
|
+
if (toggle.value || stat.value) return { "--wp-chip-c": c };
|
|
72
82
|
return {
|
|
73
83
|
color: `color-mix(in oklab, ${c} 65%, var(--wp-text))`,
|
|
74
84
|
background: `color-mix(in oklab, ${c} 18%, transparent)`,
|
|
@@ -94,9 +104,15 @@ function onRemove(e: MouseEvent) {
|
|
|
94
104
|
<span v-if="dot" class="wp-chip__dot" aria-hidden="true" />
|
|
95
105
|
<!-- 10px sub-ICON_SM: the chip font is 10.5px and a 12px icon read
|
|
96
106
|
oversized next to it. Larger chips get a larger icon. -->
|
|
97
|
-
<Icon v-if="icon" class="wp-chip__icon" :name="icon" :size="size === 'xs' ? 10 : 12" />
|
|
98
|
-
<
|
|
99
|
-
|
|
107
|
+
<Icon v-if="icon && !stat" class="wp-chip__icon" :name="icon" :size="size === 'xs' ? 10 : 12" />
|
|
108
|
+
<template v-if="stat">
|
|
109
|
+
<span v-if="count !== undefined" class="wp-chip__stat-n">{{ count }}</span>
|
|
110
|
+
<span class="wp-chip__stat-label"><slot /></span>
|
|
111
|
+
</template>
|
|
112
|
+
<template v-else>
|
|
113
|
+
<slot />
|
|
114
|
+
<span v-if="count !== undefined" class="wp-chip__count">{{ count }}</span>
|
|
115
|
+
</template>
|
|
100
116
|
<button
|
|
101
117
|
v-if="removable && !pressable"
|
|
102
118
|
type="button"
|
|
@@ -163,4 +179,37 @@ function onRemove(e: MouseEvent) {
|
|
|
163
179
|
background: color-mix(in oklab, var(--wp-chip-c) 22%, transparent);
|
|
164
180
|
color: inherit;
|
|
165
181
|
}
|
|
182
|
+
|
|
183
|
+
/* A stat tile: the count big on top, the label under it, a top edge in its
|
|
184
|
+
colour. Pressed, it fills with a little of that colour. */
|
|
185
|
+
.wp-chip--stat {
|
|
186
|
+
--wp-chip-c: var(--wp-border-strong);
|
|
187
|
+
box-sizing: border-box;
|
|
188
|
+
height: auto;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
align-items: flex-start;
|
|
191
|
+
gap: var(--wp-space-1);
|
|
192
|
+
padding: var(--wp-space-4) var(--wp-space-5);
|
|
193
|
+
border: 1px solid var(--wp-border);
|
|
194
|
+
border-top: 3px solid var(--wp-chip-c);
|
|
195
|
+
border-radius: min(8px, var(--wp-radius-cap, 999px));
|
|
196
|
+
background: var(--wp-bg-2);
|
|
197
|
+
color: var(--wp-text);
|
|
198
|
+
font-size: var(--wp-text-xs);
|
|
199
|
+
line-height: 1.3;
|
|
200
|
+
text-align: left;
|
|
201
|
+
white-space: normal;
|
|
202
|
+
}
|
|
203
|
+
.wp-chip--stat.wp-chip--toggle[aria-pressed="true"] {
|
|
204
|
+
border-color: var(--wp-chip-c);
|
|
205
|
+
background: color-mix(in oklab, var(--wp-chip-c) 12%, var(--wp-bg-2));
|
|
206
|
+
color: var(--wp-text);
|
|
207
|
+
}
|
|
208
|
+
.wp-chip__stat-n {
|
|
209
|
+
font-size: var(--wp-text-xl);
|
|
210
|
+
font-weight: var(--wp-weight-semibold);
|
|
211
|
+
font-variant-numeric: tabular-nums;
|
|
212
|
+
line-height: 1.2;
|
|
213
|
+
}
|
|
214
|
+
.wp-chip__stat-label { color: var(--wp-text-muted); }
|
|
166
215
|
</style>
|
package/src/DataTable.vue
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
*
|
|
21
21
|
* `row-clickable` makes each row open something (a trace, a detail panel):
|
|
22
22
|
* rows take focus, and a click or Enter emits `row-click`. `active-key`
|
|
23
|
-
* marks the row that is open.
|
|
23
|
+
* marks the row that is open. With a `#detail` slot the open row unfolds:
|
|
24
|
+
* the slot is drawn full width under it and each row says `aria-expanded`
|
|
25
|
+
* (toggle `active-key` in `row-click` to open and close it).
|
|
24
26
|
*/
|
|
25
|
-
import { computed } from "vue";
|
|
27
|
+
import { computed, useSlots } from "vue";
|
|
26
28
|
import Checkbox from "./Checkbox.vue";
|
|
27
29
|
import Skeleton from "./Skeleton.vue";
|
|
28
30
|
|
|
@@ -146,6 +148,10 @@ function toggleRow(row: T, on: boolean): void {
|
|
|
146
148
|
emit("update:selected", on ? [...props.selected.filter((x) => x !== k), k] : props.selected.filter((x) => x !== k));
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
const slots = useSlots();
|
|
152
|
+
const hasDetail = computed(() => Boolean(slots.detail));
|
|
153
|
+
const isActive = (row: T): boolean => props.activeKey !== null && keyOf(row) === props.activeKey;
|
|
154
|
+
|
|
149
155
|
const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
|
|
150
156
|
</script>
|
|
151
157
|
|
|
@@ -212,13 +218,13 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
|
|
|
212
218
|
</td>
|
|
213
219
|
</tr>
|
|
214
220
|
<template v-else>
|
|
221
|
+
<template v-for="(row, index) in shown" :key="keyOf(row)">
|
|
215
222
|
<tr
|
|
216
|
-
v-for="(row, index) in shown"
|
|
217
|
-
:key="keyOf(row)"
|
|
218
223
|
v-bind="rowAttrs ? rowAttrs(row) : {}"
|
|
219
224
|
:aria-selected="selectable ? picked.has(keyOf(row)) : undefined"
|
|
220
225
|
:data-selected="selectable && picked.has(keyOf(row)) ? 'true' : undefined"
|
|
221
|
-
:data-active="
|
|
226
|
+
:data-active="isActive(row) ? 'true' : undefined"
|
|
227
|
+
:aria-expanded="rowClickable && hasDetail ? isActive(row) : undefined"
|
|
222
228
|
:data-clickable="rowClickable || undefined"
|
|
223
229
|
:tabindex="rowClickable ? 0 : undefined"
|
|
224
230
|
@click="rowClickable && emit('row-click', row)"
|
|
@@ -240,6 +246,10 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
|
|
|
240
246
|
<slot :name="`cell-${c.key}`" :row="row" :value="cell(row, c.key)" :index="index">{{ cell(row, c.key) ?? "" }}</slot>
|
|
241
247
|
</td>
|
|
242
248
|
</tr>
|
|
249
|
+
<tr v-if="hasDetail && isActive(row)" class="wp-data-table__detail">
|
|
250
|
+
<td :colspan="span"><slot name="detail" :row="row" :index="index" /></td>
|
|
251
|
+
</tr>
|
|
252
|
+
</template>
|
|
243
253
|
</template>
|
|
244
254
|
</tbody>
|
|
245
255
|
</table>
|
|
@@ -248,6 +258,7 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
|
|
|
248
258
|
|
|
249
259
|
<style scoped>
|
|
250
260
|
.wp-data-table { overflow: auto; }
|
|
261
|
+
.wp-data-table__detail > td { background: var(--wp-bg-2); }
|
|
251
262
|
.wp-data-table th[data-align="right"], .wp-data-table td[data-align="right"] { text-align: right; }
|
|
252
263
|
.wp-data-table th[data-align="center"], .wp-data-table td[data-align="center"] { text-align: center; }
|
|
253
264
|
.wp-data-table th[data-align="right"] .wp-data-table__sort { flex-direction: row-reverse; }
|
package/src/ImageGrid.vue
CHANGED
|
@@ -11,9 +11,15 @@
|
|
|
11
11
|
* Tiles are buttons in reading order (one Tab stop each). A tile with no
|
|
12
12
|
* `src` yet (a frame still rendering) draws a Skeleton and can't be opened;
|
|
13
13
|
* the Lightbox steps only through the images that are there.
|
|
14
|
+
*
|
|
15
|
+
* `aspect="auto"` keeps each picture's own shape, `columns` fixes the column
|
|
16
|
+
* count and `hint` puts a magnifier on a tile under the pointer, so one
|
|
17
|
+
* screenshot that opens full size is `<ImageGrid :images="[shot]" aspect="auto"
|
|
18
|
+
* :columns="1" hint />`. A picture that fails to load emits `error`.
|
|
14
19
|
*/
|
|
15
20
|
import { computed, ref } from "vue";
|
|
16
21
|
import Lightbox from "./Lightbox.vue";
|
|
22
|
+
import Icon from "./Icon.vue";
|
|
17
23
|
import Skeleton from "./Skeleton.vue";
|
|
18
24
|
import Checkbox from "./Checkbox.vue";
|
|
19
25
|
|
|
@@ -23,8 +29,12 @@ const props = withDefaults(defineProps<{
|
|
|
23
29
|
images: GridImage[];
|
|
24
30
|
/** Smallest tile width in px. */
|
|
25
31
|
minSize?: number | undefined;
|
|
26
|
-
/** Width / height of a tile. */
|
|
27
|
-
aspect?: number | undefined;
|
|
32
|
+
/** Width / height of a tile; `"auto"` keeps each picture's own shape (no crop). */
|
|
33
|
+
aspect?: number | "auto" | undefined;
|
|
34
|
+
/** A fixed number of columns instead of as many as fit `minSize` (1 = one picture full width). */
|
|
35
|
+
columns?: number | undefined;
|
|
36
|
+
/** A magnifier in the corner of each tile on hover and focus. */
|
|
37
|
+
hint?: boolean | undefined;
|
|
28
38
|
gap?: "sm" | "md" | undefined;
|
|
29
39
|
selectable?: boolean | undefined;
|
|
30
40
|
selected?: string[] | undefined;
|
|
@@ -36,6 +46,8 @@ const props = withDefaults(defineProps<{
|
|
|
36
46
|
const emit = defineEmits<{
|
|
37
47
|
(e: "update:selected", v: string[]): void;
|
|
38
48
|
(e: "open", index: number): void;
|
|
49
|
+
/** A picture failed to load. */
|
|
50
|
+
(e: "error", index: number): void;
|
|
39
51
|
}>();
|
|
40
52
|
|
|
41
53
|
const open = ref(false);
|
|
@@ -63,7 +75,12 @@ function toggle(id: string, on: boolean): void {
|
|
|
63
75
|
class="wp-image-grid__list"
|
|
64
76
|
:data-gap="gap"
|
|
65
77
|
:aria-label="ariaLabel"
|
|
66
|
-
:
|
|
78
|
+
:data-aspect="aspect === 'auto' ? 'auto' : undefined"
|
|
79
|
+
:style="{
|
|
80
|
+
'--_min': `${minSize}px`,
|
|
81
|
+
'--_aspect': String(aspect),
|
|
82
|
+
...(columns ? { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` } : {}),
|
|
83
|
+
}"
|
|
67
84
|
>
|
|
68
85
|
<li
|
|
69
86
|
v-for="(img, i) in images"
|
|
@@ -79,7 +96,8 @@ function toggle(id: string, on: boolean): void {
|
|
|
79
96
|
:data-loading="!img.src || undefined"
|
|
80
97
|
@click="show(i)"
|
|
81
98
|
>
|
|
82
|
-
<img v-if="img.thumb || img.src" class="wp-image-grid__img" :src="img.thumb || img.src" alt="" loading="lazy">
|
|
99
|
+
<img v-if="img.thumb || img.src" class="wp-image-grid__img" :src="img.thumb || img.src" alt="" loading="lazy" @error="emit('error', i)">
|
|
100
|
+
<span v-if="hint && img.src" class="wp-image-grid__hint" aria-hidden="true"><Icon name="search-plus" :size="12" /></span>
|
|
83
101
|
<Skeleton v-else class="wp-image-grid__empty" shape="block" width="100%" height="100%" />
|
|
84
102
|
</button>
|
|
85
103
|
<Checkbox
|
|
@@ -114,6 +132,15 @@ function toggle(id: string, on: boolean): void {
|
|
|
114
132
|
.wp-image-grid__open:focus-visible { outline: none; border-color: var(--wp-border-focus); box-shadow: var(--wp-focus-ring); }
|
|
115
133
|
.wp-image-grid__tile[data-selected] .wp-image-grid__open { border-color: var(--wp-accent); box-shadow: 0 0 0 2px var(--wp-accent-glow); }
|
|
116
134
|
.wp-image-grid__img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
135
|
+
.wp-image-grid__list[data-aspect="auto"] .wp-image-grid__open { position: relative; aspect-ratio: auto; }
|
|
136
|
+
.wp-image-grid__list[data-aspect="auto"] .wp-image-grid__img { height: auto; }
|
|
137
|
+
.wp-image-grid__open { position: relative; }
|
|
138
|
+
.wp-image-grid__hint {
|
|
139
|
+
position: absolute; top: 8px; right: 8px; display: grid; place-items: center; width: 24px; height: 24px;
|
|
140
|
+
border-radius: min(6px, var(--wp-radius-cap, 999px)); background: var(--wp-scrim); color: var(--wp-on-media);
|
|
141
|
+
opacity: 0; transition: opacity var(--wp-motion-hover);
|
|
142
|
+
}
|
|
143
|
+
.wp-image-grid__open:hover .wp-image-grid__hint, .wp-image-grid__open:focus-visible .wp-image-grid__hint { opacity: 1; }
|
|
117
144
|
.wp-image-grid__open[data-loading] { cursor: progress; }
|
|
118
145
|
.wp-image-grid__empty { width: 100%; height: 100%; }
|
|
119
146
|
.wp-image-grid__check { position: absolute; top: 6px; left: 6px; }
|
|
@@ -122,6 +149,6 @@ function toggle(id: string, on: boolean): void {
|
|
|
122
149
|
text-overflow: ellipsis; white-space: nowrap;
|
|
123
150
|
}
|
|
124
151
|
@media (prefers-reduced-motion: reduce) {
|
|
125
|
-
.wp-image-grid__open { transition: none; }
|
|
152
|
+
.wp-image-grid__open, .wp-image-grid__hint { transition: none; }
|
|
126
153
|
}
|
|
127
154
|
</style>
|
package/src/Lightbox.vue
CHANGED
|
@@ -102,11 +102,15 @@ watch(() => props.open, async (v, was) => {
|
|
|
102
102
|
:aria-label="image ? `${image.alt}, ${at + 1} of ${count}` : 'Image viewer'"
|
|
103
103
|
@click.self="close"
|
|
104
104
|
>
|
|
105
|
-
|
|
105
|
+
<!-- The bar spans the width: its empty part is dark area too, so it closes. -->
|
|
106
|
+
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
|
|
107
|
+
<div class="wp-lightbox__bar" @click.self="close">
|
|
106
108
|
<span v-if="count > 1" class="wp-lightbox__count" aria-hidden="true">{{ at + 1 }} / {{ count }}</span>
|
|
107
109
|
<Button variant="ghost" icon="times" aria-label="Close" class="wp-lightbox__close" kbd="Esc" @click="close" />
|
|
108
110
|
</div>
|
|
109
|
-
|
|
111
|
+
<!-- Space around a small picture is dark area too; the picture and its zoom keep their clicks. -->
|
|
112
|
+
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
|
|
113
|
+
<figure v-if="image" class="wp-lightbox__figure" :data-zoom="zoom || undefined" @click.self="close">
|
|
110
114
|
<!-- An image with no src yet (still rendering): its shape, not an empty frame. -->
|
|
111
115
|
<Skeleton v-if="!image.src" class="wp-lightbox__loading" shape="block" width="min(70vw, 640px)" height="min(60vh, 480px)" label="Still loading" />
|
|
112
116
|
<WpImageZoom v-else-if="zoom" v-model:zoom="level" class="wp-lightbox__zoom" :src="image.src" :alt="image.alt" size="md" minimap />
|
package/src/RadioGroup.vue
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* `variant="swatch"` draws each option as a round colour swatch (its
|
|
20
20
|
* `color`), in a row, with the label as its name and tooltip: an accent or
|
|
21
|
-
* frame colour picked from a few.
|
|
21
|
+
* frame colour picked from a few. `named` writes each label under its
|
|
22
|
+
* swatch, and the `swatch` slot draws a swatch itself (a split "theme's own"
|
|
23
|
+
* circle, a rainbow for "custom"; the slot gets `{ option, checked }`).
|
|
22
24
|
*/
|
|
23
25
|
import { nextTick, ref } from "vue";
|
|
24
26
|
|
|
@@ -45,9 +47,14 @@ const props = withDefaults(defineProps<{
|
|
|
45
47
|
testPrefix?: string | undefined;
|
|
46
48
|
/** `swatch`: round colour swatches in a row, named by their label. */
|
|
47
49
|
variant?: "default" | "swatch" | undefined;
|
|
50
|
+
/** With `variant="swatch"`: show each label under its swatch. */
|
|
51
|
+
named?: boolean | undefined;
|
|
48
52
|
}>(), { orientation: "vertical", size: "md", ariaLabel: undefined, testPrefix: undefined, variant: "default" });
|
|
49
53
|
|
|
50
|
-
defineSlots<{
|
|
54
|
+
defineSlots<{
|
|
55
|
+
label?: (p: { option: RadioOption<T> }) => unknown;
|
|
56
|
+
swatch?: (p: { option: RadioOption<T>; checked: boolean }) => unknown;
|
|
57
|
+
}>();
|
|
51
58
|
|
|
52
59
|
const emit = defineEmits<{ (e: "update:modelValue", v: T): void }>();
|
|
53
60
|
const rootEl = ref<HTMLElement | null>(null);
|
|
@@ -93,6 +100,7 @@ function onKeydown(ev: KeyboardEvent, at: T): void {
|
|
|
93
100
|
:data-size="size"
|
|
94
101
|
:data-cards="cards || undefined"
|
|
95
102
|
:data-variant="variant"
|
|
103
|
+
:data-named="variant === 'swatch' && named ? '' : undefined"
|
|
96
104
|
>
|
|
97
105
|
<!-- Each radio has a tabindex (roving: only the checked one is a Tab stop). -->
|
|
98
106
|
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
|
@@ -106,18 +114,19 @@ function onKeydown(ev: KeyboardEvent, at: T): void {
|
|
|
106
114
|
:tabindex="!disabled && o.value === tabStop() ? 0 : -1"
|
|
107
115
|
:data-checked="modelValue === o.value || undefined"
|
|
108
116
|
:data-test="testPrefix ? `${testPrefix}-${o.value}` : undefined"
|
|
109
|
-
:aria-label="variant === 'swatch' ? o.label : undefined"
|
|
110
|
-
:title="variant === 'swatch' ? o.label : undefined"
|
|
117
|
+
:aria-label="variant === 'swatch' && !named ? o.label : undefined"
|
|
118
|
+
:title="variant === 'swatch' && !named ? o.label : undefined"
|
|
111
119
|
@click="!o.disabled && pick(o.value)"
|
|
112
120
|
@keydown="onKeydown($event, o.value)"
|
|
113
121
|
>
|
|
114
122
|
<span
|
|
115
123
|
v-if="variant === 'swatch'"
|
|
116
124
|
class="wp-radio-group__swatch"
|
|
117
|
-
:style="{ background: o.color }"
|
|
125
|
+
:style="$slots.swatch ? undefined : { background: o.color }"
|
|
118
126
|
aria-hidden="true"
|
|
119
|
-
|
|
127
|
+
><slot name="swatch" :option="o" :checked="modelValue === o.value" /></span>
|
|
120
128
|
<span v-else class="wp-radio-group__dot" aria-hidden="true" />
|
|
129
|
+
<span v-if="variant === 'swatch' && named" class="wp-radio-group__name">{{ o.label }}</span>
|
|
121
130
|
<span v-if="variant !== 'swatch'" class="wp-radio-group__text">
|
|
122
131
|
<span class="wp-radio-group__label"><slot name="label" :option="o">{{ o.label }}</slot></span>
|
|
123
132
|
<span v-if="o.hint" class="wp-radio-group__hint">{{ o.hint }}</span>
|
|
@@ -169,6 +178,17 @@ function onKeydown(ev: KeyboardEvent, at: T): void {
|
|
|
169
178
|
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-text) 18%, transparent);
|
|
170
179
|
}
|
|
171
180
|
.wp-radio-group[data-size="sm"] .wp-radio-group__swatch { width: 16px; height: 16px; }
|
|
181
|
+
/* Named swatches: the label under each, the ring drawn round the swatch only. */
|
|
182
|
+
.wp-radio-group[data-named] .wp-radio-group__opt {
|
|
183
|
+
flex-direction: column; align-items: center; gap: var(--wp-space-2); min-width: 52px;
|
|
184
|
+
padding: var(--wp-space-2); border-radius: var(--wp-radius-sm);
|
|
185
|
+
}
|
|
186
|
+
.wp-radio-group[data-named] .wp-radio-group__swatch { width: 28px; height: 28px; overflow: hidden; }
|
|
187
|
+
.wp-radio-group__name { color: var(--wp-text-muted); font-size: var(--wp-text-xs); line-height: 1.2; text-align: center; }
|
|
188
|
+
.wp-radio-group[data-named] .wp-radio-group__opt[data-checked] .wp-radio-group__name { color: var(--wp-text); }
|
|
189
|
+
.wp-radio-group[data-variant="swatch"][data-named] .wp-radio-group__opt[data-checked] { box-shadow: none; }
|
|
190
|
+
.wp-radio-group[data-named] .wp-radio-group__opt[data-checked] .wp-radio-group__swatch { box-shadow: 0 0 0 2px var(--wp-bg-1), 0 0 0 4px var(--wp-accent); }
|
|
191
|
+
.wp-radio-group[data-variant="swatch"][data-named] .wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
172
192
|
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt[data-checked] { box-shadow: 0 0 0 2px var(--wp-accent); }
|
|
173
193
|
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
174
194
|
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt[data-checked]:focus-visible { box-shadow: 0 0 0 2px var(--wp-accent), 0 0 0 4px var(--wp-border-focus); }
|
package/src/WpSwitch.vue
CHANGED
|
@@ -92,7 +92,9 @@ function flip(): void {
|
|
|
92
92
|
height: var(--switch-h);
|
|
93
93
|
box-sizing: border-box;
|
|
94
94
|
border: 1px solid var(--wp-border-strong);
|
|
95
|
-
|
|
95
|
+
/* A hair under half the height, not 999px: on the ComfyUI canvas Firefox
|
|
96
|
+
draws a bordered full pill in pieces that seam at zooms like 0.64 / 0.8. */
|
|
97
|
+
border-radius: min(calc(var(--switch-h) / 2 - 1px), var(--wp-radius-cap, 999px));
|
|
96
98
|
background: var(--wp-bg-3);
|
|
97
99
|
transition: background var(--wp-motion-quick), border-color var(--wp-motion-quick);
|
|
98
100
|
}
|