@dumiflex/wp-kit 0.5.0 → 0.7.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 (89) 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/BundlePreview.vue +254 -0
  7. package/src/Button.vue +73 -44
  8. package/src/Checkbox.vue +201 -63
  9. package/src/Chip.vue +5 -2
  10. package/src/CodeBlock.vue +1 -1
  11. package/src/ColorField.vue +60 -6
  12. package/src/CombinePreview.vue +150 -0
  13. package/src/Combobox.vue +10 -14
  14. package/src/ConfirmModal.vue +12 -22
  15. package/src/ConstraintMatrixPreview.vue +6 -22
  16. package/src/ConstraintPreview.vue +259 -0
  17. package/src/ContextMenu.vue +11 -30
  18. package/src/CopyButton.vue +1 -1
  19. package/src/DataTable.vue +0 -2
  20. package/src/DatePicker.vue +1 -1
  21. package/src/DerivationCondition.vue +68 -0
  22. package/src/DerivationPreview.vue +345 -0
  23. package/src/Disclosure.vue +2 -1
  24. package/src/Drawer.vue +2 -2
  25. package/src/DropZone.vue +2 -2
  26. package/src/FixedValuesPreview.vue +184 -0
  27. package/src/HsvPicker.vue +2 -2
  28. package/src/IconPicker.vue +32 -103
  29. package/src/IconPickerPanel.vue +132 -0
  30. package/src/ImageGrid.vue +2 -2
  31. package/src/Lightbox.vue +2 -2
  32. package/src/LoadingMark.vue +6 -61
  33. package/src/Modal.vue +109 -44
  34. package/src/ModalShell.vue +26 -5
  35. package/src/Pagination.vue +4 -4
  36. package/src/PayloadPreview.vue +154 -0
  37. package/src/Popover.vue +68 -12
  38. package/src/PostDetailSkeleton.vue +65 -0
  39. package/src/PostGridSkeleton.vue +61 -0
  40. package/src/PreviewGroup.vue +75 -0
  41. package/src/ProfileSkeleton.vue +73 -0
  42. package/src/RadioGroup.vue +5 -4
  43. package/src/RichText.vue +61 -0
  44. package/src/SearchInput.vue +1 -1
  45. package/src/Select.vue +53 -158
  46. package/src/SelectionBar.vue +90 -0
  47. package/src/SettingGroup.vue +57 -0
  48. package/src/SettingRow.vue +127 -0
  49. package/src/Skeleton.vue +1 -1
  50. package/src/Slider.vue +166 -55
  51. package/src/SortableList.vue +1 -1
  52. package/src/TagInput.vue +1 -1
  53. package/src/Timeline.vue +2 -2
  54. package/src/WildcardPreview.vue +607 -0
  55. package/src/WorkingLine.vue +234 -0
  56. package/src/WpIconButton.vue +3 -2
  57. package/src/WpImageZoom.vue +7 -7
  58. package/src/WpSegmented.vue +3 -1
  59. package/src/WpSwitch.vue +1 -1
  60. package/src/WpTabs.vue +1 -1
  61. package/src/_modal-motion.css +1 -1
  62. package/src/button.css +8 -6
  63. package/src/canvas-motion.ts +251 -0
  64. package/src/floating.ts +5 -0
  65. package/src/hsv.ts +1 -1
  66. package/src/kind-colour.ts +24 -1
  67. package/src/linkify.ts +142 -0
  68. package/src/loading-mark-motion.ts +12 -155
  69. package/src/matrix-axis.ts +28 -0
  70. package/src/menu.css +51 -0
  71. package/src/motion.css +73 -0
  72. package/src/pi-icons.ts +51 -0
  73. package/src/preview-readers.ts +401 -0
  74. package/src/preview.css +194 -0
  75. package/src/slider-math.ts +45 -1
  76. package/src/theme/looks.css +6 -19
  77. package/src/theme/tokens.css +12 -1108
  78. package/src/tokens.ts +15 -4
  79. package/src/tooltip.css +8 -5
  80. package/src/useDialogFocus.ts +11 -2
  81. package/src/working-line-motion.ts +135 -0
  82. package/src/CloseButton.vue +0 -69
  83. package/src/ColorPicker.vue +0 -201
  84. package/src/ConfirmDialog.vue +0 -165
  85. package/src/RangeSlider.vue +0 -183
  86. package/src/Toggle.vue +0 -36
  87. package/src/WpButton.vue +0 -75
  88. package/src/WpCheck.vue +0 -104
  89. package/src/_skeleton.css +0 -50
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/DumiFlex/wp-kit/compare/v0.6.0...v0.7.0) (2026-11-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * IconPicker all (every PrimeIcons glyph, searchable), inline, color and allow-none ([#70](https://github.com/DumiFlex/wp-kit/issues/70)) ([a0dc837](https://github.com/DumiFlex/wp-kit/commit/a0dc83716441c8061428e69faf751ab7b52ed8ae))
9
+ * RichText, the module payload previews and the post loading shapes move into the kit ([#71](https://github.com/DumiFlex/wp-kit/issues/71)) ([b92d62f](https://github.com/DumiFlex/wp-kit/commit/b92d62f87ecd44cb205d2265d7d56c9018e29144))
10
+ * SelectionBar, the one bulk-action bar over a list of ticked rows ([#69](https://github.com/DumiFlex/wp-kit/issues/69)) ([13a51e2](https://github.com/DumiFlex/wp-kit/commit/13a51e22b0e27b404700dc6f3fb2f34fd912d10a))
11
+ * SettingGroup and SettingRow, the settings page rows both apps use ([#68](https://github.com/DumiFlex/wp-kit/issues/68)) ([20f4ff8](https://github.com/DumiFlex/wp-kit/commit/20f4ff8a8c28da63fcabfd9f2c0c2bf28a4f60dd))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * draw WorkingLine's marks on a canvas so they glide in Firefox ([#67](https://github.com/DumiFlex/wp-kit/issues/67)) ([6565f15](https://github.com/DumiFlex/wp-kit/commit/6565f1575ecb224ad9e94efdef611f5c31b12849))
17
+
18
+ ## [0.6.0](https://github.com/DumiFlex/wp-kit/compare/v0.5.0...v0.6.0) (2026-11-02)
19
+
20
+
21
+ ### ⚠ BREAKING CHANGES
22
+
23
+ * WpButton, CloseButton and ConfirmDialog are removed. Use Button (size="sm" on the canvas; variant="ghost" icon="times" aria-label="Close" for a close button) and ConfirmModal (visible -> open, variant="danger" -> tone="danger"). Button shows its aria-label as a kit tooltip instead of a title attribute. Modal's panel classes are scoped; a ModalShell backdrop close needs mousedown and click on the backdrop.
24
+ * RangeSlider is removed (use Slider with a [low, high] v-model); _skeleton.css and .wp-skel-bar are removed (use Skeleton); unused global classes are removed from theme/tokens.css.
25
+ * WpCheck is removed; use Checkbox (same v-model, size, disabled and --check-on). Toggle is removed; use <WpSwitch size="md"> (error becomes invalid). ColorPicker is removed; use <ColorField presets size="md"> (test hooks are <dataTest>, <dataTest>-preset, <dataTest>-hex). The global .wp-check, .wp-checkbox* and .wp-radio classes are removed.
26
+
27
+ ### Features
28
+
29
+ * **site:** API tables under every piece, read from the components' own types ([#58](https://github.com/DumiFlex/wp-kit/issues/58)) ([e23973a](https://github.com/DumiFlex/wp-kit/commit/e23973acf57f158068594dc0d63f8172084ab945))
30
+ * **site:** search bar that finds pieces and sections ([#52](https://github.com/DumiFlex/wp-kit/issues/52)) ([8989669](https://github.com/DumiFlex/wp-kit/commit/8989669c43bc8f1511266525ee6a8b9007ee2891))
31
+ * Slider snap settles the thumb on its marks (all the time, or within a distance) ([#60](https://github.com/DumiFlex/wp-kit/issues/60)) ([9b1b5ac](https://github.com/DumiFlex/wp-kit/commit/9b1b5acf1f0d604814b300d7158691fbab007ee0))
32
+ * WorkingLine and entrance classes; AI animations on the kit site ([#56](https://github.com/DumiFlex/wp-kit/issues/56)) ([430df62](https://github.com/DumiFlex/wp-kit/commit/430df624e27e9181d174a7572f70369c7a7b428c))
33
+
34
+
35
+ ### Bug Fixes
36
+
37
+ * centre icons on whole pixels; no link underline on a link drawn as a button ([#59](https://github.com/DumiFlex/wp-kit/issues/59)) ([9de9468](https://github.com/DumiFlex/wp-kit/commit/9de94684688879cdd396f948b54650c43918a9fc))
38
+ * chip text clipped in Firefox, refresh icon wobbling as it spins ([#53](https://github.com/DumiFlex/wp-kit/issues/53)) ([02e0c84](https://github.com/DumiFlex/wp-kit/commit/02e0c8405293e39206ea8c810822b00029ee2f44))
39
+ * circles follow the look's corner cap (square in Terminal and TUI) ([#61](https://github.com/DumiFlex/wp-kit/issues/61)) ([7ae84ba](https://github.com/DumiFlex/wp-kit/commit/7ae84baae4024e20b4e67f7b5fb4ec314d3a55b4))
40
+ * tooltip and popover outlines survive a host's CSS reset ([#63](https://github.com/DumiFlex/wp-kit/issues/63)) ([fd8a334](https://github.com/DumiFlex/wp-kit/commit/fd8a334ac86cca52f83f9e0ac7b3bc3ed3b92a98))
41
+
42
+
43
+ ### Refactoring
44
+
45
+ * fold RangeSlider into Slider, drop dead global CSS; write the one-piece rule ([#57](https://github.com/DumiFlex/wp-kit/issues/57)) ([2e5d024](https://github.com/DumiFlex/wp-kit/commit/2e5d0240bbb78b467940f19ee50f4de0623ef69d))
46
+ * one Button and one Modal (WpButton, CloseButton and ConfirmDialog folded in) ([#62](https://github.com/DumiFlex/wp-kit/issues/62)) ([591546d](https://github.com/DumiFlex/wp-kit/commit/591546db46a1d6f3fa6ccef10c484bbe40689e94))
47
+ * one Checkbox, one switch, one colour field ([#54](https://github.com/DumiFlex/wp-kit/issues/54)) ([06dba67](https://github.com/DumiFlex/wp-kit/commit/06dba67d9495773bcb1088c09763b30654972bff))
48
+ * one menu (menu.css, Popover variant="menu", Select on Popover) ([#55](https://github.com/DumiFlex/wp-kit/issues/55)) ([e39dc1f](https://github.com/DumiFlex/wp-kit/commit/e39dc1f27f8d0d1efec5bae609b0bf19543d015b))
49
+
3
50
  ## [0.5.0](https://github.com/DumiFlex/wp-kit/compare/v0.4.0...v0.5.0) (2026-11-02)
4
51
 
5
52
 
package/README.md CHANGED
@@ -57,6 +57,11 @@ If you already import the kit as `@/kit/...`, alias `@/kit` to `node_modules/@du
57
57
 
58
58
  - **Vue 3.5** and **PrimeIcons 7** (peer dependencies). Icons are `pi pi-*` classes. The kit imports nothing else at runtime.
59
59
  - **A theme:** the kit's own sheets above. A host with its own colour sheet can define the names in [`SHARED_TOKENS`](https://github.com/DumiFlex/wp-kit/blob/main/src/tokens.ts) instead and use the pieces that read only those.
60
+ - **A layer order, if your app uses cascade layers** (Tailwind 4 does). A few pieces (the tooltip, Popover, DropdownMenu, the entrance classes) put their rules in `@layer wp-extension`, so a host page's unlayered CSS wins a clash (that keeps the kit polite on the ComfyUI canvas). Their outlines are drawn as rings, so a reset like Tailwind's preflight (`border-width: 0`) can't strip them. If your own CSS is layered, declare the order in your page's `<head>`, before any stylesheet, with the kit above your reset and below your utilities:
61
+
62
+ ```html
63
+ <style>@layer theme, base, wp-extension, components, utilities;</style>
64
+ ```
60
65
 
61
66
  ## Versions
62
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dumiflex/wp-kit",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Vue 3 component kit shared by the Wildcard Pipeline manager, canvas and community site.",
5
5
  "keywords": [
6
6
  "vue",
@@ -53,8 +53,8 @@
53
53
  "npm-readme": "node scripts/npm-readme.mjs"
54
54
  },
55
55
  "peerDependencies": {
56
- "vue": "^3.5.0",
57
- "primeicons": "^7.0.0 || ^8.0.0"
56
+ "primeicons": "^7.0.0 || ^8.0.0",
57
+ "vue": "^3.5.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.10.0",
@@ -72,6 +72,7 @@
72
72
  "vite": "5.4.21",
73
73
  "vitest": "2.1.9",
74
74
  "vue": "3.5.44",
75
+ "vue-component-meta": "2.2.12",
75
76
  "vue-eslint-parser": "10.4.0",
76
77
  "vue-tsc": "2.2.12"
77
78
  }
package/src/Avatar.vue CHANGED
@@ -68,7 +68,7 @@ const hue = computed(() => {
68
68
  .wp-avatar {
69
69
  --_s: var(--wp-control-h-md);
70
70
  position: relative; display: inline-grid; place-items: center; flex: none; overflow: hidden;
71
- width: var(--_s); height: var(--_s); border-radius: 50%; /* round: avatar */
71
+ width: var(--_s); height: var(--_s); border-radius: min(50%, var(--wp-radius-cap, 999px));
72
72
  background: color-mix(in srgb, var(--_hue) 22%, var(--wp-bg-2));
73
73
  color: color-mix(in srgb, var(--_hue) 70%, var(--wp-text));
74
74
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--_hue) 30%, transparent);
package/src/Banner.vue CHANGED
@@ -14,7 +14,7 @@
14
14
  * that is part of the page (a docs tip): `role="note"`, never announced.
15
15
  */
16
16
  import { computed, useSlots } from "vue";
17
- import CloseButton from "./CloseButton.vue";
17
+ import Button from "./Button.vue";
18
18
 
19
19
  export type BannerTone = "neutral" | "info" | "success" | "warn" | "danger" | "accent";
20
20
 
@@ -60,11 +60,11 @@ const iconClass = computed(() => props.icon ?? TONE_ICON[props.tone]);
60
60
  <div v-if="slots.default" class="wp-banner__text"><slot /></div>
61
61
  </div>
62
62
  <div v-if="slots.actions" class="wp-banner__actions"><slot name="actions" /></div>
63
- <CloseButton
64
- v-if="dismissible"
63
+ <Button v-if="dismissible" variant="ghost"
64
+ icon="times"
65
65
  class="wp-banner__close"
66
66
  size="sm"
67
- :label="dismissLabel"
67
+ :aria-label="dismissLabel"
68
68
  @click="emit('dismiss')"
69
69
  />
70
70
  </div>
@@ -0,0 +1,254 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Bundle: what the bundle needs from outside vs what it sets (folded from
4
+ * its children), then a child list beside the selected child's own full
5
+ * preview. Narrow hosts stack the list above the preview.
6
+ */
7
+ import { computed, ref } from "vue";
8
+ import PayloadPreview from "./PayloadPreview.vue";
9
+ import ListRow from "./ListRow.vue";
10
+ import Chip from "./Chip.vue";
11
+ import { itemType, kindColour, kindIcon, moduleSubtype } from "./kind-colour";
12
+ import {
13
+ derivationRules,
14
+ fixedValues,
15
+ innerOf,
16
+ readArray,
17
+ readGroups,
18
+ readObject,
19
+ readString,
20
+ varFlow,
21
+ wildcardOptions,
22
+ type Obj,
23
+ } from "./preview-readers";
24
+
25
+ const props = defineProps<{
26
+ payload: Obj;
27
+ refNames: Record<string, string>;
28
+ refGroups: Record<string, Record<string, string[]>>;
29
+ }>();
30
+
31
+ interface Child {
32
+ key: string;
33
+ id: string;
34
+ name: string;
35
+ type: string;
36
+ enabled: boolean;
37
+ raw: Obj;
38
+ }
39
+ function childName(c: unknown): string | null {
40
+ const meta = readObject(c, "meta");
41
+ return (
42
+ readString(meta, "name") ??
43
+ readString(meta, "library_name") ??
44
+ readString(c, "name") ??
45
+ readString(readObject(c, "payload"), "var_binding")
46
+ );
47
+ }
48
+ const children = computed<Child[]>(() =>
49
+ readArray(props.payload, "children").map((c, i) => ({
50
+ key: readString(c, "id") ?? String(i),
51
+ id: readString(c, "id") ?? "",
52
+ name: childName(c) ?? "—",
53
+ type: readString(c, "type") ?? "—",
54
+ enabled: (c as Obj | null)?.["enabled"] !== false,
55
+ raw: (c && typeof c === "object" ? c : {}) as Obj,
56
+ })),
57
+ );
58
+ /** Sibling names + wildcard axes so a child constraint resolves its
59
+ * wildcards the same way the list does. */
60
+ const childRefNames = computed(() => {
61
+ const out: Record<string, string> = { ...props.refNames };
62
+ for (const c of children.value) if (c.id) out[c.id] = c.name;
63
+ return out;
64
+ });
65
+ const childRefGroups = computed(() => {
66
+ const out: Record<string, Record<string, string[]>> = { ...props.refGroups };
67
+ for (const c of children.value) {
68
+ const g = readGroups(innerOf(c.raw), "tag_groups");
69
+ if (c.id && g) out[c.id] = g;
70
+ }
71
+ return out;
72
+ });
73
+ function summary(c: Child): string {
74
+ const inner = innerOf(c.raw);
75
+ const label = c.type.replace(/_/g, " ");
76
+ switch (c.type) {
77
+ case "wildcard": {
78
+ const n = wildcardOptions(inner).length;
79
+ const b = readString(inner, "var_binding");
80
+ return `${label} · ${n} option${n === 1 ? "" : "s"}${b ? ` · $${b}` : ""}`;
81
+ }
82
+ case "fixed_values": {
83
+ const n = fixedValues(inner).length;
84
+ return `${label} · ${n} value${n === 1 ? "" : "s"}`;
85
+ }
86
+ case "derivation": {
87
+ const n = derivationRules(inner).length;
88
+ return `${label} · ${n} rule${n === 1 ? "" : "s"}`;
89
+ }
90
+ case "combine": {
91
+ const out = readString(inner, "output_var");
92
+ return out ? `${label} · $${out}` : label;
93
+ }
94
+ case "constraint": {
95
+ const s = readString(inner, "source_wildcard_id");
96
+ const t = readString(inner, "target_wildcard_id");
97
+ const name = (id: string | null): string => (id ? (childRefNames.value[id] ?? id) : "?");
98
+ return `${label} · ${name(s)} → ${name(t)}`;
99
+ }
100
+ default:
101
+ return label;
102
+ }
103
+ }
104
+ const flow = computed(() => varFlow(props.payload));
105
+ const provide = computed(() => flow.value.reads.filter((v) => !flow.value.sets.includes(v)));
106
+
107
+ const selected = ref<string | null>(null);
108
+ const current = computed(
109
+ () => children.value.find((c) => c.key === selected.value) ?? children.value[0] ?? null,
110
+ );
111
+ const counts = computed(() => {
112
+ const out: Record<string, number> = {};
113
+ for (const c of children.value) out[c.type] = (out[c.type] ?? 0) + 1;
114
+ return out;
115
+ });
116
+ </script>
117
+
118
+ <template>
119
+ <div class="wp-pv wp-bundle">
120
+ <div class="wp-pv-stats">
121
+ <div class="wp-pv-stat">
122
+ <b>{{ children.length }}</b><span>module{{ children.length === 1 ? "" : "s" }}</span>
123
+ </div>
124
+ <div
125
+ v-for="(n, t) in counts"
126
+ :key="t"
127
+ class="wp-pv-stat wp-pv-hide-narrow"
128
+ >
129
+ <b>{{ n }}</b><span>{{ String(t).replace(/_/g, " ") }}</span>
130
+ </div>
131
+ </div>
132
+ <div
133
+ v-if="provide.length || flow.sets.length"
134
+ class="wp-bd-flow"
135
+ >
136
+ <template v-if="provide.length">
137
+ <span class="wp-pv-label">You provide</span>
138
+ <div>
139
+ <code
140
+ v-for="v in provide"
141
+ :key="`p-${v}`"
142
+ class="wp-bd-var"
143
+ >${{ v }}</code>
144
+ <span class="wp-bd-hint">read inside, not set by any module here</span>
145
+ </div>
146
+ </template>
147
+ <template v-if="flow.sets.length">
148
+ <span class="wp-pv-label">It sets</span>
149
+ <div>
150
+ <code
151
+ v-for="v in flow.sets"
152
+ :key="`s-${v}`"
153
+ class="wp-bd-var"
154
+ >${{ v }}</code>
155
+ </div>
156
+ </template>
157
+ </div>
158
+ <div class="wp-bd-split">
159
+ <nav
160
+ class="wp-bd-kids wp-bundle-children"
161
+ data-wp-rows
162
+ >
163
+ <ListRow
164
+ v-for="c in children"
165
+ :key="c.key"
166
+ class="wp-bd-kid wp-bundle-child"
167
+ :selected="current?.key === c.key"
168
+ :icon="kindIcon(c.type)"
169
+ :description="`${summary(c)}${c.enabled ? '' : ' · disabled'}`"
170
+ :muted="!c.enabled"
171
+ data-test="bundle-child"
172
+ @click="selected = c.key"
173
+ >
174
+ {{ c.name }}
175
+ </ListRow>
176
+ </nav>
177
+ <div
178
+ v-if="current"
179
+ class="wp-bd-pane"
180
+ data-test="bundle-child-body"
181
+ >
182
+ <div class="wp-bd-pane__head">
183
+ <Chip v-if="moduleSubtype(current.type)" :color="kindColour(itemType('module', moduleSubtype(current.type)))">{{ itemType('module', moduleSubtype(current.type)) }}</Chip>
184
+ <Chip v-else>{{ current.type }}</Chip>
185
+ <b>{{ current.name }}</b>
186
+ </div>
187
+ <PayloadPreview
188
+ :payload="current.raw"
189
+ :ref-names="childRefNames"
190
+ :ref-groups="childRefGroups"
191
+ nested
192
+ />
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </template>
197
+
198
+ <style scoped src="./preview.css"></style>
199
+ <style scoped>
200
+ .wp-bd-flow {
201
+ display: grid;
202
+ grid-template-columns: auto minmax(0, 1fr);
203
+ gap: 6px 14px;
204
+ align-items: baseline;
205
+ padding: 12px 16px;
206
+ border-bottom: 1px solid var(--wp-border);
207
+ }
208
+ .wp-bd-flow > div {
209
+ display: flex;
210
+ flex-wrap: wrap;
211
+ gap: 6px;
212
+ align-items: baseline;
213
+ }
214
+ .wp-bd-var {
215
+ font-family: var(--wp-font-mono);
216
+ font-size: 12px;
217
+ color: var(--wp-success);
218
+ }
219
+ .wp-bd-hint {
220
+ font-size: 11.5px;
221
+ color: var(--wp-text-dim);
222
+ }
223
+ .wp-bd-split {
224
+ display: grid;
225
+ grid-template-columns: 240px minmax(0, 1fr);
226
+ }
227
+ @container (max-width: 700px) {
228
+ .wp-bd-split {
229
+ grid-template-columns: minmax(0, 1fr);
230
+ }
231
+ .wp-bd-kids {
232
+ border-right: 0;
233
+ border-bottom: 1px solid var(--wp-border);
234
+ }
235
+ }
236
+ .wp-bd-kids {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: 2px;
240
+ padding: 8px;
241
+ border-right: 1px solid var(--wp-border);
242
+ }
243
+ .wp-bd-pane {
244
+ min-width: 0;
245
+ }
246
+ .wp-bd-pane__head {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 10px;
250
+ padding: 12px 16px;
251
+ border-bottom: 1px solid var(--wp-border);
252
+ font-size: 13px;
253
+ }
254
+ </style>
package/src/Button.vue CHANGED
@@ -1,103 +1,132 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Button — single source of truth for app buttons.
3
+ * Button: the one button, in the manager, on the canvas and on the
4
+ * community site (WpButton and CloseButton were folded in).
5
+ *
6
+ * <Button variant="primary" icon="plus">Add module</Button>
7
+ * <Button variant="ghost" icon="download" aria-label="Download snapshot" />
8
+ * <Button variant="ghost" icon="times" aria-label="Close" kbd="Esc" @click="close" />
9
+ * <Button variant="add" icon="plus" size="sm" block>Add rule</Button>
10
+ * <Button as="a" href="/docs" variant="link">Read the docs</Button>
4
11
  *
5
12
  * Variants:
6
- * primary — solid accent; main CTA per surface
7
- * secondary — subtle filled; default
8
- * ghost — transparent with hover bg; tertiary
9
- * danger — red-tinted; destructive actions
10
- * link — text-only; inline CTAs
11
- * outline — neutral bordered ghost (not accent: an accent outline read as
12
- * a second primary)
13
- * add — dashed, quiet: the "Add a row" button at the end of a list
13
+ * primary solid accent; the main action of a surface
14
+ * secondary subtle filled; the default
15
+ * ghost transparent with a hover fill; tertiary actions, close (×)
16
+ * danger red-tinted; destructive actions
17
+ * link text only; inline actions
18
+ * outline neutral bordered ghost (an accent outline read as a second primary)
19
+ * add dashed and quiet: the "Add a row" button at the end of a list
14
20
  *
15
- * `block` stretches it to the full width of its container. `pressed` makes
16
- * it a toggle (aria-pressed, accent tint while on).
21
+ * Sizes follow the control scale: sm 24 (the canvas) / md 32 (default) / lg 40.
22
+ * `icon` / `iconRight` take a PrimeIcons name, with or without `pi-`.
17
23
  *
18
- * Sizes follow the control scale: sm 24 / md 32 (default) / lg 40.
24
+ * With no slot content it is an icon button (square, `.wp-btn--icon`); give
25
+ * it an `aria-label`, which is also its tooltip (`hint` sets another, `kbd`
26
+ * adds a key chip). Use `hint`, not `v-tooltip` on the component, which would
27
+ * fight the button's own. `pressed` makes it a toggle (aria-pressed, accent
28
+ * tint while on). `block` stretches it to its container.
19
29
  *
20
- * States: default · hover · active · focus-visible · disabled · loading.
21
30
  * Disabled fades to 45% with a not-allowed cursor and no hover change.
22
- * Loading keeps the variant's own colours, swaps the lead icon for the
23
- * sparkle LoadingMark in the label colour, sets aria-busy and ignores clicks.
31
+ * Loading keeps the variant's colours, swaps the lead icon for the sparkle,
32
+ * sets aria-busy and ignores clicks.
24
33
  *
25
- * Icon-only mode auto-engages when no slot content is provided (.wp-btn--icon).
26
- * Pass `aria-label` for screen reader text when using icon-only mode.
34
+ * `as="a"` draws a link that looks like a button (pass `href`, `target`…);
35
+ * disabled then sets aria-disabled and takes it out of the tab order.
27
36
  *
28
- * The styles live in kit/button.css, shared with the canvas
29
- * Button twin.
37
+ * Its stylesheet (`button.css`) is pulled in scoped, so the canvas never
38
+ * carries the `.wp-btn` classes globally; the manager also loads it globally
39
+ * for the few hand-written `.wp-btn` links. Row icon buttons on the canvas
40
+ * are WpIconButton.
30
41
  */
31
42
  import { computed, useSlots } from "vue";
32
43
  import Icon from "./Icon.vue";
33
44
  import LoadingMark from "./LoadingMark.vue";
45
+ import { vTooltip } from "./tooltip";
34
46
 
35
47
  type Variant = "primary" | "secondary" | "ghost" | "danger" | "link" | "outline" | "add";
36
48
  type Size = "sm" | "md" | "lg";
37
49
 
38
- interface Props {
50
+ const props = withDefaults(defineProps<{
39
51
  variant?: Variant | undefined;
40
52
  size?: Size | undefined;
53
+ /** PrimeIcons name ("plus" or "pi-plus"), drawn before the label. */
41
54
  icon?: string | undefined;
55
+ /** PrimeIcons name drawn after the label (a chevron, an external link). */
42
56
  iconRight?: string | undefined;
43
57
  loading?: boolean | undefined;
44
58
  disabled?: boolean | undefined;
59
+ /** Stretch to the container's full width. */
45
60
  block?: boolean | undefined;
46
61
  /** Makes it a toggle (`aria-pressed`): on, it wears the accent tint. */
47
62
  pressed?: boolean | undefined;
48
63
  type?: "button" | "submit" | "reset" | undefined;
49
64
  ariaLabel?: string | undefined;
50
- }
51
- const props = withDefaults(defineProps<Props>(), {
65
+ /** Tooltip text; an icon button shows its aria-label when this is unset. */
66
+ hint?: string | undefined;
67
+ /** A shortcut shown in the tooltip's key chip, e.g. "Esc". */
68
+ kbd?: string | undefined;
69
+ /** `a` draws a link that looks like a button. */
70
+ as?: "button" | "a" | undefined;
71
+ }>(), {
52
72
  variant: "secondary",
53
73
  size: "md",
54
74
  type: "button",
55
75
  pressed: undefined,
76
+ icon: undefined,
77
+ iconRight: undefined,
78
+ ariaLabel: undefined,
79
+ hint: undefined,
80
+ kbd: undefined,
81
+ as: "button",
56
82
  });
57
83
 
58
- const emit = defineEmits<{
59
- (e: "click", evt: MouseEvent): void;
60
- }>();
84
+ const emit = defineEmits<{ click: [evt: MouseEvent] }>();
61
85
 
62
86
  const slots = useSlots();
63
-
64
- const hasContent = computed(() => Boolean(slots.default));
65
-
66
- /** Native hover tooltip for icon-only buttons. Their `aria-label` names them
67
- * for screen readers, but a sighted user hovering a bare icon gets nothing —
68
- * so mirror the label into `title` when there's no visible text. A button with
69
- * text already says what it does, so it stays untitled. */
70
- const titleAttr = computed(() =>
71
- !hasContent.value && props.ariaLabel ? props.ariaLabel : undefined);
87
+ const iconOnly = computed(() => !slots.default);
88
+ const isLink = computed(() => props.as === "a");
89
+ const off = computed(() => Boolean(props.disabled || props.loading));
72
90
 
73
91
  const classes = computed(() => [
74
92
  "wp-btn",
75
93
  `wp-btn--${props.variant}`,
76
94
  `wp-btn--${props.size}`,
77
- !hasContent.value && "wp-btn--icon",
95
+ iconOnly.value && "wp-btn--icon",
78
96
  props.block && "wp-btn--block",
79
97
  ]);
80
98
 
81
- function onClick(e: MouseEvent) {
82
- if (props.disabled || props.loading) return;
99
+ const markSize = computed(() => (props.size === "lg" ? 16 : props.size === "md" ? 14 : 12));
100
+
101
+ function onClick(e: MouseEvent): void {
102
+ if (off.value) {
103
+ // A link can't be disabled natively; stop it following its href.
104
+ if (isLink.value) e.preventDefault();
105
+ return;
106
+ }
83
107
  emit("click", e);
84
108
  }
85
109
  </script>
86
110
 
87
111
  <template>
88
- <button
89
- :type="type"
112
+ <component
113
+ :is="as"
114
+ v-tooltip="{ text: hint ?? (iconOnly ? ariaLabel : undefined), kbd }"
115
+ :type="isLink ? undefined : type"
90
116
  :class="classes"
91
- :disabled="disabled || loading"
117
+ :disabled="isLink ? undefined : off"
118
+ :aria-disabled="isLink && off ? 'true' : undefined"
119
+ :tabindex="isLink && off ? -1 : undefined"
92
120
  :aria-label="ariaLabel"
93
- :title="titleAttr"
94
121
  :aria-busy="loading || undefined"
95
122
  :aria-pressed="pressed === undefined ? undefined : pressed"
96
123
  @click="onClick"
97
124
  >
98
- <LoadingMark v-if="loading" variant="sparkle" mono :size="size === 'lg' ? 16 : 14" decorative />
125
+ <LoadingMark v-if="loading" variant="sparkle" mono :size="markSize" decorative />
99
126
  <Icon v-else-if="icon" :name="icon" />
100
- <span v-if="hasContent" class="wp-btn__label"><slot /></span>
127
+ <span v-if="!iconOnly" class="wp-btn__label"><slot /></span>
101
128
  <Icon v-if="iconRight" :name="iconRight" />
102
- </button>
129
+ </component>
103
130
  </template>
131
+
132
+ <style scoped src="./button.css"></style>