@dumiflex/wp-kit 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +5 -0
  3. package/package.json +4 -3
  4. package/src/Avatar.vue +1 -1
  5. package/src/Banner.vue +4 -4
  6. package/src/Button.vue +73 -44
  7. package/src/Checkbox.vue +201 -63
  8. package/src/Chip.vue +5 -2
  9. package/src/CodeBlock.vue +33 -6
  10. package/src/ColorField.vue +60 -6
  11. package/src/Combobox.vue +10 -14
  12. package/src/ConfirmModal.vue +12 -22
  13. package/src/ContextMenu.vue +11 -30
  14. package/src/CopyButton.vue +1 -1
  15. package/src/DataTable.vue +3 -4
  16. package/src/DatePicker.vue +1 -1
  17. package/src/Disclosure.vue +2 -1
  18. package/src/Drawer.vue +2 -2
  19. package/src/DropZone.vue +2 -2
  20. package/src/HsvPicker.vue +2 -2
  21. package/src/IconPicker.vue +1 -1
  22. package/src/ImageGrid.vue +17 -15
  23. package/src/Lightbox.vue +6 -3
  24. package/src/LoadingMark.vue +93 -51
  25. package/src/Modal.vue +109 -44
  26. package/src/ModalShell.vue +26 -5
  27. package/src/Pagination.vue +4 -4
  28. package/src/Popover.vue +68 -12
  29. package/src/RadioGroup.vue +5 -4
  30. package/src/SearchInput.vue +1 -1
  31. package/src/Select.vue +53 -158
  32. package/src/Skeleton.vue +21 -5
  33. package/src/Slider.vue +166 -55
  34. package/src/SortableList.vue +1 -1
  35. package/src/TagInput.vue +1 -1
  36. package/src/Timeline.vue +2 -2
  37. package/src/WorkingLine.vue +320 -0
  38. package/src/WpIconButton.vue +3 -2
  39. package/src/WpImageZoom.vue +7 -7
  40. package/src/WpSegmented.vue +3 -1
  41. package/src/WpSwitch.vue +1 -1
  42. package/src/WpTabs.vue +1 -1
  43. package/src/_modal-motion.css +1 -1
  44. package/src/button.css +8 -6
  45. package/src/floating.ts +5 -0
  46. package/src/highlight.ts +199 -0
  47. package/src/hsv.ts +1 -1
  48. package/src/loading-mark-motion.ts +249 -0
  49. package/src/menu.css +51 -0
  50. package/src/motion.css +73 -0
  51. package/src/slider-math.ts +45 -1
  52. package/src/theme/looks.css +6 -19
  53. package/src/theme/tokens.css +12 -1108
  54. package/src/tokens.ts +8 -3
  55. package/src/tooltip.css +8 -5
  56. package/src/useDialogFocus.ts +11 -2
  57. package/src/CloseButton.vue +0 -69
  58. package/src/ColorPicker.vue +0 -201
  59. package/src/ConfirmDialog.vue +0 -165
  60. package/src/RangeSlider.vue +0 -183
  61. package/src/Toggle.vue +0 -36
  62. package/src/WpButton.vue +0 -75
  63. package/src/WpCheck.vue +0 -104
  64. package/src/_skeleton.css +0 -50
package/src/Slider.vue CHANGED
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T extends number | [number, number]">
2
2
  /**
3
3
  * Slider: a number picked by dragging along a range (a weight, a threshold,
4
4
  * a size). Use it when the feel of "more or less" matters more than the
@@ -8,92 +8,190 @@
8
8
  * <Slider v-model="weight" :min="0" :max="2" :step="0.05" aria-label="Weight" with-input />
9
9
  * <Slider v-model="size" :min="4" :max="160" :marks="[{ value: 32, label: '32' }]" :format="(v) => `${v} px`" />
10
10
  *
11
- * APG slider: the thumb is the focus stop; arrows move one step, PageUp /
12
- * PageDown ten, Home / End jump to the ends, and `format` gives screen
13
- * readers the value as words ("32 px"). Clicking the track jumps there and
14
- * the drag carries on from it.
11
+ * Give it a pair (`v-model="range"` with `[low, high]`) for a low and a high
12
+ * number on one track (a weight range, a token budget, a sweep's from–to):
13
+ * two thumbs that can't cross, kept at least `min-gap` apart, and with
14
+ * `with-input` a WpNumber for each end. A click on the track moves the
15
+ * nearer thumb.
16
+ *
17
+ * <Slider v-model="range" :min="0" :max="2" :step="0.1" :min-gap="0.2" aria-label="Weight" with-input />
18
+ *
19
+ * `snap` settles the thumb on the marks. `snap` (true) makes the marks the only
20
+ * values: a drag, a click or a typed number lands on the nearest mark and
21
+ * the arrows go mark to mark. A number pulls the thumb onto a mark only once
22
+ * the pointer comes within that distance of one, and moves it by `step`
23
+ * elsewhere (keys and typing stay exact, so a thumb can always leave a mark).
24
+ *
25
+ * <Slider v-model="cfg" :min="1" :max="30" :marks="[{ value: 7, label: '7' }]" :snap="1" />
26
+ *
27
+ * APG slider: each thumb is a focus stop; arrows move one step, PageUp /
28
+ * PageDown ten, Home / End jump to the ends (for a pair, the ends that thumb
29
+ * can reach), and `format` gives screen readers the value as words ("32 px").
30
+ * A pair's thumbs are "<label>, low end" / "high end", their min and max the
31
+ * other thumb, so screen readers hear the real limits. Clicking the track
32
+ * jumps there and the drag carries on from it.
15
33
  */
16
34
  import { computed, ref } from "vue";
17
- import { keyStep, percentOf, snap, valueAt } from "./slider-math";
35
+ import { keyStep, keyToMark, percentOf, snap, snapToMarks, valueAt } from "./slider-math";
18
36
  import WpNumber from "./WpNumber.vue";
19
37
 
20
38
  const props = withDefaults(defineProps<{
21
- modelValue: number;
39
+ modelValue: T;
22
40
  min?: number | undefined;
23
41
  max?: number | undefined;
24
42
  step?: number | undefined;
43
+ /** A pair only: the smallest distance between the two ends. */
44
+ minGap?: number | undefined;
25
45
  size?: "sm" | "md" | "lg" | undefined;
26
46
  disabled?: boolean | undefined;
27
47
  ariaLabel?: string | undefined;
28
48
  /** Labelled ticks under the track. */
29
49
  marks?: { value: number; label?: string }[] | undefined;
50
+ /**
51
+ * Settle on the marks: true allows only mark values (drag, click, typing,
52
+ * arrows mark to mark); a number pulls the dragged thumb onto a mark within
53
+ * that distance of it and leaves it free elsewhere.
54
+ */
55
+ snap?: boolean | number | undefined;
30
56
  /** How the value reads (the tooltip-free label and screen readers). */
31
57
  format?: ((v: number) => string) | undefined;
32
- /** Show the value after the track. */
58
+ /** Show the value (a pair: "low – high") after the track. */
33
59
  showValue?: boolean | undefined;
34
- /** Put a WpNumber after the track to type the exact value. */
60
+ /** Put a WpNumber after the track (a pair: one at each side) to type the exact value. */
35
61
  withInput?: boolean | undefined;
36
- }>(), { min: 0, max: 100, step: 1, size: "md", ariaLabel: undefined, marks: undefined });
62
+ }>(), { min: 0, max: 100, step: 1, minGap: 0, size: "md", ariaLabel: undefined, marks: undefined });
37
63
 
38
- const emit = defineEmits<{ (e: "update:modelValue", v: number): void; (e: "change", v: number): void }>();
64
+ const emit = defineEmits<{ (e: "update:modelValue", v: T): void; (e: "change", v: T): void }>();
39
65
 
40
66
  const trackEl = ref<HTMLElement | null>(null);
41
- const dragging = ref(false);
67
+ /** The thumb being dragged (0 = the only one, or a pair's low end). */
68
+ const active = ref<0 | 1 | null>(null);
42
69
 
43
- const value = computed(() => snap(props.modelValue, props.min, props.max, props.step));
44
- const pct = computed(() => percentOf(value.value, props.min, props.max));
45
- const text = computed(() => (props.format ? props.format(value.value) : String(value.value)));
70
+ const isPair = computed(() => Array.isArray(props.modelValue));
71
+ const sn = (v: number): number => snap(v, props.min, props.max, props.step);
72
+ /** The marks' values inside the range, for `snap`. */
73
+ const markValues = computed(() => (props.marks ?? []).map((m) => m.value).filter((v) => v >= props.min && v <= props.max));
74
+ /** Only marks can be picked (`snap` true with marks). */
75
+ const marksOnly = computed(() => props.snap === true && markValues.value.length > 0);
76
+ const toMark = (v: number): number => snapToMarks(v, markValues.value, props.snap);
46
77
 
47
- function set(v: number): void {
48
- if (props.disabled) return;
49
- if (v !== props.modelValue) emit("update:modelValue", v);
78
+ /** The thumbs' values: one, or a pair sorted low to high. */
79
+ const ends = computed<number[]>(() => {
80
+ const m = props.modelValue;
81
+ if (typeof m === "number") return [sn(m)];
82
+ return [sn(Math.min(m[0], m[1])), sn(Math.max(m[0], m[1]))];
83
+ });
84
+ const lo = computed(() => ends.value[0] ?? props.min);
85
+ const hi = computed(() => ends.value[ends.value.length - 1] ?? props.max);
86
+ const pcts = computed(() => ends.value.map((v) => percentOf(v, props.min, props.max)));
87
+ const loPct = computed(() => pcts.value[0] ?? 0);
88
+ const hiPct = computed(() => pcts.value[pcts.value.length - 1] ?? 0);
89
+
90
+ const text = (v: number): string => (props.format ? props.format(v) : String(v));
91
+ const label = computed(() => props.ariaLabel ?? (isPair.value ? "Range" : undefined));
92
+
93
+ /** The lowest and highest value thumb `i` can take. */
94
+ function limits(i: number): [number, number] {
95
+ if (!isPair.value) return [props.min, props.max];
96
+ return i === 0 ? [props.min, hi.value - props.minGap] : [lo.value + props.minGap, props.max];
97
+ }
98
+
99
+ function thumbLabel(i: number): string | undefined {
100
+ if (!isPair.value) return label.value;
101
+ return `${label.value ?? "Range"}, ${i === 0 ? "low" : "high"} end`;
102
+ }
103
+
104
+ function asModel(vals: number[]): T {
105
+ const out = isPair.value ? [vals[0] ?? lo.value, vals[1] ?? hi.value] : (vals[0] ?? lo.value);
106
+ return out as T;
107
+ }
108
+
109
+ /** Put thumb `i` at `v` (a pair's thumb kept inside the other end minus the gap); returns the new model. */
110
+ function setEnd(i: 0 | 1, v: number): T {
111
+ let vals: number[];
112
+ if (!isPair.value) vals = [v];
113
+ else if (i === 0) vals = [Math.min(v, sn(hi.value - props.minGap)), hi.value];
114
+ else vals = [lo.value, Math.max(v, sn(lo.value + props.minGap))];
115
+ const next = asModel(vals);
116
+ if (!props.disabled && JSON.stringify(next) !== JSON.stringify(props.modelValue)) emit("update:modelValue", next);
117
+ return next;
118
+ }
119
+
120
+ function current(): T {
121
+ return asModel(ends.value);
50
122
  }
51
123
 
52
124
  function fromPointer(ev: PointerEvent): number {
53
125
  const r = trackEl.value?.getBoundingClientRect();
54
- if (!r) return value.value;
55
- return valueAt(ev.clientX, r.left, r.width, props.min, props.max, props.step);
126
+ return r ? toMark(valueAt(ev.clientX, r.left, r.width, props.min, props.max, props.step)) : lo.value;
56
127
  }
57
128
 
58
129
  function onPointerDown(ev: PointerEvent): void {
59
130
  if (props.disabled || ev.button !== 0) return;
60
131
  ev.preventDefault();
61
- dragging.value = true;
132
+ const v = fromPointer(ev);
133
+ const i: 0 | 1 = isPair.value && (Math.abs(v - lo.value) > Math.abs(v - hi.value) || v > hi.value) ? 1 : 0;
134
+ active.value = i;
62
135
  (ev.currentTarget as HTMLElement).setPointerCapture?.(ev.pointerId);
63
- set(fromPointer(ev));
64
- (trackEl.value?.querySelector("[role=slider]") as HTMLElement | null)?.focus();
136
+ setEnd(i, v);
137
+ trackEl.value?.querySelectorAll<HTMLElement>("[role=slider]")[i]?.focus();
65
138
  }
66
139
 
67
140
  function onPointerMove(ev: PointerEvent): void {
68
- if (dragging.value) set(fromPointer(ev));
141
+ if (active.value !== null) setEnd(active.value, fromPointer(ev));
69
142
  }
70
143
 
71
144
  function onPointerUp(ev: PointerEvent): void {
72
- if (!dragging.value) return;
73
- dragging.value = false;
145
+ if (active.value === null) return;
146
+ active.value = null;
74
147
  (ev.currentTarget as HTMLElement).releasePointerCapture?.(ev.pointerId);
75
- emit("change", value.value);
148
+ emit("change", current());
76
149
  }
77
150
 
78
- function onKeydown(ev: KeyboardEvent): void {
151
+ function onKeydown(ev: KeyboardEvent, i: 0 | 1): void {
79
152
  if (props.disabled) return;
80
- const next = keyStep(ev.key, value.value, props.min, props.max, props.step);
153
+ const now = i === 0 ? lo.value : hi.value;
154
+ const next = marksOnly.value
155
+ ? keyToMark(ev.key, now, markValues.value)
156
+ : keyStep(ev.key, now, props.min, props.max, props.step);
81
157
  if (next === null) return;
82
158
  ev.preventDefault();
83
- set(next);
84
- emit("change", next);
159
+ const [low, high] = limits(i);
160
+ const v = marksOnly.value ? next : ev.key === "Home" ? low : ev.key === "End" ? high : next;
161
+ emit("change", setEnd(i, v));
162
+ }
163
+
164
+ function onTyped(i: 0 | 1, v: number | null): void {
165
+ if (v === null || props.disabled) return;
166
+ emit("change", setEnd(i, marksOnly.value ? toMark(sn(v)) : sn(v)));
85
167
  }
86
168
 
87
- function onTyped(v: number | null): void {
88
- if (v === null) return;
89
- const next = snap(v, props.min, props.max, props.step);
90
- set(next);
91
- emit("change", next);
169
+ function passed(v: number): boolean {
170
+ return isPair.value ? v >= lo.value && v <= hi.value : v <= lo.value;
92
171
  }
93
172
  </script>
94
173
 
95
174
  <template>
96
- <div class="wp-slider" :data-size="size" :data-disabled="disabled || undefined" :data-dragging="dragging || undefined" :data-marked="marks?.length ? 'true' : undefined">
175
+ <div
176
+ class="wp-slider"
177
+ :data-size="size"
178
+ :data-pair="isPair || undefined"
179
+ :data-disabled="disabled || undefined"
180
+ :data-dragging="active !== null || undefined"
181
+ :data-marked="marks?.length ? 'true' : undefined"
182
+ >
183
+ <WpNumber
184
+ v-if="withInput && isPair"
185
+ class="wp-slider__input"
186
+ :model-value="lo"
187
+ :min="min"
188
+ :max="hi - props.minGap"
189
+ :step="step"
190
+ :size="size"
191
+ :disabled="disabled"
192
+ :aria-label="thumbLabel(0)"
193
+ @update:model-value="onTyped(0, $event)"
194
+ />
97
195
  <div
98
196
  ref="trackEl"
99
197
  class="wp-slider__track"
@@ -103,43 +201,51 @@ function onTyped(v: number | null): void {
103
201
  @pointercancel="onPointerUp"
104
202
  >
105
203
  <div class="wp-slider__rail" />
106
- <div class="wp-slider__fill" :style="{ width: `calc(${pct}% + var(--_thumb) / 2)` }" />
204
+ <div
205
+ class="wp-slider__fill"
206
+ :style="isPair
207
+ ? { left: `${loPct}%`, width: `${hiPct - loPct}%` }
208
+ : { width: `calc(${loPct}% + var(--_thumb) / 2)` }"
209
+ />
107
210
  <span
108
211
  v-for="m in marks ?? []"
109
212
  :key="m.value"
110
213
  class="wp-slider__mark"
111
- :data-passed="m.value <= value || undefined"
214
+ :data-passed="passed(m.value) || undefined"
112
215
  :style="{ left: `${percentOf(m.value, props.min, props.max)}%` }"
113
216
  aria-hidden="true"
114
217
  ><span v-if="m.label" class="wp-slider__mark-label">{{ m.label }}</span></span>
115
218
  <!-- Focusable unless disabled (tabindex is bound). -->
116
219
  <!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
117
220
  <div
221
+ v-for="(v, i) in ends"
222
+ :key="i"
118
223
  class="wp-slider__thumb"
119
224
  role="slider"
120
225
  :tabindex="disabled ? -1 : 0"
121
- :aria-label="ariaLabel"
122
- :aria-valuemin="min"
123
- :aria-valuemax="max"
124
- :aria-valuenow="value"
125
- :aria-valuetext="format ? text : undefined"
226
+ :aria-label="thumbLabel(i)"
227
+ :aria-valuemin="limits(i)[0]"
228
+ :aria-valuemax="limits(i)[1]"
229
+ :aria-valuenow="v"
230
+ :aria-valuetext="format ? text(v) : undefined"
126
231
  :aria-disabled="disabled || undefined"
127
- :style="{ left: `${pct}%` }"
128
- @keydown="onKeydown"
232
+ :data-active="active === i || undefined"
233
+ :style="{ left: `${pcts[i]}%` }"
234
+ @keydown="onKeydown($event, i === 1 ? 1 : 0)"
129
235
  />
130
236
  </div>
131
- <span v-if="showValue && !withInput" class="wp-slider__value" aria-hidden="true">{{ text }}</span>
237
+ <span v-if="showValue && !withInput" class="wp-slider__value" aria-hidden="true">{{ isPair ? `${text(lo)} – ${text(hi)}` : text(lo) }}</span>
132
238
  <WpNumber
133
239
  v-if="withInput"
134
240
  class="wp-slider__input"
135
- :model-value="value"
136
- :min="min"
241
+ :model-value="isPair ? hi : lo"
242
+ :min="isPair ? lo + props.minGap : min"
137
243
  :max="max"
138
244
  :step="step"
139
245
  :size="size"
140
246
  :disabled="disabled"
141
- :aria-label="ariaLabel ? `${ariaLabel}, exact value` : 'Exact value'"
142
- @update:model-value="onTyped"
247
+ :aria-label="isPair ? thumbLabel(1) : (ariaLabel ? `${ariaLabel}, exact value` : 'Exact value')"
248
+ @update:model-value="onTyped(isPair ? 1 : 0, $event)"
143
249
  />
144
250
  </div>
145
251
  </template>
@@ -158,10 +264,11 @@ function onTyped(v: number | null): void {
158
264
  position: relative; flex: 1; min-width: 60px; height: var(--_h);
159
265
  margin: 0 calc(var(--_thumb) / 2); cursor: pointer; touch-action: none;
160
266
  }
267
+ .wp-slider[data-pair] .wp-slider__track { min-width: 80px; }
161
268
  .wp-slider__rail, .wp-slider__fill {
162
269
  position: absolute; top: 50%; left: 0; height: 4px; margin-top: -2px; border-radius: var(--wp-radius-cap, 999px);
163
270
  }
164
- .wp-slider__rail { right: 0; 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); }
271
+ .wp-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); }
165
272
  .wp-slider__fill { left: calc(var(--_thumb) / -2); background: var(--wp-accent); }
166
273
  .wp-slider[data-size="sm"] .wp-slider__rail, .wp-slider[data-size="sm"] .wp-slider__fill { height: 3px; margin-top: -1.5px; }
167
274
  .wp-slider__thumb {
@@ -171,8 +278,10 @@ function onTyped(v: number | null): void {
171
278
  box-shadow: var(--wp-shadow-sm); outline: none;
172
279
  transition: transform var(--wp-motion-quick), box-shadow var(--wp-motion-quick);
173
280
  }
174
- .wp-slider__track:hover .wp-slider__thumb, .wp-slider[data-dragging] .wp-slider__thumb { transform: scale(1.12); }
175
- .wp-slider__thumb:focus-visible { box-shadow: var(--wp-focus-ring); }
281
+ .wp-slider:not([data-pair]) .wp-slider__track:hover .wp-slider__thumb,
282
+ .wp-slider[data-pair] .wp-slider__thumb:hover,
283
+ .wp-slider__thumb[data-active] { transform: scale(1.12); }
284
+ .wp-slider__thumb:focus-visible { box-shadow: var(--wp-focus-ring); z-index: 1; }
176
285
  .wp-slider__mark {
177
286
  position: absolute; top: 50%; width: 2px; height: 8px; margin: -4px 0 0 -1px; border-radius: min(1px, var(--wp-radius-cap, 999px));
178
287
  background: var(--wp-border-strong); pointer-events: none;
@@ -184,11 +293,13 @@ function onTyped(v: number | null): void {
184
293
  }
185
294
  .wp-slider__value {
186
295
  flex: none; min-width: 3ch; text-align: right; color: var(--wp-text); font: 500 var(--wp-text-sm)/1 var(--wp-font-sans);
187
- font-variant-numeric: tabular-nums;
296
+ font-variant-numeric: tabular-nums; white-space: nowrap;
188
297
  }
189
298
  .wp-slider[data-size="sm"] .wp-slider__value { font-size: var(--wp-text-xs); }
190
299
  .wp-slider__input { flex: none; width: 84px; }
300
+ .wp-slider[data-pair] .wp-slider__input { width: 76px; }
191
301
  .wp-slider[data-size="sm"] .wp-slider__input { width: 64px; }
302
+ .wp-slider[data-pair][data-size="sm"] .wp-slider__input { width: 60px; }
192
303
  .wp-slider[data-disabled] { opacity: 0.45; }
193
304
  .wp-slider[data-disabled] .wp-slider__track { cursor: not-allowed; }
194
305
  .wp-slider[data-disabled] .wp-slider__thumb { transform: none; }
@@ -241,7 +241,7 @@ function onGripBlur(i: number): void {
241
241
  background: transparent; color: var(--wp-text-dim); cursor: grab; touch-action: none;
242
242
  }
243
243
  .wp-sortable[data-size="sm"] .wp-sortable__grip { width: 20px; min-height: 20px; }
244
- .wp-sortable__grip .pi { font-size: 11px; }
244
+ .wp-sortable__grip .pi { font-size: 12px; }
245
245
  .wp-sortable[data-size="sm"] .wp-sortable__grip .pi { font-size: 10px; }
246
246
  .wp-sortable__grip:hover:not(:disabled) { color: var(--wp-text); background: var(--wp-row-hover); }
247
247
  .wp-sortable__grip:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
package/src/TagInput.vue CHANGED
@@ -263,7 +263,7 @@ function setOpen(v: boolean): void {
263
263
  color: var(--wp-text); font-size: var(--wp-text-xs); line-height: 1;
264
264
  }
265
265
  .wp-tag-input[data-size="sm"] .wp-tag-input__chip { height: 16px; padding-left: 5px; font-size: 10.5px; }
266
- .wp-tag-input__chip-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
266
+ .wp-tag-input__chip-text { min-width: 0; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
267
267
  /* Plain ✕: no background zone, the chip keeps its height; the hit area is 20 px. */
268
268
  .wp-tag-input__remove {
269
269
  display: inline-grid; place-items: center; width: 16px; height: 16px; margin: -2px 0; padding: 0;
package/src/Timeline.vue CHANGED
@@ -71,13 +71,13 @@ withDefaults(defineProps<{
71
71
  }
72
72
  .wp-timeline__dot {
73
73
  position: relative; display: grid; place-items: center; flex: none; box-sizing: border-box;
74
- width: var(--_dot); height: var(--_dot); margin-top: 1px; border-radius: 50%; /* round: timeline dot */
74
+ width: var(--_dot); height: var(--_dot); margin-top: 1px; border-radius: min(50%, var(--wp-radius-cap, 999px));
75
75
  border: 2px solid var(--_tone); background: var(--wp-bg-1); color: var(--_tone);
76
76
  }
77
77
  .wp-timeline__item:not([data-icon]) .wp-timeline__dot { transform: scale(0.6); background: var(--_tone); }
78
78
  .wp-timeline__dot .pi { font-size: calc(var(--_dot) * 0.45); }
79
79
  .wp-timeline__item[data-pending] .wp-timeline__dot::after {
80
- content: ""; position: absolute; inset: -4px; border-radius: 50%; /* round: timeline dot ring */ border: 2px solid var(--_tone);
80
+ content: ""; position: absolute; inset: -4px; border-radius: min(50%, var(--wp-radius-cap, 999px)); border: 2px solid var(--_tone);
81
81
  animation: wp-timeline-pulse 1.6s ease-out infinite;
82
82
  }
83
83
  @keyframes wp-timeline-pulse { from { opacity: 0.7; transform: scale(0.8); } to { opacity: 0; transform: scale(1.4); } }