@dumiflex/wp-kit 0.0.0-stage → 0.3.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 +51 -0
- package/LICENSE +674 -0
- package/README.md +76 -3
- package/package.json +77 -6
- package/src/Avatar.vue +83 -0
- package/src/Badge.vue +97 -0
- package/src/Banner.vue +118 -0
- package/src/BookmarkButton.vue +27 -0
- package/src/BundleMeta.vue +51 -0
- package/src/Button.vue +103 -0
- package/src/Card.vue +68 -0
- package/src/Checkbox.vue +107 -0
- package/src/Chip.vue +97 -0
- package/src/ChipToggle.vue +92 -0
- package/src/ChoiceCards.vue +133 -0
- package/src/CloseButton.vue +69 -0
- package/src/CodeBlock.vue +119 -0
- package/src/ColorField.vue +206 -0
- package/src/ColorPicker.vue +201 -0
- package/src/Combobox.vue +334 -0
- package/src/ConfirmDialog.vue +163 -0
- package/src/ConfirmModal.vue +147 -0
- package/src/ConstraintMatrixPreview.vue +708 -0
- package/src/ContextMenu.vue +379 -0
- package/src/CopyButton.vue +156 -0
- package/src/DataTable.vue +292 -0
- package/src/DatePicker.vue +234 -0
- package/src/DescriptionList.vue +73 -0
- package/src/DiffView.vue +68 -0
- package/src/Disclosure.vue +166 -0
- package/src/DragHandle.vue +49 -0
- package/src/Drawer.vue +131 -0
- package/src/DropZone.vue +196 -0
- package/src/DropdownMenu.vue +72 -0
- package/src/EmptyState.vue +111 -0
- package/src/Field.vue +28 -0
- package/src/HsvPicker.vue +208 -0
- package/src/Icon.vue +39 -0
- package/src/IconPicker.vue +209 -0
- package/src/ImageGrid.vue +125 -0
- package/src/InlineEdit.vue +183 -0
- package/src/Input.vue +99 -0
- package/src/Kbd.vue +56 -0
- package/src/KindChip.vue +53 -0
- package/src/Lightbox.vue +173 -0
- package/src/ListFilter.vue +85 -0
- package/src/ListRow.vue +204 -0
- package/src/LoadingMark.vue +161 -0
- package/src/Logo.vue +60 -0
- package/src/Modal.vue +76 -0
- package/src/ModalShell.vue +107 -0
- package/src/Pagination.vue +106 -0
- package/src/Popover.vue +171 -0
- package/src/PostCard.vue +211 -0
- package/src/ProgressBar.vue +90 -0
- package/src/RadioGroup.vue +177 -0
- package/src/RangeSlider.vue +183 -0
- package/src/RatingBadge.vue +42 -0
- package/src/RelativeDate.vue +35 -0
- package/src/ResizeHandle.vue +143 -0
- package/src/SearchInput.vue +199 -0
- package/src/Select.vue +641 -0
- package/src/Separator.vue +54 -0
- package/src/Skeleton.vue +82 -0
- package/src/Slider.vue +198 -0
- package/src/SortableList.vue +260 -0
- package/src/SplitPane.vue +68 -0
- package/src/StarButton.vue +40 -0
- package/src/Stepper.vue +141 -0
- package/src/SupportLinks.vue +93 -0
- package/src/TagInput.vue +300 -0
- package/src/Textarea.vue +57 -0
- package/src/Timeline.vue +92 -0
- package/src/ToastCard.vue +119 -0
- package/src/ToastHost.vue +40 -0
- package/src/Toggle.vue +36 -0
- package/src/TokenizedValue.vue +310 -0
- package/src/Toolbar.vue +110 -0
- package/src/VirtualList.vue +87 -0
- package/src/VisuallyHidden.vue +29 -0
- package/src/WpButton.vue +75 -0
- package/src/WpCheck.vue +102 -0
- package/src/WpIconButton.vue +107 -0
- package/src/WpImageZoom.vue +544 -0
- package/src/WpNumber.vue +183 -0
- package/src/WpSegmented.vue +175 -0
- package/src/WpSelect.vue +89 -0
- package/src/WpSwitch.vue +163 -0
- package/src/WpTabs.vue +188 -0
- package/src/_modal-motion.css +33 -0
- package/src/_skeleton.css +50 -0
- package/src/axis-color.ts +42 -0
- package/src/button.css +169 -0
- package/src/calendar.ts +59 -0
- package/src/clipboard.ts +50 -0
- package/src/colour-presets.ts +12 -0
- package/src/diff.ts +51 -0
- package/src/drop-zone.ts +41 -0
- package/src/flip.ts +237 -0
- package/src/floating.ts +99 -0
- package/src/hsv.ts +100 -0
- package/src/icon-catalog.ts +52 -0
- package/src/image-zoom.ts +93 -0
- package/src/logo-paths.ts +21 -0
- package/src/matrix-axis.ts +91 -0
- package/src/pagination.ts +27 -0
- package/src/platform-keys.ts +29 -0
- package/src/rich-tokenize.ts +357 -0
- package/src/select-types.ts +32 -0
- package/src/slider-math.ts +45 -0
- package/src/sortable.ts +22 -0
- package/src/theme/fonts/OFL-Fraunces.txt +93 -0
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -0
- package/src/theme/fonts/OFL-Manrope.txt +93 -0
- package/src/theme/fonts/fraunces-latin-wght-normal.woff2 +0 -0
- package/src/theme/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/src/theme/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/src/theme/looks.css +428 -0
- package/src/theme/palettes.css +254 -0
- package/src/theme/tokens.css +2894 -0
- package/src/tokens.ts +56 -0
- package/src/tooltip.css +43 -0
- package/src/tooltip.ts +146 -0
- package/src/useDialogFocus.ts +73 -0
- package/src/useGrowableField.ts +357 -0
- package/src/useModalKeyShield.ts +116 -0
- package/src/useToast.ts +48 -0
- package/src/wp-logo.svg +14 -0
package/src/PostCard.vue
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A community post as a card: type chip, NSFW / Featured / recommended
|
|
4
|
+
* badges, version, `owner/name` slug, bundle contents, description, up to
|
|
5
|
+
* four tags, then stars, downloads and when it was updated. A coloured stripe
|
|
6
|
+
* on top says the type; a featured card is tinted, a deleted one faded.
|
|
7
|
+
*
|
|
8
|
+
* The whole card is one link. It renders an `<a>` by default; pass the app's
|
|
9
|
+
* router link as `as` (and its `to`) so the kit never imports a router:
|
|
10
|
+
*
|
|
11
|
+
* <PostCard :as="RouterLink" :to="`/p/${post.slug}`" :post="card(post)" />
|
|
12
|
+
* <PostCard href="/p/dumi/warm" :post="…" />
|
|
13
|
+
* <PostCard as="button" type="button" :post="…" @click="open" />
|
|
14
|
+
*/
|
|
15
|
+
import { computed, type Component } from "vue";
|
|
16
|
+
import BundleMeta from "./BundleMeta.vue";
|
|
17
|
+
import Icon from "./Icon.vue";
|
|
18
|
+
import KindChip, { type KindChipSubtype } from "./KindChip.vue";
|
|
19
|
+
import RatingBadge from "./RatingBadge.vue";
|
|
20
|
+
import RelativeDate from "./RelativeDate.vue";
|
|
21
|
+
|
|
22
|
+
export interface PostCardData {
|
|
23
|
+
/** `owner/name`. */
|
|
24
|
+
slug: string;
|
|
25
|
+
kind: "module" | "bundle";
|
|
26
|
+
subtype?: KindChipSubtype | "bundle" | null | undefined;
|
|
27
|
+
description?: string | undefined;
|
|
28
|
+
tags?: string[] | undefined;
|
|
29
|
+
stars: number;
|
|
30
|
+
downloads: number;
|
|
31
|
+
/** ISO time of the last update. */
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
rating?: "sfw" | "nsfw" | undefined;
|
|
34
|
+
featured?: boolean | undefined;
|
|
35
|
+
/** The latest version is marked recommended. */
|
|
36
|
+
recommended?: boolean | undefined;
|
|
37
|
+
/** Soft-deleted: the card fades and shows "deleted" where the version goes. */
|
|
38
|
+
deleted?: boolean | undefined;
|
|
39
|
+
version?: number | null | undefined;
|
|
40
|
+
bundleModules?: { count: number; names: string[] } | null | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const props = withDefaults(defineProps<{
|
|
44
|
+
post: PostCardData;
|
|
45
|
+
/** The viewer starred it: the star in the footer fills. */
|
|
46
|
+
starred?: boolean | undefined;
|
|
47
|
+
/** Element or component for the link, e.g. the app's RouterLink. */
|
|
48
|
+
as?: string | Component | undefined;
|
|
49
|
+
}>(), { starred: false, as: "a" });
|
|
50
|
+
|
|
51
|
+
const type = computed(() => (props.post.kind === "bundle" ? "bundle" : (props.post.subtype ?? "fixed_values")));
|
|
52
|
+
const owner = computed(() => props.post.slug.split("/")[0]);
|
|
53
|
+
const name = computed(() => props.post.slug.split("/").slice(1).join("/"));
|
|
54
|
+
const tags = computed(() => props.post.tags ?? []);
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<component
|
|
59
|
+
:is="as"
|
|
60
|
+
class="wp-post-card"
|
|
61
|
+
:data-type="type"
|
|
62
|
+
:data-featured="post.featured || undefined"
|
|
63
|
+
:data-deleted="post.deleted || undefined"
|
|
64
|
+
>
|
|
65
|
+
<div class="wp-post-card__top">
|
|
66
|
+
<div class="wp-post-card__badges">
|
|
67
|
+
<KindChip :kind="post.kind" :subtype="post.subtype" />
|
|
68
|
+
<RatingBadge :rating="post.rating" :featured="post.featured" />
|
|
69
|
+
<span v-if="post.recommended" class="wp-chip wp-chip--success">
|
|
70
|
+
<span class="wp-chip__dot" />recommended
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
<span v-if="post.deleted" class="wp-chip wp-chip--danger"><Icon name="trash" :size="10" />deleted</span>
|
|
74
|
+
<span v-else class="wp-chip">v{{ post.version ?? 1 }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="wp-post-card__slug">
|
|
78
|
+
<span class="wp-post-card__owner">{{ owner }}</span><span class="wp-post-card__sep">/</span>{{ name }}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<BundleMeta
|
|
82
|
+
v-if="post.kind === 'bundle' && post.bundleModules"
|
|
83
|
+
:count="post.bundleModules.count"
|
|
84
|
+
:names="post.bundleModules.names"
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<p v-if="post.description" class="wp-post-card__desc">{{ post.description }}</p>
|
|
88
|
+
|
|
89
|
+
<div v-if="tags.length" class="wp-post-card__tags">
|
|
90
|
+
<span v-for="t in tags.slice(0, 4)" :key="t" class="wp-chip">{{ t }}</span>
|
|
91
|
+
<span v-if="tags.length > 4" class="wp-chip">+{{ tags.length - 4 }}</span>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="wp-post-card__foot">
|
|
95
|
+
<span class="wp-post-card__stat" :data-active="starred || undefined">
|
|
96
|
+
<Icon :name="starred ? 'star-fill' : 'star'" :size="11" />{{ post.stars.toLocaleString() }}
|
|
97
|
+
</span>
|
|
98
|
+
<span class="wp-post-card__stat">
|
|
99
|
+
<Icon name="download" :size="11" />{{ post.downloads.toLocaleString() }}
|
|
100
|
+
</span>
|
|
101
|
+
<RelativeDate class="wp-post-card__when" :value="post.updatedAt" />
|
|
102
|
+
</div>
|
|
103
|
+
</component>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
.wp-post-card {
|
|
108
|
+
--post-card-c: var(--wp-text-dim);
|
|
109
|
+
position: relative;
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
gap: 14px;
|
|
113
|
+
width: 100%;
|
|
114
|
+
padding: 20px 20px 18px;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
break-inside: avoid;
|
|
117
|
+
/* The page spaces its cards (a grid gap or margins); one set here would
|
|
118
|
+
beat the page's rule, since scoped styles win ties. */
|
|
119
|
+
font: inherit;
|
|
120
|
+
color: inherit;
|
|
121
|
+
text-align: left;
|
|
122
|
+
text-decoration: none;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
background: var(--wp-bg-1);
|
|
125
|
+
border: 1px solid var(--wp-border);
|
|
126
|
+
border-radius: min(12px, var(--wp-radius-cap, 999px));
|
|
127
|
+
transition: transform var(--wp-motion-quick) ease, border-color var(--wp-motion-quick) ease, box-shadow var(--wp-motion-quick) ease;
|
|
128
|
+
}
|
|
129
|
+
.wp-post-card::before {
|
|
130
|
+
content: "";
|
|
131
|
+
position: absolute;
|
|
132
|
+
inset: 0 0 auto;
|
|
133
|
+
height: 3px;
|
|
134
|
+
opacity: 0.85;
|
|
135
|
+
background: linear-gradient(90deg, var(--post-card-c), color-mix(in oklab, var(--post-card-c) 50%, transparent));
|
|
136
|
+
}
|
|
137
|
+
.wp-post-card[data-type="fixed_values"] { --post-card-c: var(--wp-kind-fixed); }
|
|
138
|
+
.wp-post-card[data-type="combine"] { --post-card-c: var(--wp-kind-combine); }
|
|
139
|
+
.wp-post-card[data-type="wildcard"] { --post-card-c: var(--wp-kind-wildcard); }
|
|
140
|
+
.wp-post-card[data-type="constraint"] { --post-card-c: var(--wp-kind-constraint); }
|
|
141
|
+
.wp-post-card[data-type="derivation"] { --post-card-c: var(--wp-kind-derivation); }
|
|
142
|
+
.wp-post-card[data-type="bundle"] { --post-card-c: var(--wp-bundle-default); }
|
|
143
|
+
.wp-post-card:hover {
|
|
144
|
+
transform: translateY(-3px);
|
|
145
|
+
border-color: var(--wp-border-strong);
|
|
146
|
+
box-shadow: var(--wp-shadow-lg);
|
|
147
|
+
}
|
|
148
|
+
.wp-post-card:focus-visible { outline: none; box-shadow: var(--wp-focus-ring); }
|
|
149
|
+
.wp-post-card:hover .wp-post-card__slug { color: var(--wp-accent-text-strong); }
|
|
150
|
+
.wp-post-card[data-featured] {
|
|
151
|
+
background: linear-gradient(180deg, color-mix(in oklab, var(--wp-warn) 5%, var(--wp-bg-1)), var(--wp-bg-1) 60%);
|
|
152
|
+
border-color: color-mix(in oklab, var(--wp-warn) 25%, var(--wp-border));
|
|
153
|
+
}
|
|
154
|
+
.wp-post-card[data-featured]:hover { border-color: color-mix(in oklab, var(--wp-warn) 40%, var(--wp-border)); }
|
|
155
|
+
.wp-post-card[data-deleted] { opacity: 0.62; filter: grayscale(40%); }
|
|
156
|
+
.wp-post-card[data-deleted]:hover { opacity: 0.85; filter: none; }
|
|
157
|
+
|
|
158
|
+
.wp-post-card__top {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-wrap: wrap;
|
|
161
|
+
align-items: flex-start;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
gap: 8px;
|
|
164
|
+
min-height: 22px;
|
|
165
|
+
}
|
|
166
|
+
.wp-post-card__badges { display: flex; flex: 1; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
|
|
167
|
+
.wp-post-card__slug {
|
|
168
|
+
font-family: var(--wp-font-mono);
|
|
169
|
+
font-size: 15.5px;
|
|
170
|
+
font-weight: 500;
|
|
171
|
+
line-height: 1.35;
|
|
172
|
+
color: var(--wp-text);
|
|
173
|
+
word-break: break-word;
|
|
174
|
+
transition: color var(--wp-motion-quick) ease;
|
|
175
|
+
}
|
|
176
|
+
.wp-post-card__owner { font-weight: 400; color: var(--wp-text-muted); }
|
|
177
|
+
.wp-post-card__sep { color: var(--wp-accent-text); }
|
|
178
|
+
.wp-post-card__desc {
|
|
179
|
+
display: -webkit-box;
|
|
180
|
+
margin: 0;
|
|
181
|
+
overflow: hidden;
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
line-height: 19px;
|
|
184
|
+
color: var(--wp-text-muted);
|
|
185
|
+
text-wrap: pretty;
|
|
186
|
+
-webkit-line-clamp: 3;
|
|
187
|
+
-webkit-box-orient: vertical;
|
|
188
|
+
}
|
|
189
|
+
.wp-post-card__tags { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
190
|
+
.wp-post-card__tags .wp-chip { padding: 2px 8px; font-size: 11px; }
|
|
191
|
+
.wp-post-card__foot {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 16px;
|
|
195
|
+
margin-top: 4px;
|
|
196
|
+
padding-top: 16px;
|
|
197
|
+
border-top: 1px solid var(--wp-border);
|
|
198
|
+
font-size: 12px;
|
|
199
|
+
color: var(--wp-text-muted);
|
|
200
|
+
}
|
|
201
|
+
.wp-post-card__stat {
|
|
202
|
+
display: inline-flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
gap: 5px;
|
|
205
|
+
font-family: var(--wp-font-mono);
|
|
206
|
+
font-variant-numeric: tabular-nums;
|
|
207
|
+
}
|
|
208
|
+
.wp-post-card__stat :deep(.pi) { color: var(--wp-text-dim); }
|
|
209
|
+
.wp-post-card__stat[data-active] :deep(.pi) { color: var(--wp-accent-text); }
|
|
210
|
+
.wp-post-card__when { margin-left: auto; font-size: 11.5px; color: var(--wp-text-dim); }
|
|
211
|
+
</style>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Progress bar: how far along a job is. Give `value` (0 to `max`) for a bar that
|
|
4
|
+
* fills; leave it out (or `null`) when nobody knows yet, and the bar sweeps.
|
|
5
|
+
* Works on the canvas (`size="sm"`) and in the manager (`md`).
|
|
6
|
+
*
|
|
7
|
+
* <ProgressBar :value="done" :max="total" label="Testing seeds" show-value />
|
|
8
|
+
* <ProgressBar label="Loading the model" />
|
|
9
|
+
*
|
|
10
|
+
* It is an APG progressbar: `aria-valuenow` only while the value is known,
|
|
11
|
+
* so a screen reader says "busy" instead of a made-up number. The sweep
|
|
12
|
+
* stands still under reduced motion.
|
|
13
|
+
*/
|
|
14
|
+
import { computed } from "vue";
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
/** How far along, 0 to `max`; `null` or left out = not known yet. */
|
|
18
|
+
value?: number | null | undefined;
|
|
19
|
+
max?: number | undefined;
|
|
20
|
+
tone?: "accent" | "success" | "warn" | "danger" | undefined;
|
|
21
|
+
size?: "sm" | "md" | undefined;
|
|
22
|
+
/** The job's name for screen readers, and the line above the bar with `show-value`. */
|
|
23
|
+
label?: string | undefined;
|
|
24
|
+
/** Show the label and the percentage above the bar. */
|
|
25
|
+
showValue?: boolean | undefined;
|
|
26
|
+
}>(), { value: null, max: 100, tone: "accent", size: "md", label: undefined });
|
|
27
|
+
|
|
28
|
+
const known = computed(() => typeof props.value === "number" && Number.isFinite(props.value));
|
|
29
|
+
const pct = computed(() => {
|
|
30
|
+
if (!known.value || props.max <= 0) return 0;
|
|
31
|
+
return Math.min(100, Math.max(0, ((props.value as number) / props.max) * 100));
|
|
32
|
+
});
|
|
33
|
+
const pctText = computed(() => `${Math.round(pct.value)}%`);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="wp-progress" :data-size="size" :data-tone="tone">
|
|
38
|
+
<div v-if="showValue && (label || known)" class="wp-progress__head" aria-hidden="true">
|
|
39
|
+
<span class="wp-progress__label">{{ label }}</span>
|
|
40
|
+
<span v-if="known" class="wp-progress__value">{{ pctText }}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div
|
|
43
|
+
class="wp-progress__track"
|
|
44
|
+
role="progressbar"
|
|
45
|
+
:aria-label="label"
|
|
46
|
+
aria-valuemin="0"
|
|
47
|
+
:aria-valuemax="known ? max : undefined"
|
|
48
|
+
:aria-valuenow="known ? value ?? undefined : undefined"
|
|
49
|
+
:aria-valuetext="known ? pctText : undefined"
|
|
50
|
+
:aria-busy="known ? undefined : 'true'"
|
|
51
|
+
:data-indeterminate="known ? undefined : 'true'"
|
|
52
|
+
>
|
|
53
|
+
<div class="wp-progress__fill" :style="known ? { width: `${pct}%` } : undefined" />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.wp-progress { --_tone: var(--wp-accent); display: flex; flex-direction: column; gap: var(--wp-space-2); min-width: 0; width: 100%; }
|
|
60
|
+
.wp-progress[data-tone="success"] { --_tone: var(--wp-success); }
|
|
61
|
+
.wp-progress[data-tone="warn"] { --_tone: var(--wp-warn); }
|
|
62
|
+
.wp-progress[data-tone="danger"] { --_tone: var(--wp-danger); }
|
|
63
|
+
.wp-progress__head {
|
|
64
|
+
display: flex; justify-content: space-between; gap: var(--wp-space-3);
|
|
65
|
+
color: var(--wp-text-muted); font: 500 var(--wp-text-xs)/1.3 var(--wp-font-sans);
|
|
66
|
+
}
|
|
67
|
+
.wp-progress__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
68
|
+
.wp-progress__value { flex: none; font-variant-numeric: tabular-nums; color: var(--wp-text); }
|
|
69
|
+
.wp-progress__track {
|
|
70
|
+
position: relative; overflow: hidden; height: 6px;
|
|
71
|
+
border-radius: var(--wp-radius-cap, 999px); background: var(--wp-bg-3);
|
|
72
|
+
}
|
|
73
|
+
.wp-progress[data-size="sm"] .wp-progress__track { height: 4px; }
|
|
74
|
+
.wp-progress__fill {
|
|
75
|
+
height: 100%; border-radius: inherit; background: var(--_tone);
|
|
76
|
+
transition: width var(--wp-motion-swap);
|
|
77
|
+
}
|
|
78
|
+
.wp-progress__track[data-indeterminate] .wp-progress__fill {
|
|
79
|
+
position: absolute; inset: 0 auto 0 0; width: 40%;
|
|
80
|
+
animation: wp-progress-sweep 1.4s var(--wp-motion-curve-collapse) infinite;
|
|
81
|
+
}
|
|
82
|
+
@keyframes wp-progress-sweep {
|
|
83
|
+
from { transform: translateX(-100%); }
|
|
84
|
+
to { transform: translateX(250%); }
|
|
85
|
+
}
|
|
86
|
+
@media (prefers-reduced-motion: reduce) {
|
|
87
|
+
.wp-progress__fill { transition: none; }
|
|
88
|
+
.wp-progress__track[data-indeterminate] .wp-progress__fill { animation: none; width: 100%; opacity: 0.45; }
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends string">
|
|
2
|
+
/**
|
|
3
|
+
* Radio group: one of a few options, each with room for a line saying what
|
|
4
|
+
* it does. Use it when the options need explaining; for two to five short
|
|
5
|
+
* words that fit on one row use the Segmented control. Works on the canvas
|
|
6
|
+
* (`size="sm"`) and in the manager (`md`).
|
|
7
|
+
*
|
|
8
|
+
* <RadioGroup v-model="mode" :options="[{ value: 'tags', label: 'Tags', hint: 'Danbooru-style, comma separated' }]" aria-label="Prompt style" />
|
|
9
|
+
*
|
|
10
|
+
* APG radio group: one Tab stop (the chosen option), the arrow keys move and
|
|
11
|
+
* choose, disabled options are skipped. `orientation="horizontal"` lays the
|
|
12
|
+
* options out in a row. The `label` slot draws an option's label itself
|
|
13
|
+
* (a name plus a badge), and `test-prefix` gives each option a `data-test`.
|
|
14
|
+
*
|
|
15
|
+
* <RadioGroup v-model="provider" :options="opts" test-prefix="provider">
|
|
16
|
+
* <template #label="{ option }">{{ option.label }} <Badge text="Local" /></template>
|
|
17
|
+
* </RadioGroup>
|
|
18
|
+
*
|
|
19
|
+
* `variant="swatch"` draws each option as a round colour swatch (its
|
|
20
|
+
* `color`), in a row, with the label as its name and tooltip: an accent or
|
|
21
|
+
* frame colour picked from a few.
|
|
22
|
+
*/
|
|
23
|
+
import { nextTick, ref } from "vue";
|
|
24
|
+
|
|
25
|
+
export interface RadioOption<V extends string = string> {
|
|
26
|
+
value: V;
|
|
27
|
+
label: string;
|
|
28
|
+
hint?: string | undefined;
|
|
29
|
+
disabled?: boolean | undefined;
|
|
30
|
+
/** The swatch colour, for `variant="swatch"`. */
|
|
31
|
+
color?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<{
|
|
35
|
+
modelValue: T | null;
|
|
36
|
+
options: RadioOption<T>[];
|
|
37
|
+
orientation?: "vertical" | "horizontal" | undefined;
|
|
38
|
+
size?: "sm" | "md" | undefined;
|
|
39
|
+
disabled?: boolean | undefined;
|
|
40
|
+
invalid?: boolean | undefined;
|
|
41
|
+
ariaLabel?: string | undefined;
|
|
42
|
+
/** Draw each option as a bordered card (for a few big choices). */
|
|
43
|
+
cards?: boolean | undefined;
|
|
44
|
+
/** `data-test` of each option is `<testPrefix>-<value>`. */
|
|
45
|
+
testPrefix?: string | undefined;
|
|
46
|
+
/** `swatch`: round colour swatches in a row, named by their label. */
|
|
47
|
+
variant?: "default" | "swatch" | undefined;
|
|
48
|
+
}>(), { orientation: "vertical", size: "md", ariaLabel: undefined, testPrefix: undefined, variant: "default" });
|
|
49
|
+
|
|
50
|
+
defineSlots<{ label?: (p: { option: RadioOption<T> }) => unknown }>();
|
|
51
|
+
|
|
52
|
+
const emit = defineEmits<{ (e: "update:modelValue", v: T): void }>();
|
|
53
|
+
const rootEl = ref<HTMLElement | null>(null);
|
|
54
|
+
|
|
55
|
+
function pick(v: T): void {
|
|
56
|
+
if (!props.disabled && v !== props.modelValue) emit("update:modelValue", v);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tabStop(): T | undefined {
|
|
60
|
+
const live = props.options.filter((o) => !o.disabled);
|
|
61
|
+
return (live.find((o) => o.value === props.modelValue) ?? live[0])?.value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onKeydown(ev: KeyboardEvent, at: T): void {
|
|
65
|
+
if (props.disabled) return;
|
|
66
|
+
const live = props.options.filter((o) => !o.disabled);
|
|
67
|
+
const i = live.findIndex((o) => o.value === at);
|
|
68
|
+
let next: number;
|
|
69
|
+
switch (ev.key) {
|
|
70
|
+
case "ArrowDown": case "ArrowRight": next = (i + 1) % live.length; break;
|
|
71
|
+
case "ArrowUp": case "ArrowLeft": next = (i - 1 + live.length) % live.length; break;
|
|
72
|
+
case " ": ev.preventDefault(); pick(at); return;
|
|
73
|
+
default: return;
|
|
74
|
+
}
|
|
75
|
+
ev.preventDefault();
|
|
76
|
+
const v = live[next].value;
|
|
77
|
+
pick(v);
|
|
78
|
+
void nextTick(() => {
|
|
79
|
+
rootEl.value?.querySelectorAll<HTMLElement>("[role=radio]")[props.options.findIndex((o) => o.value === v)]?.focus();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div
|
|
86
|
+
ref="rootEl"
|
|
87
|
+
class="wp-radio-group"
|
|
88
|
+
role="radiogroup"
|
|
89
|
+
:aria-label="ariaLabel"
|
|
90
|
+
:aria-disabled="disabled || undefined"
|
|
91
|
+
:aria-invalid="invalid || undefined"
|
|
92
|
+
:data-orientation="orientation"
|
|
93
|
+
:data-size="size"
|
|
94
|
+
:data-cards="cards || undefined"
|
|
95
|
+
:data-variant="variant"
|
|
96
|
+
>
|
|
97
|
+
<!-- Each radio has a tabindex (roving: only the checked one is a Tab stop). -->
|
|
98
|
+
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
|
99
|
+
<div
|
|
100
|
+
v-for="o in options"
|
|
101
|
+
:key="o.value"
|
|
102
|
+
class="wp-radio-group__opt"
|
|
103
|
+
role="radio"
|
|
104
|
+
:aria-checked="modelValue === o.value"
|
|
105
|
+
:aria-disabled="disabled || o.disabled || undefined"
|
|
106
|
+
:tabindex="!disabled && o.value === tabStop() ? 0 : -1"
|
|
107
|
+
:data-checked="modelValue === o.value || undefined"
|
|
108
|
+
:data-test="testPrefix ? `${testPrefix}-${o.value}` : undefined"
|
|
109
|
+
:aria-label="variant === 'swatch' ? o.label : undefined"
|
|
110
|
+
:title="variant === 'swatch' ? o.label : undefined"
|
|
111
|
+
@click="!o.disabled && pick(o.value)"
|
|
112
|
+
@keydown="onKeydown($event, o.value)"
|
|
113
|
+
>
|
|
114
|
+
<span
|
|
115
|
+
v-if="variant === 'swatch'"
|
|
116
|
+
class="wp-radio-group__swatch"
|
|
117
|
+
:style="{ background: o.color }"
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
/>
|
|
120
|
+
<span v-else class="wp-radio-group__dot" aria-hidden="true" />
|
|
121
|
+
<span v-if="variant !== 'swatch'" class="wp-radio-group__text">
|
|
122
|
+
<span class="wp-radio-group__label"><slot name="label" :option="o">{{ o.label }}</slot></span>
|
|
123
|
+
<span v-if="o.hint" class="wp-radio-group__hint">{{ o.hint }}</span>
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<style scoped>
|
|
130
|
+
.wp-radio-group { display: flex; flex-direction: column; gap: var(--wp-space-3); min-width: 0; font-family: var(--wp-font-sans); }
|
|
131
|
+
.wp-radio-group[data-orientation="horizontal"] { flex-direction: row; flex-wrap: wrap; gap: var(--wp-space-5); }
|
|
132
|
+
.wp-radio-group[data-size="sm"] { gap: var(--wp-space-2); }
|
|
133
|
+
.wp-radio-group__opt {
|
|
134
|
+
display: flex; align-items: flex-start; gap: var(--wp-space-3); min-width: 0;
|
|
135
|
+
padding: 2px; border-radius: var(--wp-radius-sm); color: var(--wp-text); cursor: pointer; outline: none;
|
|
136
|
+
}
|
|
137
|
+
.wp-radio-group[data-cards] .wp-radio-group__opt {
|
|
138
|
+
flex: 1 1 0; padding: var(--wp-space-4); border: 1px solid var(--wp-border); border-radius: var(--wp-radius); background: var(--wp-bg-1);
|
|
139
|
+
transition: border-color var(--wp-motion-hover), background var(--wp-motion-hover);
|
|
140
|
+
}
|
|
141
|
+
.wp-radio-group[data-cards] .wp-radio-group__opt[data-checked] { border-color: var(--wp-accent); background: color-mix(in srgb, var(--wp-accent) 8%, var(--wp-bg-1)); }
|
|
142
|
+
.wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
143
|
+
.wp-radio-group__opt[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
|
|
144
|
+
.wp-radio-group__dot {
|
|
145
|
+
position: relative; flex: none; box-sizing: border-box; width: 16px; height: 16px; margin-top: 1px;
|
|
146
|
+
border: 1.5px solid var(--wp-border-strong); border-radius: 50%; /* round: a radio is round in every look */ background: var(--wp-bg-1);
|
|
147
|
+
transition: border-color var(--wp-motion-hover);
|
|
148
|
+
}
|
|
149
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__dot { width: 13px; height: 13px; }
|
|
150
|
+
.wp-radio-group__opt:hover:not([aria-disabled="true"]) .wp-radio-group__dot { border-color: var(--wp-accent); }
|
|
151
|
+
.wp-radio-group__opt[data-checked] .wp-radio-group__dot { border-color: var(--wp-accent); }
|
|
152
|
+
.wp-radio-group__opt[data-checked] .wp-radio-group__dot::after {
|
|
153
|
+
content: ""; position: absolute; inset: 3px; border-radius: 50%; /* round: radio dot */ background: var(--wp-accent);
|
|
154
|
+
}
|
|
155
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot::after { inset: 2.5px; }
|
|
156
|
+
/* A picked option that is disabled turns grey, so it can't pass for a live one. */
|
|
157
|
+
.wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot { border-color: var(--wp-text-dim); }
|
|
158
|
+
.wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot::after { background: var(--wp-text-dim); }
|
|
159
|
+
.wp-radio-group[aria-invalid="true"] .wp-radio-group__dot { border-color: var(--wp-error-border); }
|
|
160
|
+
.wp-radio-group__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
161
|
+
.wp-radio-group__label { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--wp-space-3); font-size: var(--wp-text-sm); line-height: 18px; }
|
|
162
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__label { font-size: var(--wp-text-xs); line-height: 15px; }
|
|
163
|
+
.wp-radio-group__hint { color: var(--wp-text-muted); font-size: var(--wp-text-xs); line-height: 1.4; }
|
|
164
|
+
.wp-radio-group[data-variant="swatch"] { flex-direction: row; flex-wrap: wrap; gap: var(--wp-space-3); }
|
|
165
|
+
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt { padding: 2px; border-radius: min(50%, var(--wp-radius-cap, 999px)); }
|
|
166
|
+
.wp-radio-group__swatch {
|
|
167
|
+
display: block; width: 20px; height: 20px; border-radius: min(50%, var(--wp-radius-cap, 999px));
|
|
168
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-text) 18%, transparent);
|
|
169
|
+
}
|
|
170
|
+
.wp-radio-group[data-size="sm"] .wp-radio-group__swatch { width: 16px; height: 16px; }
|
|
171
|
+
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt[data-checked] { box-shadow: 0 0 0 2px var(--wp-accent); }
|
|
172
|
+
.wp-radio-group[data-variant="swatch"] .wp-radio-group__opt:focus-visible { box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
173
|
+
.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); }
|
|
174
|
+
@media (prefers-reduced-motion: reduce) {
|
|
175
|
+
.wp-radio-group__dot, .wp-radio-group[data-cards] .wp-radio-group__opt { transition: none; }
|
|
176
|
+
}
|
|
177
|
+
</style>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Range slider: a low and a high number on one track (a weight range, a
|
|
4
|
+
* token budget, a sweep's from–to). Two thumbs that can't cross, kept at
|
|
5
|
+
* least `min-gap` apart; `with-input` adds a WpNumber for each end. Works on
|
|
6
|
+
* the canvas (`size="sm"`) and in the manager (`md`).
|
|
7
|
+
*
|
|
8
|
+
* <RangeSlider v-model="range" :min="0" :max="2" :step="0.1" aria-label="Weight" with-input />
|
|
9
|
+
*
|
|
10
|
+
* Each thumb is an APG slider ("Weight, low end" / "high end") whose
|
|
11
|
+
* min and max are the other thumb, so screen readers hear the real limits.
|
|
12
|
+
* A click on the track moves the nearer thumb.
|
|
13
|
+
*/
|
|
14
|
+
import { computed, ref } from "vue";
|
|
15
|
+
import { keyStep, percentOf, snap, valueAt } from "./slider-math";
|
|
16
|
+
import WpNumber from "./WpNumber.vue";
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<{
|
|
19
|
+
modelValue: [number, number];
|
|
20
|
+
min?: number | undefined;
|
|
21
|
+
max?: number | undefined;
|
|
22
|
+
step?: number | undefined;
|
|
23
|
+
/** The smallest distance between the two ends. */
|
|
24
|
+
minGap?: number | undefined;
|
|
25
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
26
|
+
disabled?: boolean | undefined;
|
|
27
|
+
ariaLabel?: string | undefined;
|
|
28
|
+
format?: ((v: number) => string) | undefined;
|
|
29
|
+
withInput?: boolean | undefined;
|
|
30
|
+
}>(), { min: 0, max: 100, step: 1, minGap: 0, size: "md", ariaLabel: "Range" });
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{ (e: "update:modelValue", v: [number, number]): void; (e: "change", v: [number, number]): void }>();
|
|
33
|
+
|
|
34
|
+
const trackEl = ref<HTMLElement | null>(null);
|
|
35
|
+
const active = ref<0 | 1 | null>(null);
|
|
36
|
+
|
|
37
|
+
const lo = computed(() => snap(Math.min(props.modelValue[0], props.modelValue[1]), props.min, props.max, props.step));
|
|
38
|
+
const hi = computed(() => snap(Math.max(props.modelValue[0], props.modelValue[1]), props.min, props.max, props.step));
|
|
39
|
+
const loPct = computed(() => percentOf(lo.value, props.min, props.max));
|
|
40
|
+
const hiPct = computed(() => percentOf(hi.value, props.min, props.max));
|
|
41
|
+
const text = (v: number): string => (props.format ? props.format(v) : String(v));
|
|
42
|
+
|
|
43
|
+
/** Put one end at `v`, kept inside the other end minus the gap. */
|
|
44
|
+
function setEnd(end: 0 | 1, v: number): [number, number] {
|
|
45
|
+
const next: [number, number] = end === 0
|
|
46
|
+
? [Math.min(v, snap(hi.value - props.minGap, props.min, props.max, props.step)), hi.value]
|
|
47
|
+
: [lo.value, Math.max(v, snap(lo.value + props.minGap, props.min, props.max, props.step))];
|
|
48
|
+
if (next[0] !== props.modelValue[0] || next[1] !== props.modelValue[1]) emit("update:modelValue", next);
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fromPointer(ev: PointerEvent): number {
|
|
53
|
+
const r = trackEl.value?.getBoundingClientRect();
|
|
54
|
+
return r ? valueAt(ev.clientX, r.left, r.width, props.min, props.max, props.step) : lo.value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function onPointerDown(ev: PointerEvent): void {
|
|
58
|
+
if (props.disabled || ev.button !== 0) return;
|
|
59
|
+
ev.preventDefault();
|
|
60
|
+
const v = fromPointer(ev);
|
|
61
|
+
active.value = Math.abs(v - lo.value) <= Math.abs(v - hi.value) && !(v > hi.value) ? 0 : 1;
|
|
62
|
+
(ev.currentTarget as HTMLElement).setPointerCapture?.(ev.pointerId);
|
|
63
|
+
setEnd(active.value, v);
|
|
64
|
+
trackEl.value?.querySelectorAll<HTMLElement>("[role=slider]")[active.value]?.focus();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onPointerMove(ev: PointerEvent): void {
|
|
68
|
+
if (active.value !== null) setEnd(active.value, fromPointer(ev));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onPointerUp(ev: PointerEvent): void {
|
|
72
|
+
if (active.value === null) return;
|
|
73
|
+
active.value = null;
|
|
74
|
+
(ev.currentTarget as HTMLElement).releasePointerCapture?.(ev.pointerId);
|
|
75
|
+
emit("change", [lo.value, hi.value]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onKeydown(ev: KeyboardEvent, end: 0 | 1): void {
|
|
79
|
+
if (props.disabled) return;
|
|
80
|
+
const lowLimit = end === 0 ? props.min : lo.value + props.minGap;
|
|
81
|
+
const highLimit = end === 0 ? hi.value - props.minGap : props.max;
|
|
82
|
+
const cur = end === 0 ? lo.value : hi.value;
|
|
83
|
+
const next = keyStep(ev.key, cur, props.min, props.max, props.step);
|
|
84
|
+
if (next === null) return;
|
|
85
|
+
ev.preventDefault();
|
|
86
|
+
const v = ev.key === "Home" ? lowLimit : ev.key === "End" ? highLimit : next;
|
|
87
|
+
emit("change", setEnd(end, v));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onTyped(end: 0 | 1, v: number | null): void {
|
|
91
|
+
if (v === null) return;
|
|
92
|
+
emit("change", setEnd(end, snap(v, props.min, props.max, props.step)));
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div class="wp-range-slider" :data-size="size" :data-disabled="disabled || undefined">
|
|
98
|
+
<WpNumber
|
|
99
|
+
v-if="withInput"
|
|
100
|
+
class="wp-range-slider__input"
|
|
101
|
+
:model-value="lo"
|
|
102
|
+
:min="min"
|
|
103
|
+
:max="hi - props.minGap"
|
|
104
|
+
:step="step"
|
|
105
|
+
:size="size"
|
|
106
|
+
:disabled="disabled"
|
|
107
|
+
:aria-label="`${ariaLabel}, low end`"
|
|
108
|
+
@update:model-value="onTyped(0, $event)"
|
|
109
|
+
/>
|
|
110
|
+
<div
|
|
111
|
+
ref="trackEl"
|
|
112
|
+
class="wp-range-slider__track"
|
|
113
|
+
@pointerdown="onPointerDown"
|
|
114
|
+
@pointermove="onPointerMove"
|
|
115
|
+
@pointerup="onPointerUp"
|
|
116
|
+
@pointercancel="onPointerUp"
|
|
117
|
+
>
|
|
118
|
+
<div class="wp-range-slider__rail" />
|
|
119
|
+
<div class="wp-range-slider__fill" :style="{ left: `${loPct}%`, width: `${hiPct - loPct}%` }" />
|
|
120
|
+
<!-- Focusable unless disabled (tabindex is bound). -->
|
|
121
|
+
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
|
|
122
|
+
<div
|
|
123
|
+
v-for="end in ([0, 1] as const)"
|
|
124
|
+
:key="end"
|
|
125
|
+
class="wp-range-slider__thumb"
|
|
126
|
+
role="slider"
|
|
127
|
+
:tabindex="disabled ? -1 : 0"
|
|
128
|
+
:aria-label="`${ariaLabel}, ${end === 0 ? 'low' : 'high'} end`"
|
|
129
|
+
:aria-valuemin="end === 0 ? min : lo + props.minGap"
|
|
130
|
+
:aria-valuemax="end === 0 ? hi - props.minGap : max"
|
|
131
|
+
:aria-valuenow="end === 0 ? lo : hi"
|
|
132
|
+
:aria-valuetext="format ? text(end === 0 ? lo : hi) : undefined"
|
|
133
|
+
:aria-disabled="disabled || undefined"
|
|
134
|
+
:data-active="active === end || undefined"
|
|
135
|
+
:style="{ left: `${end === 0 ? loPct : hiPct}%` }"
|
|
136
|
+
@keydown="onKeydown($event, end)"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
<WpNumber
|
|
140
|
+
v-if="withInput"
|
|
141
|
+
class="wp-range-slider__input"
|
|
142
|
+
:model-value="hi"
|
|
143
|
+
:min="lo + props.minGap"
|
|
144
|
+
:max="max"
|
|
145
|
+
:step="step"
|
|
146
|
+
:size="size"
|
|
147
|
+
:disabled="disabled"
|
|
148
|
+
:aria-label="`${ariaLabel}, high end`"
|
|
149
|
+
@update:model-value="onTyped(1, $event)"
|
|
150
|
+
/>
|
|
151
|
+
<span v-else class="wp-range-slider__value" aria-hidden="true">{{ text(lo) }} – {{ text(hi) }}</span>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
|
|
155
|
+
<style scoped>
|
|
156
|
+
.wp-range-slider {
|
|
157
|
+
--_h: var(--wp-control-h-md); --_thumb: 16px;
|
|
158
|
+
display: flex; align-items: center; gap: var(--wp-space-4); min-width: 0; width: 100%; min-height: var(--_h);
|
|
159
|
+
font-family: var(--wp-font-sans);
|
|
160
|
+
}
|
|
161
|
+
.wp-range-slider[data-size="sm"] { --_h: var(--wp-control-h-sm); --_thumb: 12px; gap: var(--wp-space-3); }
|
|
162
|
+
.wp-range-slider[data-size="lg"] { --_h: var(--wp-control-h-lg); --_thumb: 18px; }
|
|
163
|
+
.wp-range-slider__track { position: relative; flex: 1; min-width: 80px; height: var(--_h); margin: 0 calc(var(--_thumb) / 2); cursor: pointer; touch-action: none; }
|
|
164
|
+
.wp-range-slider__rail, .wp-range-slider__fill { position: absolute; top: 50%; height: 4px; margin-top: -2px; border-radius: var(--wp-radius-cap, 999px); }
|
|
165
|
+
.wp-range-slider__rail { left: calc(var(--_thumb) / -2); right: calc(var(--_thumb) / -2); background: var(--wp-bg-3); box-shadow: inset 0 0 0 1px var(--wp-border-soft); }
|
|
166
|
+
.wp-range-slider__fill { background: var(--wp-accent); }
|
|
167
|
+
.wp-range-slider[data-size="sm"] .wp-range-slider__rail, .wp-range-slider[data-size="sm"] .wp-range-slider__fill { height: 3px; margin-top: -1.5px; }
|
|
168
|
+
.wp-range-slider__thumb {
|
|
169
|
+
position: absolute; top: 50%; box-sizing: border-box; width: var(--_thumb); height: var(--_thumb);
|
|
170
|
+
margin: calc(var(--_thumb) / -2) 0 0 calc(var(--_thumb) / -2);
|
|
171
|
+
border: 2px solid var(--wp-accent); border-radius: min(50%, var(--wp-radius-cap, 999px)); background: var(--wp-bg); box-shadow: var(--wp-shadow-sm); outline: none;
|
|
172
|
+
transition: transform var(--wp-motion-quick), box-shadow var(--wp-motion-quick);
|
|
173
|
+
}
|
|
174
|
+
.wp-range-slider__thumb[data-active], .wp-range-slider__thumb:hover { transform: scale(1.12); }
|
|
175
|
+
.wp-range-slider__thumb:focus-visible { box-shadow: 0 0 0 3px var(--wp-focus-ring); z-index: 1; }
|
|
176
|
+
.wp-range-slider__value { flex: none; color: var(--wp-text); font: 500 var(--wp-text-sm)/1 var(--wp-font-sans); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
177
|
+
.wp-range-slider[data-size="sm"] .wp-range-slider__value { font-size: var(--wp-text-xs); }
|
|
178
|
+
.wp-range-slider__input { flex: none; width: 76px; }
|
|
179
|
+
.wp-range-slider[data-size="sm"] .wp-range-slider__input { width: 60px; }
|
|
180
|
+
.wp-range-slider[data-disabled] { opacity: 0.45; }
|
|
181
|
+
.wp-range-slider[data-disabled] .wp-range-slider__track { cursor: not-allowed; }
|
|
182
|
+
@media (prefers-reduced-motion: reduce) { .wp-range-slider__thumb { transition: none; } }
|
|
183
|
+
</style>
|