@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/CodeBlock.vue CHANGED
@@ -8,17 +8,27 @@
8
8
  * <CodeBlock :code="json" language="JSON" max-height="240px" />
9
9
  * <CodeBlock :code="log" title="Request" line-numbers v-model:wrap="wrap" />
10
10
  *
11
- * No syntax colours (they cost bundle for little gain here). A block that
11
+ * Syntax colours come from `language` (JSON, TS, Vue/HTML, CSS, shell, the
12
+ * prompt template grammar; see highlight.ts), or from `syntax` when the head
13
+ * shows a title instead of a language tag:
14
+ *
15
+ * <CodeBlock :code="template" title="Template" syntax="prompt" />
16
+ *
17
+ * Unknown languages stay plain. The colours are shared tokens, so a block
18
+ * reads right on the canvas, in the manager and in light mode. A block that
12
19
  * scrolls can be focused, so the keyboard can scroll it too.
13
20
  */
14
21
  import { computed, ref, watch } from "vue";
15
22
  import CopyButton from "./CopyButton.vue";
16
23
  import { vTooltip } from "./tooltip";
24
+ import { highlight, highlightLanguage, highlightLines } from "./highlight";
17
25
 
18
26
  const props = withDefaults(defineProps<{
19
27
  code: string;
20
- /** A small tag in the head, e.g. "JSON". */
28
+ /** A small tag in the head, e.g. "JSON". Also picks the syntax colours. */
21
29
  language?: string | undefined;
30
+ /** Syntax colours without a tag in the head ("prompt", "json", "ts" …). */
31
+ syntax?: string | undefined;
22
32
  /** A name in the head, e.g. "Prompt as sent". */
23
33
  title?: string | undefined;
24
34
  /** Wrap long lines. Bind with v-model:wrap, or leave it to the button. */
@@ -31,7 +41,7 @@ const props = withDefaults(defineProps<{
31
41
  /** Leave out the head (copy and wrap). */
32
42
  bare?: boolean | undefined;
33
43
  size?: "sm" | "md" | undefined;
34
- }>(), { language: undefined, title: undefined, wrap: undefined, maxHeight: undefined, size: "md" });
44
+ }>(), { language: undefined, syntax: undefined, title: undefined, wrap: undefined, maxHeight: undefined, size: "md" });
35
45
 
36
46
  const emit = defineEmits<{ (e: "update:wrap", v: boolean): void }>();
37
47
 
@@ -43,7 +53,8 @@ function toggleWrap(): void {
43
53
  emit("update:wrap", own.value);
44
54
  }
45
55
 
46
- const lines = computed(() => props.code.replace(/\n$/, "").split("\n"));
56
+ const tokens = computed(() => highlight(props.code, highlightLanguage(props.syntax ?? props.language)));
57
+ const lines = computed(() => highlightLines(highlight(props.code.replace(/\n$/, ""), highlightLanguage(props.syntax ?? props.language))));
47
58
  const name = computed(() => props.title ?? props.language ?? "Code");
48
59
  </script>
49
60
 
@@ -69,7 +80,7 @@ const name = computed(() => props.title ?? props.language ?? "Code");
69
80
  tabindex="0"
70
81
  :aria-label="name"
71
82
  :style="{ maxHeight }"
72
- ><code v-if="!lineNumbers" class="wp-code__code">{{ code }}</code><code v-else class="wp-code__code wp-code__code--numbered"><span v-for="(l, i) in lines" :key="i" class="wp-code__line"><span class="wp-code__num" aria-hidden="true">{{ i + 1 }}</span><span class="wp-code__text">{{ l || "\u00a0" }}</span></span></code></pre>
83
+ ><code v-if="!lineNumbers" class="wp-code__code"><template v-for="(t, i) in tokens" :key="i"><span v-if="t.kind !== 'plain'" :class="`wp-code__t--${t.kind}`">{{ t.text }}</span><template v-else>{{ t.text }}</template></template></code><code v-else class="wp-code__code wp-code__code--numbered"><span v-for="(l, i) in lines" :key="i" class="wp-code__line"><span class="wp-code__num" aria-hidden="true">{{ i + 1 }}</span><span class="wp-code__text"><template v-for="(t, j) in l" :key="j"><span v-if="t.kind !== 'plain'" :class="`wp-code__t--${t.kind}`">{{ t.text }}</span><template v-else>{{ t.text }}</template></template><template v-if="!l.length">{{ "\u00a0" }}</template></span></span></code></pre>
73
84
  </figure>
74
85
  </template>
75
86
 
@@ -96,7 +107,7 @@ const name = computed(() => props.title ?? props.language ?? "Code");
96
107
  margin: 0; padding: 0; border: 0; border-radius: var(--wp-radius-sm);
97
108
  background: transparent; color: var(--wp-text-muted); cursor: pointer;
98
109
  }
99
- .wp-code__tool .pi { font-size: 11px; }
110
+ .wp-code__tool .pi { font-size: 12px; }
100
111
  .wp-code__tool:hover { background: var(--wp-row-hover); color: var(--wp-text); }
101
112
  .wp-code__tool[aria-pressed="true"] { color: var(--wp-accent-text); background: var(--wp-bg-3); }
102
113
  .wp-code__tool:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
@@ -104,6 +115,8 @@ const name = computed(() => props.title ?? props.language ?? "Code");
104
115
  margin: 0; padding: var(--wp-space-4); overflow: auto;
105
116
  color: var(--wp-text); font: 400 var(--wp-text-sm)/1.55 var(--wp-font-mono);
106
117
  white-space: pre; tab-size: 2;
118
+ /* Code shows as written: no `===` or `<!--` ligatures. */
119
+ font-variant-ligatures: none;
107
120
  }
108
121
  .wp-code[data-size="sm"] .wp-code__pre { padding: var(--wp-space-3); font-size: var(--wp-text-xs); }
109
122
  .wp-code[data-wrap] .wp-code__pre { white-space: pre-wrap; overflow-wrap: anywhere; }
@@ -116,4 +129,18 @@ const name = computed(() => props.title ?? props.language ?? "Code");
116
129
  color: var(--wp-text-dim); user-select: none; font-variant-numeric: tabular-nums;
117
130
  }
118
131
  .wp-code__text { display: table-cell; }
132
+
133
+ /* Syntax colours: shared tokens only, so they hold on the canvas and in light mode. */
134
+ .wp-code__t--keyword { color: var(--wp-violet); }
135
+ .wp-code__t--string { color: var(--wp-green); }
136
+ .wp-code__t--number { color: var(--wp-amber); }
137
+ .wp-code__t--comment { color: var(--wp-text-dim); font-style: italic; }
138
+ .wp-code__t--punct { color: var(--wp-text-muted); }
139
+ .wp-code__t--key, .wp-code__t--attr { color: var(--wp-teal); }
140
+ .wp-code__t--tag { color: var(--wp-rose); }
141
+ .wp-code__t--fn { color: var(--wp-info); }
142
+ .wp-code__t--var { color: var(--wp-accent-text); }
143
+ .wp-code__t--ref { color: var(--wp-teal); }
144
+ .wp-code__t--brace { color: var(--wp-amber); }
145
+ .wp-code__t--escape { color: var(--wp-text-dim); }
119
146
  </style>
@@ -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
@@ -24,6 +24,7 @@
24
24
  */
25
25
  import { computed } from "vue";
26
26
  import Checkbox from "./Checkbox.vue";
27
+ import Skeleton from "./Skeleton.vue";
27
28
 
28
29
  export interface DataColumn<R> {
29
30
  key: string;
@@ -164,7 +165,6 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
164
165
  <tr>
165
166
  <th v-if="selectable" class="wp-data-table__check" scope="col">
166
167
  <Checkbox
167
- class="wp-checkbox--inline"
168
168
  :model-value="allPicked"
169
169
  :indeterminate="somePicked"
170
170
  :disabled="loading || allKeys.length === 0"
@@ -202,8 +202,8 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
202
202
  </thead>
203
203
  <tbody>
204
204
  <template v-if="loading && rows.length === 0">
205
- <tr v-for="n in loadingRows" :key="`skel-${n}`" class="wp-row-skel" aria-hidden="true">
206
- <td v-for="i in span" :key="i"><span class="wp-skel-bar" :style="{ width: `${40 + ((n * 17 + i * 23) % 50)}%` }" /></td>
205
+ <tr v-for="n in loadingRows" :key="`skel-${n}`" class="wp-data-table__loading" aria-hidden="true">
206
+ <td v-for="i in span" :key="i"><Skeleton :width="`${40 + ((n * 17 + i * 23) % 50)}%`" /></td>
207
207
  </tr>
208
208
  </template>
209
209
  <tr v-else-if="shown.length === 0" class="wp-data-table__empty">
@@ -226,7 +226,6 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
226
226
  >
227
227
  <td v-if="selectable" class="wp-data-table__check" @click.stop>
228
228
  <Checkbox
229
- class="wp-checkbox--inline"
230
229
  :model-value="picked.has(keyOf(row))"
231
230
  aria-label="Select row"
232
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>