@dumiflex/wp-kit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +32 -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 +1 -1
  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 +0 -2
  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 +2 -2
  23. package/src/Lightbox.vue +2 -2
  24. package/src/Modal.vue +109 -44
  25. package/src/ModalShell.vue +26 -5
  26. package/src/Pagination.vue +4 -4
  27. package/src/Popover.vue +68 -12
  28. package/src/RadioGroup.vue +5 -4
  29. package/src/SearchInput.vue +1 -1
  30. package/src/Select.vue +53 -158
  31. package/src/Skeleton.vue +1 -1
  32. package/src/Slider.vue +166 -55
  33. package/src/SortableList.vue +1 -1
  34. package/src/TagInput.vue +1 -1
  35. package/src/Timeline.vue +2 -2
  36. package/src/WorkingLine.vue +320 -0
  37. package/src/WpIconButton.vue +3 -2
  38. package/src/WpImageZoom.vue +7 -7
  39. package/src/WpSegmented.vue +3 -1
  40. package/src/WpSwitch.vue +1 -1
  41. package/src/WpTabs.vue +1 -1
  42. package/src/_modal-motion.css +1 -1
  43. package/src/button.css +8 -6
  44. package/src/floating.ts +5 -0
  45. package/src/hsv.ts +1 -1
  46. package/src/menu.css +51 -0
  47. package/src/motion.css +73 -0
  48. package/src/slider-math.ts +45 -1
  49. package/src/theme/looks.css +6 -19
  50. package/src/theme/tokens.css +12 -1108
  51. package/src/tokens.ts +8 -3
  52. package/src/tooltip.css +8 -5
  53. package/src/useDialogFocus.ts +11 -2
  54. package/src/CloseButton.vue +0 -69
  55. package/src/ColorPicker.vue +0 -201
  56. package/src/ConfirmDialog.vue +0 -165
  57. package/src/RangeSlider.vue +0 -183
  58. package/src/Toggle.vue +0 -36
  59. package/src/WpButton.vue +0 -75
  60. package/src/WpCheck.vue +0 -104
  61. package/src/_skeleton.css +0 -50
@@ -8,14 +8,20 @@
8
8
  * so it follows the swatch on scroll and closes on Escape or a click
9
9
  * outside. Works on the canvas (`size="sm"`) and in the manager.
10
10
  *
11
+ * `presets` adds a row of preset swatches under the square (`true` for the
12
+ * kit's COLOUR_PRESETS, or a list of hex colours); a click takes one and
13
+ * closes. `compact` shows the swatch alone, for a tight spot (a name row).
14
+ *
11
15
  * <ColorField v-model="labelColor" alpha allow-none size="sm" aria-label="Label band" />
16
+ * <ColorField v-model="color" presets size="md" aria-label="Category colour" />
12
17
  */
13
18
  import { computed, ref, watch } from "vue";
14
19
  import HsvPicker from "./HsvPicker.vue";
15
20
  import Popover from "./Popover.vue";
16
21
  import WpNumber from "./WpNumber.vue";
17
- import WpButton from "./WpButton.vue";
22
+ import Button from "./Button.vue";
18
23
  import { rgbToHex } from "./hsv";
24
+ import { COLOUR_PRESETS } from "./colour-presets";
19
25
 
20
26
  const props = withDefaults(defineProps<{
21
27
  modelValue: string;
@@ -23,11 +29,15 @@ const props = withDefaults(defineProps<{
23
29
  alpha?: boolean | undefined;
24
30
  /** Offer "None" (writes `none`). */
25
31
  allowNone?: boolean | undefined;
32
+ /** Preset swatches: `true` for COLOUR_PRESETS, or your own hex list. */
33
+ presets?: readonly string[] | boolean | undefined;
34
+ /** The swatch alone, without the value beside it. */
35
+ compact?: boolean | undefined;
26
36
  size?: "sm" | "md" | undefined;
27
37
  ariaLabel?: string | undefined;
28
38
  disabled?: boolean | undefined;
29
39
  dataTest?: string | undefined;
30
- }>(), { alpha: false, allowNone: false, size: "sm", ariaLabel: "Colour", disabled: false, dataTest: undefined });
40
+ }>(), { alpha: false, allowNone: false, presets: false, compact: false, size: "sm", ariaLabel: "Colour", disabled: false, dataTest: undefined });
31
41
 
32
42
  const emit = defineEmits<{ "update:modelValue": [value: string] }>();
33
43
 
@@ -79,6 +89,17 @@ function onDraft(ev: Event): void {
79
89
  const p = parse(v);
80
90
  if (p) emit("update:modelValue", compose(p.rgb, props.alpha ? p.alpha : 255));
81
91
  }
92
+ const presetList = computed<readonly string[]>(() => {
93
+ if (props.presets === true) return COLOUR_PRESETS;
94
+ return Array.isArray(props.presets) ? props.presets : [];
95
+ });
96
+ function isActivePreset(preset: string): boolean {
97
+ return !isNone.value && preset.trim().toLowerCase() === rgb.value && alphaValue.value === 255;
98
+ }
99
+ function pickPreset(preset: string): void {
100
+ emit("update:modelValue", preset);
101
+ open.value = false;
102
+ }
82
103
  function pickNone(): void {
83
104
  emit("update:modelValue", "none");
84
105
  open.value = false;
@@ -92,7 +113,7 @@ function pickNone(): void {
92
113
  v-bind="triggerProps"
93
114
  type="button"
94
115
  class="wp-color-field wp-field-box"
95
- :class="[`wp-color-field--${size}`, `wp-field-box--${size}`, { 'is-none': isNone }]"
116
+ :class="[`wp-color-field--${size}`, `wp-field-box--${size}`, { 'is-none': isNone, 'wp-color-field--compact': compact }]"
96
117
  :disabled="disabled"
97
118
  :aria-label="`${ariaLabel}: ${shown}`"
98
119
  :data-test="dataTest"
@@ -100,7 +121,7 @@ function pickNone(): void {
100
121
  <span class="wp-color-field__chip" aria-hidden="true">
101
122
  <span class="wp-color-field__fill" :style="{ background: swatch }" />
102
123
  </span>
103
- <span class="wp-color-field__value" aria-hidden="true">{{ shown }}</span>
124
+ <span v-if="!compact" class="wp-color-field__value" aria-hidden="true">{{ shown }}</span>
104
125
  </button>
105
126
  </template>
106
127
  <div class="wp-color-field__panel" @keydown.stop>
@@ -128,7 +149,21 @@ function pickNone(): void {
128
149
  @update:model-value="onOpacity"
129
150
  />
130
151
  </div>
131
- <WpButton
152
+ <div v-if="presetList.length" class="wp-color-field__presets" role="group" :aria-label="`${ariaLabel} presets`">
153
+ <button
154
+ v-for="preset in presetList"
155
+ :key="preset"
156
+ type="button"
157
+ class="wp-color-field__preset"
158
+ :style="{ background: preset }"
159
+ :title="preset"
160
+ :aria-pressed="isActivePreset(preset)"
161
+ :aria-label="`Use ${preset}`"
162
+ :data-test="dataTest ? `${dataTest}-preset` : undefined"
163
+ @click="pickPreset(preset)"
164
+ />
165
+ </div>
166
+ <Button
132
167
  v-if="allowNone"
133
168
  size="sm"
134
169
  variant="ghost"
@@ -136,7 +171,7 @@ function pickNone(): void {
136
171
  :pressed="isNone"
137
172
  :data-test="dataTest ? `${dataTest}-none` : undefined"
138
173
  @click="pickNone"
139
- >None</WpButton>
174
+ >None</Button>
140
175
  </div>
141
176
  </Popover>
142
177
  </template>
@@ -165,6 +200,8 @@ function pickNone(): void {
165
200
  /* Checks show through a see-through colour. */
166
201
  background: repeating-conic-gradient(var(--wp-bg-4) 0 25%, var(--wp-bg-1) 0 50%) 0 0 / 8px 8px;
167
202
  }
203
+ .wp-color-field--md .wp-color-field__chip { width: 20px; height: 20px; border-radius: min(4px, var(--wp-radius-cap, 999px)); }
204
+ .wp-color-field--compact { padding: 0 3px; }
168
205
  .wp-color-field__fill { position: absolute; inset: 0; }
169
206
  .wp-color-field.is-none .wp-color-field__chip::after {
170
207
  content: "";
@@ -185,4 +222,21 @@ function pickNone(): void {
185
222
  .wp-color-field__row { display: flex; gap: 6px; align-items: center; }
186
223
  .wp-color-field__hex { flex: 1; min-width: 0; height: var(--wp-field-h); padding: 0 6px; font-family: var(--wp-font-mono, monospace); }
187
224
  .wp-color-field__alpha { width: 72px; flex: none; }
225
+ .wp-color-field__presets { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
226
+ .wp-color-field__preset {
227
+ width: 100%;
228
+ aspect-ratio: 1 / 1;
229
+ min-width: 0;
230
+ padding: 0;
231
+ border: 1px solid var(--wp-border);
232
+ border-radius: min(4px, var(--wp-radius-cap, 999px));
233
+ cursor: pointer;
234
+ transition: transform var(--wp-motion-quick), box-shadow var(--wp-motion-quick);
235
+ }
236
+ .wp-color-field__preset:hover { transform: scale(1.08); }
237
+ .wp-color-field__preset:focus-visible { outline: none; box-shadow: var(--wp-focus-ring); }
238
+ .wp-color-field__preset[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--wp-bg-1), 0 0 0 4px var(--wp-accent-500); }
239
+ @media (prefers-reduced-motion: reduce) {
240
+ .wp-color-field__preset { transition: none; }
241
+ }
188
242
  </style>
package/src/Combobox.vue CHANGED
@@ -175,7 +175,7 @@ function setOpen(v: boolean): void {
175
175
  <!-- Popover's autofocus prop is off here: focus stays in the box you type in. -->
176
176
  <!-- eslint-disable vuejs-accessibility/no-autofocus -->
177
177
  <template>
178
- <Popover :open="open" role="group" match-width :autofocus="false" :label="`${ariaLabel} choices`" @update:open="setOpen">
178
+ <Popover :open="open" variant="menu" role="group" match-width :max-height="280" :autofocus="false" :label="`${ariaLabel} choices`" @update:open="setOpen">
179
179
  <template #trigger>
180
180
  <!-- A click on the box's padding focuses the text field, which has the keys. -->
181
181
  <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
@@ -245,7 +245,7 @@ function setOpen(v: boolean): void {
245
245
  v-for="(o, i) in matches"
246
246
  :id="`${listId}-${i}`"
247
247
  :key="String(o.value)"
248
- class="wp-combo__opt"
248
+ class="wp-combo__opt wp-menu__item"
249
249
  role="option"
250
250
  :aria-selected="isPicked(o.value)"
251
251
  :aria-disabled="o.disabled || undefined"
@@ -265,6 +265,7 @@ function setOpen(v: boolean): void {
265
265
  </template>
266
266
 
267
267
  <style scoped src="./field.css"></style>
268
+ <style scoped src="./menu.css"></style>
268
269
 
269
270
  <style scoped>
270
271
  .wp-combo {
@@ -284,7 +285,7 @@ function setOpen(v: boolean): void {
284
285
  color: var(--wp-text); font-size: var(--wp-text-xs); line-height: 1;
285
286
  }
286
287
  .wp-combo[data-size="sm"] .wp-combo__chip { height: 16px; padding-left: 5px; font-size: 10.5px; }
287
- .wp-combo__chip-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
288
+ .wp-combo__chip-text { min-width: 0; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
288
289
  .wp-combo__remove, .wp-combo__clear {
289
290
  display: inline-grid; place-items: center; width: 16px; height: 16px; padding: 0;
290
291
  border: 0; border-radius: min(50%, var(--wp-radius-cap, 999px)); background: transparent; color: var(--wp-text-muted); cursor: pointer;
@@ -299,7 +300,7 @@ function setOpen(v: boolean): void {
299
300
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
300
301
  color: var(--wp-text-muted); font-size: 10px; pointer-events: none;
301
302
  }
302
- .wp-combo[data-size="sm"] .wp-combo__caret { right: 8px; font-size: 9px; }
303
+ .wp-combo[data-size="sm"] .wp-combo__caret { right: 8px; font-size: 10px; }
303
304
  .wp-combo__field {
304
305
  flex: 1 0 6ch; min-width: 6ch; height: calc(var(--_h) - 8px); margin: 0; padding: 0 2px;
305
306
  border: 0; outline: none; background: transparent; color: inherit; font: inherit;
@@ -311,20 +312,15 @@ function setOpen(v: boolean): void {
311
312
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
312
313
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
313
314
  }
314
- .wp-combo__list { margin: calc(var(--wp-space-2) * -1); padding: 0; list-style: none; max-height: 280px; overflow: auto; }
315
- .wp-combo__opt {
316
- display: flex; align-items: flex-start; gap: var(--wp-space-2);
317
- padding: 6px var(--wp-space-4) 6px var(--wp-space-2); border-radius: var(--wp-radius-sm);
318
- color: var(--wp-text); font-size: var(--wp-text-sm); cursor: pointer;
319
- }
320
- .wp-combo__opt:hover, .wp-combo__opt[data-active] { background: var(--wp-row-hover); }
321
- .wp-combo__opt[data-active] { box-shadow: inset 2px 0 0 var(--wp-accent); }
322
- .wp-combo__opt[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
315
+ /* The panel and the rows' padding, corners, text and states are the shared
316
+ * menu look (Popover variant="menu" + menu.css); the list scrolls in the panel. */
317
+ .wp-combo__list { margin: 0; padding: 0; list-style: none; }
318
+ .wp-combo__opt { display: flex; align-items: flex-start; gap: var(--wp-space-2); }
323
319
  .wp-combo__tick { width: 14px; margin-top: 2px; font-size: 10px; color: var(--wp-accent); visibility: hidden; }
324
320
  .wp-combo__tick[data-on] { visibility: visible; }
325
321
  .wp-combo__opt-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
326
322
  .wp-combo__hint { color: var(--wp-text-muted); font-size: var(--wp-text-xs); }
327
- .wp-combo__empty { padding: 6px var(--wp-space-4); color: var(--wp-text-muted); font-size: var(--wp-text-sm); }
323
+ .wp-combo__empty { padding: 6px 8px; color: var(--wp-text-muted); font-size: 12px; }
328
324
  @media (prefers-reduced-motion: reduce) {
329
325
  .wp-combo { transition: none; }
330
326
  }
@@ -2,22 +2,23 @@
2
2
  /**
3
3
  * Ask before an action, in the manager's own dialog.
4
4
  *
5
- * A thin wrapper on Modal: the same backdrop, focus trap, Escape and focus
6
- * return as every other manager dialog, with a Cancel button and a confirm
7
- * button that names the action. The canvas keeps `ConfirmDialog`, which
8
- * wears the node colours; this one wears the manager's.
5
+ * Built on Modal: the same backdrop, focus trap, Escape and focus return as
6
+ * every other dialog, with a Cancel button and a confirm button that names
7
+ * the action. One confirm for the manager, the canvas and the community site
8
+ * (ConfirmDialog was folded in: `visible` → `v-model:open`/`open`,
9
+ * `variant="danger"` → `tone="danger"`); it wears the colours of its side.
9
10
  *
10
11
  * Escape, the backdrop, the ✕ and Cancel all emit `cancel`. Enter confirms
11
12
  * unless focus is on a button or a text box (which then gets the key itself).
12
- * The `confirm-*` data-test names match ConfirmDialog's, so a test can drive
13
- * either.
13
+ * Data-test names: `confirm-title`, `confirm-body`, `confirm-cancel`,
14
+ * `confirm-confirm`.
14
15
  *
15
16
  * `requireType` makes the user type that exact text before Confirm works
16
17
  * (deleting an account); `confirmDisabled` holds Confirm off for a gate the
17
18
  * page owns (a required reason in the slot). `warn` asks about something
18
19
  * that can be undone but matters: an outlined button and an info mark.
19
20
  */
20
- import { computed, onBeforeUnmount, ref, watch } from "vue";
21
+ import { computed, ref, watch } from "vue";
21
22
  import Modal from "./Modal.vue";
22
23
  import Button from "./Button.vue";
23
24
  import Icon from "./Icon.vue";
@@ -70,26 +71,15 @@ function onKeydown(ev: KeyboardEvent): void {
70
71
  emit("confirm");
71
72
  }
72
73
 
73
- watch(
74
- () => props.open,
75
- (open) => {
76
- if (open) {
77
- typed.value = "";
78
- window.addEventListener("keydown", onKeydown);
79
- }
80
- else window.removeEventListener("keydown", onKeydown);
81
- },
82
- { immediate: true },
83
- );
84
- onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
74
+ watch(() => props.open, (open) => { if (open) typed.value = ""; }, { immediate: true });
85
75
  </script>
86
76
 
87
77
  <template>
88
- <Modal :open="open" size="sm" @update:open="onOpen">
78
+ <Modal :open="open" size="sm" @update:open="onOpen" @keydown="onKeydown">
89
79
  <template #header="{ titleId }">
90
80
  <span class="wp-confirm-modal__head" :class="`wp-confirm-modal__head--${tone}`">
91
81
  <Icon :name="tone === 'danger' ? 'exclamation-triangle' : 'info-circle'" />
92
- <h3 :id="titleId" class="wp-modal__title" data-test="confirm-title">{{ title }}</h3>
82
+ <h3 :id="titleId" class="wp-confirm-modal__title" data-test="confirm-title">{{ title }}</h3>
93
83
  </span>
94
84
  </template>
95
85
  <div class="wp-confirm-modal__body">
@@ -123,7 +113,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
123
113
  }
124
114
  .wp-confirm-modal__head--danger { color: var(--wp-danger); }
125
115
  .wp-confirm-modal__head--warn { color: var(--wp-warn); }
126
- .wp-confirm-modal__head .wp-modal__title { color: var(--wp-text); }
116
+ .wp-confirm-modal__title { margin: 0; font-size: var(--wp-text-md); font-weight: 600; color: var(--wp-text); }
127
117
  .wp-confirm-modal__text {
128
118
  margin: 0;
129
119
  color: var(--wp-text-muted);
@@ -204,7 +204,7 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
204
204
  <ul
205
205
  v-if="visible"
206
206
  ref="menuEl"
207
- class="wp-ctxmenu"
207
+ class="wp-ctxmenu wp-menu"
208
208
  :style="{ left: `${pos.x}px`, top: `${pos.y}px` }"
209
209
  role="menu"
210
210
  @click.stop
@@ -235,7 +235,8 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
235
235
  can't see it); keys come from the menu's own keydown. -->
236
236
  <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions, vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/mouse-events-have-key-events -->
237
237
  <li
238
- class="wp-ctxmenu__item"
238
+ class="wp-ctxmenu__item wp-menu__item"
239
+ :data-active="activeIndex === i || undefined"
239
240
  :class="{
240
241
  'wp-ctxmenu__item--danger': entry.danger,
241
242
  'wp-ctxmenu__item--accent': entry.accent,
@@ -268,26 +269,16 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
268
269
  </Teleport>
269
270
  </template>
270
271
 
272
+ <style scoped src="./menu.css"></style>
273
+
271
274
  <style scoped>
275
+ /* The panel (background, border, corners, padding, shadow, layer, text) and
276
+ * the rows' padding, corners and hover / active / disabled states are the
277
+ * shared menu look (menu.css), the same as Select's and Combobox's lists. */
272
278
  .wp-ctxmenu {
273
279
  position: fixed;
274
- z-index: 10000;
275
- background: var(--wp-bg-2);
276
- border: 1px solid var(--wp-border-strong);
277
- border-radius: var(--wp-radius-sm);
278
- box-shadow: var(--wp-shadow-md);
279
- /* Tighter padding than the old 4px-0 to match the v2 mockup. Items
280
- * carry their own 6px padding + rounded corners so the menu reads
281
- * as a stack of pills, not a flat list. */
282
- padding: 4px;
283
- /* Bumped from 180px to 250px — subtitles wrap awkwardly at narrower
284
- * widths. Existing callers without subtitles still fit comfortably. */
280
+ /* Subtitles wrap awkwardly below 250px. */
285
281
  min-width: 250px;
286
- list-style: none;
287
- margin: 0;
288
- font-family: var(--wp-font-sans, sans-serif);
289
- font-size: 12px;
290
- color: var(--wp-text);
291
282
  user-select: none;
292
283
  }
293
284
  /* Header — scope label at the top of the menu. Border-bottom acts as
@@ -319,21 +310,16 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
319
310
  padding: 6px 8px 4px 8px;
320
311
  }
321
312
  .wp-ctxmenu__item {
322
- padding: 6px 8px;
323
- border-radius: min(4px, var(--wp-radius-cap, 999px));
324
- cursor: pointer;
325
313
  white-space: nowrap;
326
314
  display: flex;
327
315
  align-items: center;
328
316
  gap: 8px;
329
317
  }
330
- .wp-ctxmenu__item--active,
331
- .wp-ctxmenu__item:hover { background: var(--wp-accent-glow); color: var(--wp-accent); }
332
318
  /* A checkable item's tick: always takes its room, shows only when on. */
333
319
  .wp-ctxmenu__check { width: 14px; font-size: 11px; text-align: center; visibility: hidden; color: var(--wp-accent); }
334
320
  .wp-ctxmenu__check[data-on] { visibility: visible; }
335
321
  .wp-ctxmenu__item--danger { color: var(--wp-red); }
336
- .wp-ctxmenu__item--danger.wp-ctxmenu__item--active,
322
+ .wp-ctxmenu__item--danger[data-active="true"],
337
323
  .wp-ctxmenu__item--danger:hover { background: var(--wp-red-bg); color: var(--wp-red); }
338
324
  /* Accent variant — recommended action highlight. Tinted background +
339
325
  * accent text in idle state so the eye lands on it; hover uses the
@@ -342,16 +328,11 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
342
328
  color: var(--wp-accent);
343
329
  background: color-mix(in oklab, var(--wp-accent) 12%, transparent);
344
330
  }
345
- .wp-ctxmenu__item--accent.wp-ctxmenu__item--active,
331
+ .wp-ctxmenu__item--accent[data-active="true"],
346
332
  .wp-ctxmenu__item--accent:hover {
347
333
  background: var(--wp-accent-glow);
348
334
  color: var(--wp-accent);
349
335
  }
350
- .wp-ctxmenu__item--disabled {
351
- color: var(--wp-text-dim);
352
- cursor: not-allowed;
353
- }
354
- .wp-ctxmenu__item--disabled:hover { background: transparent; color: var(--wp-text-dim); }
355
336
  .wp-ctxmenu__icon,
356
337
  .wp-ctxmenu__icon-spacer {
357
338
  width: 14px;
@@ -119,7 +119,7 @@ onBeforeUnmount(() => clearTimeout(timer));
119
119
  .wp-copy-btn[data-size="lg"] { min-width: var(--wp-control-h-lg); height: var(--wp-control-h-lg); }
120
120
  .wp-copy-btn[data-variant="secondary"] { background: var(--wp-bg-3); border-color: var(--wp-border); color: var(--wp-text); }
121
121
  .wp-copy-btn:not([data-inline]) .pi { font-size: 12px; }
122
- .wp-copy-btn[data-size="sm"]:not([data-inline]) .pi { font-size: 11px; }
122
+ .wp-copy-btn[data-size="sm"]:not([data-inline]) .pi { font-size: 12px; }
123
123
  .wp-copy-btn:not([data-inline]):hover:not(:disabled) { background: var(--wp-bg-3); border-color: var(--wp-border); color: var(--wp-text); }
124
124
  .wp-copy-btn[data-variant="secondary"]:hover:not(:disabled) { background: var(--wp-bg-4); border-color: var(--wp-border-strong); }
125
125
  .wp-copy-btn:not([data-inline]):focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
package/src/DataTable.vue CHANGED
@@ -165,7 +165,6 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
165
165
  <tr>
166
166
  <th v-if="selectable" class="wp-data-table__check" scope="col">
167
167
  <Checkbox
168
- class="wp-checkbox--inline"
169
168
  :model-value="allPicked"
170
169
  :indeterminate="somePicked"
171
170
  :disabled="loading || allKeys.length === 0"
@@ -227,7 +226,6 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
227
226
  >
228
227
  <td v-if="selectable" class="wp-data-table__check" @click.stop>
229
228
  <Checkbox
230
- class="wp-checkbox--inline"
231
229
  :model-value="picked.has(keyOf(row))"
232
230
  aria-label="Select row"
233
231
  @update:model-value="(on: boolean) => toggleRow(row, on)"
@@ -209,7 +209,7 @@ function clear(): void {
209
209
  padding: 0 var(--wp-space-2); border: 0; border-radius: var(--wp-radius-sm);
210
210
  background: transparent; color: var(--wp-text-muted); cursor: pointer; font: inherit;
211
211
  }
212
- .wp-date__nav .pi { font-size: 11px; }
212
+ .wp-date__nav .pi { font-size: 12px; }
213
213
  .wp-date__nav:hover, .wp-date__link:hover:not(:disabled) { background: var(--wp-row-hover); color: var(--wp-text); }
214
214
  .wp-date__nav:focus-visible, .wp-date__link:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
215
215
  .wp-date__link:disabled { opacity: 0.45; cursor: not-allowed; }
@@ -120,7 +120,8 @@ function toggle(): void {
120
120
  .wp-disclosure__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
121
121
  .wp-disclosure__chev {
122
122
  flex: none;
123
- font-size: 0.75em;
123
+ /* A whole even size: 0.75em came to 9px and sat half a pixel off. */
124
+ font-size: 10px;
124
125
  color: var(--wp-text-muted);
125
126
  transition: transform var(--wp-motion-swap) var(--wp-motion-curve-collapse);
126
127
  }
package/src/Drawer.vue CHANGED
@@ -5,7 +5,7 @@
5
5
  * Tab stays inside it and focus goes back where it was. With `modal` off
6
6
  * there is no backdrop, Tab moves on to the page and the page stays usable.
7
7
  */
8
- import CloseButton from "./CloseButton.vue";
8
+ import Button from "./Button.vue";
9
9
  import { ref, useId, useSlots } from "vue";
10
10
  import { useDialogFocus } from "./useDialogFocus";
11
11
 
@@ -66,7 +66,7 @@ useDialogFocus(panel, () => props.open, {
66
66
  <h3 v-if="title" :id="titleId" class="wp-drawer__title">{{ title }}</h3>
67
67
  <slot name="header" />
68
68
  <span class="wp-spacer" />
69
- <CloseButton kbd="Esc" data-test="drawer-close" @click="close" />
69
+ <Button variant="ghost" icon="times" aria-label="Close" kbd="Esc" data-test="drawer-close" @click="close" />
70
70
  </header>
71
71
  <div class="wp-drawer__body">
72
72
  <slot />
package/src/DropZone.vue CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { computed, ref } from "vue";
17
17
  import { dragItemMaybeAccepted, fileAccepted, formatBytes } from "./drop-zone";
18
- import WpButton from "./WpButton.vue";
18
+ import Button from "./Button.vue";
19
19
 
20
20
  export interface RejectedFile { file: File; reason: string }
21
21
 
@@ -152,7 +152,7 @@ defineExpose({ browse });
152
152
  <span class="wp-drop__label">{{ over === "refuse" ? refuseWhy : mainLabel }}</span>
153
153
  <span v-if="hint && over !== 'refuse'" class="wp-drop__hint">{{ hint }}{{ maxSize ? ` · up to ${formatBytes(maxSize)}` : "" }}</span>
154
154
  </span>
155
- <WpButton class="wp-drop__browse" variant="secondary" size="sm" icon="pi-folder-open" :disabled="disabled" @click="browse">{{ browseLabel }}</WpButton>
155
+ <Button class="wp-drop__browse" variant="secondary" size="sm" icon="pi-folder-open" :disabled="disabled" @click="browse">{{ browseLabel }}</Button>
156
156
  <input ref="inputEl" class="wp-drop__input" type="file" :accept="accept" :multiple="multiple" tabindex="-1" aria-hidden="true" @change="onPicked">
157
157
  </div>
158
158
  </template>
package/src/HsvPicker.vue CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * HsvPicker — saturation/value square + hue slider.
4
4
  *
5
- * Shared between manager ColorPicker / TweaksPanel AND the canvas
5
+ * Shared between ColorField / TweaksPanel AND the canvas
6
6
  * bundle modal. CSS uses `--wp-*` tokens with hardcoded fallbacks so
7
7
  * the picker renders inside the extension where the manager spacing
8
8
  * tokens (`--wp-space-*`) aren't loaded.
@@ -167,7 +167,7 @@ const hueCursorStyle = computed(() => ({
167
167
  position: absolute;
168
168
  width: 14px;
169
169
  height: 14px;
170
- border-radius: 50%; /* round: picker cursor on the colour field */
170
+ border-radius: min(50%, var(--wp-radius-cap, 999px));
171
171
  border: 2px solid #fff;
172
172
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 1px 2px rgba(0, 0, 0, 0.45);
173
173
  transform: translate(-50%, -50%);
@@ -144,7 +144,7 @@ function pick(name: string | null): void {
144
144
  line-height: 1;
145
145
  }
146
146
  .wp-iconpick__none { color: var(--wp-text-dim); line-height: 1; }
147
- .wp-iconpick__caret { font-size: 9px; color: var(--wp-text-dim); line-height: 1; }
147
+ .wp-iconpick__caret { font-size: 10px; color: var(--wp-text-dim); line-height: 1; }
148
148
  </style>
149
149
 
150
150
  <style>
package/src/ImageGrid.vue CHANGED
@@ -15,7 +15,7 @@
15
15
  import { computed, ref } from "vue";
16
16
  import Lightbox from "./Lightbox.vue";
17
17
  import Skeleton from "./Skeleton.vue";
18
- import WpCheck from "./WpCheck.vue";
18
+ import Checkbox from "./Checkbox.vue";
19
19
 
20
20
  export interface GridImage { id: string; src: string; alt: string; thumb?: string; caption?: string }
21
21
 
@@ -82,7 +82,7 @@ function toggle(id: string, on: boolean): void {
82
82
  <img v-if="img.thumb || img.src" class="wp-image-grid__img" :src="img.thumb || img.src" alt="" loading="lazy">
83
83
  <Skeleton v-else class="wp-image-grid__empty" shape="block" width="100%" height="100%" />
84
84
  </button>
85
- <WpCheck
85
+ <Checkbox
86
86
  v-if="selectable"
87
87
  class="wp-image-grid__check"
88
88
  :model-value="props.selected.includes(img.id)"
package/src/Lightbox.vue CHANGED
@@ -16,7 +16,7 @@
16
16
  import { computed, nextTick, ref, watch } from "vue";
17
17
  import ModalShell from "./ModalShell.vue";
18
18
  import WpImageZoom from "./WpImageZoom.vue";
19
- import CloseButton from "./CloseButton.vue";
19
+ import Button from "./Button.vue";
20
20
  import Skeleton from "./Skeleton.vue";
21
21
  import { focusablesIn } from "./useDialogFocus";
22
22
 
@@ -104,7 +104,7 @@ watch(() => props.open, async (v, was) => {
104
104
  >
105
105
  <div class="wp-lightbox__bar">
106
106
  <span v-if="count > 1" class="wp-lightbox__count" aria-hidden="true">{{ at + 1 }} / {{ count }}</span>
107
- <CloseButton class="wp-lightbox__close" kbd="Esc" @click="close" />
107
+ <Button variant="ghost" icon="times" aria-label="Close" class="wp-lightbox__close" kbd="Esc" @click="close" />
108
108
  </div>
109
109
  <figure v-if="image" class="wp-lightbox__figure" :data-zoom="zoom || undefined">
110
110
  <!-- An image with no src yet (still rendering): its shape, not an empty frame. -->