@dumiflex/wp-kit 0.0.0-stage → 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 -0
- package/LICENSE +674 -0
- package/README.md +72 -2
- package/package.json +76 -4
- 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 +109 -0
- package/src/Chip.vue +163 -0
- package/src/ChoiceCards.vue +133 -0
- package/src/CloseButton.vue +69 -0
- package/src/CodeBlock.vue +119 -0
- package/src/ColorField.vue +188 -0
- package/src/ColorPicker.vue +201 -0
- package/src/Combobox.vue +331 -0
- package/src/ConfirmDialog.vue +165 -0
- package/src/ConfirmModal.vue +147 -0
- package/src/ConstraintMatrixPreview.vue +708 -0
- package/src/ContextMenu.vue +386 -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/Lightbox.vue +173 -0
- package/src/ListFilter.vue +85 -0
- package/src/ListRow.vue +204 -0
- package/src/LoadingMark.vue +175 -0
- package/src/Logo.vue +60 -0
- package/src/Modal.vue +78 -0
- package/src/ModalShell.vue +109 -0
- package/src/Pagination.vue +106 -0
- package/src/Popover.vue +180 -0
- package/src/PostCard.vue +210 -0
- package/src/ProgressBar.vue +90 -0
- package/src/RadioGroup.vue +177 -0
- package/src/RangeSlider.vue +183 -0
- package/src/RelativeDate.vue +35 -0
- package/src/ResizeHandle.vue +143 -0
- package/src/SearchInput.vue +192 -0
- package/src/Select.vue +644 -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 +297 -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 +104 -0
- package/src/WpIconButton.vue +107 -0
- package/src/WpImageZoom.vue +544 -0
- package/src/WpNumber.vue +178 -0
- package/src/WpSegmented.vue +175 -0
- package/src/WpSelect.vue +93 -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/field.css +50 -0
- package/src/flip.ts +237 -0
- package/src/floating-layer.ts +48 -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/kind-colour.ts +28 -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 +2896 -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
|
@@ -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: 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>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ value: string | null | undefined }>();
|
|
5
|
+
|
|
6
|
+
const display = computed(() => {
|
|
7
|
+
if (!props.value) return "—";
|
|
8
|
+
const ts = Date.parse(props.value);
|
|
9
|
+
if (Number.isNaN(ts)) return "—";
|
|
10
|
+
const diff = Date.now() - ts;
|
|
11
|
+
const sec = Math.floor(diff / 1000);
|
|
12
|
+
if (sec < 60) return "just now";
|
|
13
|
+
const min = Math.floor(sec / 60);
|
|
14
|
+
if (min < 60) return `${min}m ago`;
|
|
15
|
+
const hr = Math.floor(min / 60);
|
|
16
|
+
if (hr < 24) return `${hr}h ago`;
|
|
17
|
+
const day = Math.floor(hr / 24);
|
|
18
|
+
if (day < 7) return `${day}d ago`;
|
|
19
|
+
if (day < 30) return `${Math.floor(day / 7)}w ago`;
|
|
20
|
+
if (day < 365) return `${Math.floor(day / 30)}mo ago`;
|
|
21
|
+
return `${Math.floor(day / 365)}y ago`;
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<span class="wp-reldate" :title="value || ''">{{ display }}</span>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.wp-reldate {
|
|
31
|
+
font-size: 0.75rem;
|
|
32
|
+
line-height: 1rem;
|
|
33
|
+
color: var(--wp-text2);
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Resize handle: the grip on a panel's edge that drags it wider or taller
|
|
4
|
+
* (a drawer's width, a column, a box's height). It owns the size in pixels
|
|
5
|
+
* through v-model; the parent puts that size on the panel. Works on the
|
|
6
|
+
* canvas and in the manager. Two panels side by side use SplitPane, which
|
|
7
|
+
* is built on this.
|
|
8
|
+
*
|
|
9
|
+
* <aside :style="{ width: `${w}px` }">…<ResizeHandle v-model="w" axis="x" reverse :min="280" :max="720" label="Chat width" /></aside>
|
|
10
|
+
*
|
|
11
|
+
* APG window splitter: `role="separator"` that takes focus; the arrows move
|
|
12
|
+
* it 16 px (Shift: 64), Home / End jump to min / max, Enter or a double
|
|
13
|
+
* click puts back `default-size`. `reverse` is for a handle on a panel's
|
|
14
|
+
* left or top edge, where dragging toward the start makes it bigger.
|
|
15
|
+
*/
|
|
16
|
+
import { computed, ref } from "vue";
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<{
|
|
19
|
+
/** The panel's size in px. */
|
|
20
|
+
modelValue: number;
|
|
21
|
+
/** x: the handle changes width (an upright bar). y: height (a flat bar). */
|
|
22
|
+
axis?: "x" | "y" | undefined;
|
|
23
|
+
min?: number | undefined;
|
|
24
|
+
max?: number | undefined;
|
|
25
|
+
/** Dragging toward the start (left / up) makes the panel bigger. */
|
|
26
|
+
reverse?: boolean | undefined;
|
|
27
|
+
/** Where Enter and a double click put it back. */
|
|
28
|
+
defaultSize?: number | undefined;
|
|
29
|
+
/** Keyboard step in px. */
|
|
30
|
+
step?: number | undefined;
|
|
31
|
+
/** What it resizes, for screen readers ("Chat width"). */
|
|
32
|
+
label: string;
|
|
33
|
+
/** The id of the panel it resizes. */
|
|
34
|
+
controls?: string | undefined;
|
|
35
|
+
disabled?: boolean | undefined;
|
|
36
|
+
}>(), { axis: "x", min: 0, max: Number.POSITIVE_INFINITY, defaultSize: undefined, step: 16, controls: undefined });
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{ (e: "update:modelValue", v: number): void; (e: "end", v: number): void }>();
|
|
39
|
+
|
|
40
|
+
const dragging = ref(false);
|
|
41
|
+
let start = 0;
|
|
42
|
+
let startSize = 0;
|
|
43
|
+
|
|
44
|
+
const clamp = (v: number): number => Math.round(Math.min(props.max, Math.max(props.min, v)));
|
|
45
|
+
const finiteMax = computed(() => (Number.isFinite(props.max) ? props.max : undefined));
|
|
46
|
+
|
|
47
|
+
function set(v: number): void {
|
|
48
|
+
const next = clamp(v);
|
|
49
|
+
if (next !== props.modelValue) emit("update:modelValue", next);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onPointerDown(ev: PointerEvent): void {
|
|
53
|
+
if (props.disabled || ev.button !== 0) return;
|
|
54
|
+
ev.preventDefault();
|
|
55
|
+
dragging.value = true;
|
|
56
|
+
start = props.axis === "x" ? ev.clientX : ev.clientY;
|
|
57
|
+
startSize = props.modelValue;
|
|
58
|
+
(ev.currentTarget as HTMLElement).setPointerCapture?.(ev.pointerId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onPointerMove(ev: PointerEvent): void {
|
|
62
|
+
if (!dragging.value) return;
|
|
63
|
+
const d = (props.axis === "x" ? ev.clientX : ev.clientY) - start;
|
|
64
|
+
set(startSize + (props.reverse ? -d : d));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onPointerUp(ev: PointerEvent): void {
|
|
68
|
+
if (!dragging.value) return;
|
|
69
|
+
dragging.value = false;
|
|
70
|
+
(ev.currentTarget as HTMLElement).releasePointerCapture?.(ev.pointerId);
|
|
71
|
+
emit("end", props.modelValue);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function reset(): void {
|
|
75
|
+
if (props.defaultSize === undefined || props.disabled) return;
|
|
76
|
+
set(props.defaultSize);
|
|
77
|
+
emit("end", clamp(props.defaultSize));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onKeydown(ev: KeyboardEvent): void {
|
|
81
|
+
if (props.disabled) return;
|
|
82
|
+
const s = ev.shiftKey ? props.step * 4 : props.step;
|
|
83
|
+
const grow = props.reverse ? -1 : 1;
|
|
84
|
+
const fwd = props.axis === "x" ? "ArrowRight" : "ArrowDown";
|
|
85
|
+
const back = props.axis === "x" ? "ArrowLeft" : "ArrowUp";
|
|
86
|
+
let next: number;
|
|
87
|
+
if (ev.key === fwd) next = props.modelValue + s * grow;
|
|
88
|
+
else if (ev.key === back) next = props.modelValue - s * grow;
|
|
89
|
+
else if (ev.key === "Home") next = props.min;
|
|
90
|
+
else if (ev.key === "End" && finiteMax.value !== undefined) next = finiteMax.value;
|
|
91
|
+
else if (ev.key === "Enter") { ev.preventDefault(); reset(); return; }
|
|
92
|
+
else return;
|
|
93
|
+
ev.preventDefault();
|
|
94
|
+
set(next);
|
|
95
|
+
emit("end", clamp(next));
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<!-- APG window splitter: a focusable separator is interactive. -->
|
|
100
|
+
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
|
101
|
+
<template>
|
|
102
|
+
<div
|
|
103
|
+
class="wp-resize"
|
|
104
|
+
role="separator"
|
|
105
|
+
:tabindex="disabled ? -1 : 0"
|
|
106
|
+
:aria-label="label"
|
|
107
|
+
:aria-orientation="axis === 'x' ? 'vertical' : 'horizontal'"
|
|
108
|
+
:aria-valuenow="modelValue"
|
|
109
|
+
:aria-valuemin="min"
|
|
110
|
+
:aria-valuemax="finiteMax"
|
|
111
|
+
:aria-controls="controls"
|
|
112
|
+
:aria-disabled="disabled || undefined"
|
|
113
|
+
:data-axis="axis"
|
|
114
|
+
:data-dragging="dragging || undefined"
|
|
115
|
+
@pointerdown="onPointerDown"
|
|
116
|
+
@pointermove="onPointerMove"
|
|
117
|
+
@pointerup="onPointerUp"
|
|
118
|
+
@pointercancel="onPointerUp"
|
|
119
|
+
@dblclick="reset"
|
|
120
|
+
@keydown="onKeydown"
|
|
121
|
+
/>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<style scoped>
|
|
125
|
+
.wp-resize {
|
|
126
|
+
position: relative; flex: none; z-index: 1; outline: none; touch-action: none;
|
|
127
|
+
}
|
|
128
|
+
.wp-resize[data-axis="x"] { width: 8px; margin: 0 -4px; cursor: col-resize; align-self: stretch; }
|
|
129
|
+
.wp-resize[data-axis="y"] { height: 8px; margin: -4px 0; cursor: row-resize; width: 100%; }
|
|
130
|
+
.wp-resize::after {
|
|
131
|
+
content: ""; position: absolute; border-radius: min(1px, var(--wp-radius-cap, 999px)); background: transparent;
|
|
132
|
+
transition: background var(--wp-motion-hover);
|
|
133
|
+
}
|
|
134
|
+
.wp-resize[data-axis="x"]::after { top: 0; bottom: 0; left: 3px; width: 2px; }
|
|
135
|
+
.wp-resize[data-axis="y"]::after { left: 0; right: 0; top: 3px; height: 2px; }
|
|
136
|
+
.wp-resize:hover::after, .wp-resize[data-dragging]::after { background: var(--wp-accent); }
|
|
137
|
+
.wp-resize:focus-visible::after { background: var(--wp-border-focus); box-shadow: 0 0 0 1px var(--wp-border-focus); }
|
|
138
|
+
.wp-resize[aria-disabled="true"] { cursor: default; }
|
|
139
|
+
.wp-resize[aria-disabled="true"]::after { background: transparent; }
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
.wp-resize::after { transition: none; }
|
|
142
|
+
}
|
|
143
|
+
</style>
|