@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.
- package/CHANGELOG.md +32 -0
- package/README.md +5 -0
- package/package.json +4 -3
- package/src/Avatar.vue +1 -1
- package/src/Banner.vue +4 -4
- package/src/Button.vue +73 -44
- package/src/Checkbox.vue +201 -63
- package/src/Chip.vue +5 -2
- package/src/CodeBlock.vue +1 -1
- package/src/ColorField.vue +60 -6
- package/src/Combobox.vue +10 -14
- package/src/ConfirmModal.vue +12 -22
- package/src/ContextMenu.vue +11 -30
- package/src/CopyButton.vue +1 -1
- package/src/DataTable.vue +0 -2
- package/src/DatePicker.vue +1 -1
- package/src/Disclosure.vue +2 -1
- package/src/Drawer.vue +2 -2
- package/src/DropZone.vue +2 -2
- package/src/HsvPicker.vue +2 -2
- package/src/IconPicker.vue +1 -1
- package/src/ImageGrid.vue +2 -2
- package/src/Lightbox.vue +2 -2
- package/src/Modal.vue +109 -44
- package/src/ModalShell.vue +26 -5
- package/src/Pagination.vue +4 -4
- package/src/Popover.vue +68 -12
- package/src/RadioGroup.vue +5 -4
- package/src/SearchInput.vue +1 -1
- package/src/Select.vue +53 -158
- package/src/Skeleton.vue +1 -1
- package/src/Slider.vue +166 -55
- package/src/SortableList.vue +1 -1
- package/src/TagInput.vue +1 -1
- package/src/Timeline.vue +2 -2
- package/src/WorkingLine.vue +320 -0
- package/src/WpIconButton.vue +3 -2
- package/src/WpImageZoom.vue +7 -7
- package/src/WpSegmented.vue +3 -1
- package/src/WpSwitch.vue +1 -1
- package/src/WpTabs.vue +1 -1
- package/src/_modal-motion.css +1 -1
- package/src/button.css +8 -6
- package/src/floating.ts +5 -0
- package/src/hsv.ts +1 -1
- package/src/menu.css +51 -0
- package/src/motion.css +73 -0
- package/src/slider-math.ts +45 -1
- package/src/theme/looks.css +6 -19
- package/src/theme/tokens.css +12 -1108
- package/src/tokens.ts +8 -3
- package/src/tooltip.css +8 -5
- package/src/useDialogFocus.ts +11 -2
- package/src/CloseButton.vue +0 -69
- package/src/ColorPicker.vue +0 -201
- package/src/ConfirmDialog.vue +0 -165
- package/src/RangeSlider.vue +0 -183
- package/src/Toggle.vue +0 -36
- package/src/WpButton.vue +0 -75
- package/src/WpCheck.vue +0 -104
- package/src/_skeleton.css +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/DumiFlex/wp-kit/compare/v0.5.0...v0.6.0) (2026-11-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* 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.
|
|
9
|
+
* 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.
|
|
10
|
+
* 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.
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **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))
|
|
15
|
+
* **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))
|
|
16
|
+
* 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))
|
|
17
|
+
* 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))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* 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))
|
|
23
|
+
* 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))
|
|
24
|
+
* 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))
|
|
25
|
+
* 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))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Refactoring
|
|
29
|
+
|
|
30
|
+
* 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))
|
|
31
|
+
* 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))
|
|
32
|
+
* 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))
|
|
33
|
+
* 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))
|
|
34
|
+
|
|
3
35
|
## [0.5.0](https://github.com/DumiFlex/wp-kit/compare/v0.4.0...v0.5.0) (2026-11-02)
|
|
4
36
|
|
|
5
37
|
|
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.
|
|
3
|
+
"version": "0.6.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
|
-
"
|
|
57
|
-
"
|
|
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
|
|
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
|
|
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
|
-
<
|
|
64
|
-
|
|
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>
|
package/src/Button.vue
CHANGED
|
@@ -1,103 +1,132 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Button
|
|
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
|
|
7
|
-
* secondary
|
|
8
|
-
* ghost
|
|
9
|
-
* danger
|
|
10
|
-
* link
|
|
11
|
-
* outline
|
|
12
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
|
23
|
-
*
|
|
31
|
+
* Loading keeps the variant's colours, swaps the lead icon for the sparkle,
|
|
32
|
+
* sets aria-busy and ignores clicks.
|
|
24
33
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
95
|
+
iconOnly.value && "wp-btn--icon",
|
|
78
96
|
props.block && "wp-btn--block",
|
|
79
97
|
]);
|
|
80
98
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
<
|
|
89
|
-
:
|
|
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="
|
|
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="
|
|
125
|
+
<LoadingMark v-if="loading" variant="sparkle" mono :size="markSize" decorative />
|
|
99
126
|
<Icon v-else-if="icon" :name="icon" />
|
|
100
|
-
<span v-if="
|
|
127
|
+
<span v-if="!iconOnly" class="wp-btn__label"><slot /></span>
|
|
101
128
|
<Icon v-if="iconRight" :name="iconRight" />
|
|
102
|
-
</
|
|
129
|
+
</component>
|
|
103
130
|
</template>
|
|
131
|
+
|
|
132
|
+
<style scoped src="./button.css"></style>
|
package/src/Checkbox.vue
CHANGED
|
@@ -1,109 +1,247 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The one checkbox, for the manager, the community site and canvas widgets.
|
|
4
|
+
*
|
|
5
|
+
* <Checkbox v-model="picked" label="Include in bundle" />
|
|
6
|
+
* <Checkbox v-model="on" size="sm" aria-label="Enabled" /> a row's box
|
|
7
|
+
* <Checkbox :model-value="false" indeterminate label="Some selected" />
|
|
8
|
+
* <Checkbox v-model="favorites" field label="Favorites only" />
|
|
9
|
+
*
|
|
10
|
+
* Box: a `<button role="checkbox">`, so a wrapping `<label>` ticks it like an
|
|
11
|
+
* input. Click, Space and Enter toggle. `size="sm"` is the 12px box of the
|
|
12
|
+
* canvas rows' on/off mark; md is 14px. A parent can tint the checked fill
|
|
13
|
+
* with `--check-on` (a bundle header uses its bundle colour).
|
|
14
|
+
*
|
|
15
|
+
* Without a label the box is the root: `class`, `title`, `data-test` and a
|
|
16
|
+
* parent's scoped styles land on it. With a label (prop or the default slot)
|
|
17
|
+
* or `field`, the root is a wrapper around the box and the words, and a click
|
|
18
|
+
* on the words toggles too.
|
|
19
|
+
*
|
|
20
|
+
* The styles are scoped and read only SHARED_TOKENS, so the box ships in the
|
|
21
|
+
* canvas chunk and wears each side's colours.
|
|
22
|
+
*/
|
|
23
|
+
import { computed, useAttrs, useId, useSlots } from "vue";
|
|
24
|
+
|
|
25
|
+
defineOptions({ inheritAttrs: false });
|
|
3
26
|
|
|
4
27
|
interface Props {
|
|
5
|
-
|
|
28
|
+
/** Checked state: use with `v-model` or `:model-value` + `@update:model-value`. */
|
|
29
|
+
modelValue?: boolean | undefined;
|
|
6
30
|
label?: string | undefined;
|
|
7
31
|
disabled?: boolean | undefined;
|
|
8
32
|
error?: boolean | undefined;
|
|
9
33
|
id?: string | undefined;
|
|
10
34
|
ariaLabel?: string | undefined;
|
|
35
|
+
size?: "sm" | "md" | undefined;
|
|
11
36
|
/**
|
|
12
|
-
* Tri-state indicator. When true AND `modelValue` is false, the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* controls where the underlying collection is partially selected.
|
|
16
|
-
* `modelValue=true` always wins — a fully-checked box never
|
|
17
|
-
* renders as indeterminate even if the prop is set.
|
|
37
|
+
* Tri-state indicator. When true AND `modelValue` is false, the box shows a
|
|
38
|
+
* dash and announces `aria-checked="mixed"` (a select-all over a partial
|
|
39
|
+
* selection). `modelValue=true` always wins.
|
|
18
40
|
*/
|
|
19
41
|
indeterminate?: boolean | undefined;
|
|
20
42
|
/**
|
|
21
43
|
* Draw the whole row as an input-height field (border, fill, full width)
|
|
22
|
-
* whose label fills it, for a yes/no filter
|
|
23
|
-
*
|
|
44
|
+
* whose label fills it, for a yes/no filter in a form grid beside Selects
|
|
45
|
+
* (the list pages' "Favorites only").
|
|
24
46
|
*/
|
|
25
47
|
field?: boolean | undefined;
|
|
26
48
|
}
|
|
27
|
-
const props = defineProps<Props>()
|
|
49
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
50
|
+
modelValue: false,
|
|
51
|
+
label: undefined,
|
|
52
|
+
disabled: false,
|
|
53
|
+
error: false,
|
|
54
|
+
id: undefined,
|
|
55
|
+
ariaLabel: undefined,
|
|
56
|
+
size: "md",
|
|
57
|
+
indeterminate: false,
|
|
58
|
+
field: false,
|
|
59
|
+
});
|
|
28
60
|
|
|
29
|
-
const emit = defineEmits<{
|
|
30
|
-
(e: "update:modelValue", v: boolean): void;
|
|
31
|
-
}>();
|
|
61
|
+
const emit = defineEmits<{ "update:modelValue": [next: boolean] }>();
|
|
32
62
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
);
|
|
63
|
+
const attrs = useAttrs();
|
|
64
|
+
const slots = useSlots();
|
|
65
|
+
const labelId = useId();
|
|
36
66
|
|
|
67
|
+
const hasLabel = computed(() => !!props.label || !!slots.default);
|
|
68
|
+
/** The wrapper is a real box (label beside the box, or the field look). */
|
|
69
|
+
const wrapped = computed(() => hasLabel.value || props.field);
|
|
70
|
+
const showIndeterminate = computed(() => props.indeterminate && !props.modelValue);
|
|
37
71
|
const ariaChecked = computed<"true" | "false" | "mixed">(() => {
|
|
38
72
|
if (props.modelValue) return "true";
|
|
39
|
-
|
|
40
|
-
return "false";
|
|
73
|
+
return showIndeterminate.value ? "mixed" : "false";
|
|
41
74
|
});
|
|
42
75
|
|
|
43
|
-
function toggle() {
|
|
76
|
+
function toggle(): void {
|
|
44
77
|
if (props.disabled) return;
|
|
45
78
|
emit("update:modelValue", !props.modelValue);
|
|
46
79
|
}
|
|
47
80
|
</script>
|
|
48
81
|
|
|
49
82
|
<template>
|
|
50
|
-
<span
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
<span
|
|
84
|
+
v-if="wrapped"
|
|
85
|
+
v-bind="attrs"
|
|
86
|
+
class="wp-checkbox"
|
|
87
|
+
:class="[`wp-checkbox--${size}`, { 'wp-checkbox--field': field, 'is-disabled': disabled }]"
|
|
88
|
+
>
|
|
89
|
+
<!-- With a label the root is a <span>, not a <label>: a native label
|
|
90
|
+
re-dispatches the click on its control in some pointer paths (a click
|
|
91
|
+
on the tick SVG), which toggled twice and left the model unchanged.
|
|
92
|
+
No comment sits beside the roots: it would make the root a fragment. -->
|
|
57
93
|
<button
|
|
94
|
+
:id="id"
|
|
95
|
+
:aria-label="ariaLabel ?? label"
|
|
96
|
+
:aria-labelledby="!ariaLabel && !label ? labelId : undefined"
|
|
58
97
|
type="button"
|
|
98
|
+
role="checkbox"
|
|
59
99
|
class="wp-check"
|
|
100
|
+
:class="{ 'wp-check--sm': size === 'sm' }"
|
|
60
101
|
:data-checked="modelValue ? 'true' : 'false'"
|
|
61
102
|
:data-indeterminate="showIndeterminate ? 'true' : 'false'"
|
|
62
103
|
:aria-checked="ariaChecked"
|
|
63
|
-
:aria-label="ariaLabel ?? label"
|
|
64
104
|
:aria-invalid="error || undefined"
|
|
105
|
+
:aria-disabled="disabled || undefined"
|
|
65
106
|
:disabled="disabled"
|
|
66
|
-
role="checkbox"
|
|
67
107
|
@click="toggle"
|
|
108
|
+
@keydown.enter.prevent="toggle"
|
|
68
109
|
>
|
|
69
|
-
<!--
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
paths in some browsers/automation drivers and double-fire toggle.
|
|
74
|
-
No inline style here: the TUI look hides the drawn mark (it reads
|
|
75
|
-
[x] instead), and an inline display would beat it. -->
|
|
76
|
-
<svg
|
|
77
|
-
v-if="modelValue"
|
|
78
|
-
viewBox="0 0 12 12"
|
|
79
|
-
fill="none"
|
|
80
|
-
aria-hidden="true"
|
|
81
|
-
>
|
|
82
|
-
<path
|
|
83
|
-
d="M3 6.2l2.2 2.2L9 4.4"
|
|
84
|
-
stroke="currentColor"
|
|
85
|
-
stroke-width="1.8"
|
|
86
|
-
stroke-linecap="round"
|
|
87
|
-
stroke-linejoin="round"
|
|
88
|
-
/>
|
|
110
|
+
<!-- No inline style on the marks: the TUI look hides them (it writes
|
|
111
|
+
[x] / [-]) and an inline display would beat that rule. -->
|
|
112
|
+
<svg v-if="modelValue" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
113
|
+
<path d="M3 6.2l2.2 2.2L9 4.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
89
114
|
</svg>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
v-else-if="showIndeterminate"
|
|
93
|
-
viewBox="0 0 12 12"
|
|
94
|
-
fill="none"
|
|
95
|
-
aria-hidden="true"
|
|
96
|
-
>
|
|
97
|
-
<path
|
|
98
|
-
d="M3 6h6"
|
|
99
|
-
stroke="currentColor"
|
|
100
|
-
stroke-width="1.8"
|
|
101
|
-
stroke-linecap="round"
|
|
102
|
-
/>
|
|
115
|
+
<svg v-else-if="showIndeterminate" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
116
|
+
<path d="M3 6h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
|
103
117
|
</svg>
|
|
104
118
|
</button>
|
|
105
119
|
<!-- A click on the words toggles too, like a <label>; keyboard users use the box itself. -->
|
|
106
120
|
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
|
|
107
|
-
<span
|
|
121
|
+
<span :id="labelId" class="wp-checkbox__label" @click="toggle"><slot>{{ label }}</slot></span>
|
|
108
122
|
</span>
|
|
123
|
+
<button
|
|
124
|
+
v-else
|
|
125
|
+
v-bind="attrs"
|
|
126
|
+
:id="id"
|
|
127
|
+
:aria-label="ariaLabel"
|
|
128
|
+
type="button"
|
|
129
|
+
role="checkbox"
|
|
130
|
+
class="wp-check"
|
|
131
|
+
:class="{ 'wp-check--sm': size === 'sm' }"
|
|
132
|
+
:data-checked="modelValue ? 'true' : 'false'"
|
|
133
|
+
:data-indeterminate="showIndeterminate ? 'true' : 'false'"
|
|
134
|
+
:aria-checked="ariaChecked"
|
|
135
|
+
:aria-invalid="error || undefined"
|
|
136
|
+
:aria-disabled="disabled || undefined"
|
|
137
|
+
:disabled="disabled"
|
|
138
|
+
@click="toggle"
|
|
139
|
+
@keydown.enter.prevent="toggle"
|
|
140
|
+
>
|
|
141
|
+
<!-- Box only: the box is the root, so the caller's class, scoped styles,
|
|
142
|
+
title and data-test land on it. -->
|
|
143
|
+
<svg v-if="modelValue" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
144
|
+
<path d="M3 6.2l2.2 2.2L9 4.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
145
|
+
</svg>
|
|
146
|
+
<svg v-else-if="showIndeterminate" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
147
|
+
<path d="M3 6h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
|
148
|
+
</svg>
|
|
149
|
+
</button>
|
|
109
150
|
</template>
|
|
151
|
+
|
|
152
|
+
<style scoped>
|
|
153
|
+
/* State selectors sit in :where() so every rule stays at the base box's
|
|
154
|
+
* weight: the looks (theme/looks.css: TUI's [x], Soft's round corners) are
|
|
155
|
+
* one notch heavier and win, as they did over the old global sheet. */
|
|
156
|
+
.wp-checkbox {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 8px;
|
|
160
|
+
min-height: var(--wp-control-h-md);
|
|
161
|
+
color: var(--wp-text);
|
|
162
|
+
font: var(--wp-text-sm) / 1.3 var(--wp-font-sans);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
user-select: none;
|
|
165
|
+
}
|
|
166
|
+
.wp-checkbox--sm { gap: 6px; min-height: var(--wp-control-h-sm); font-size: var(--wp-text-xs); }
|
|
167
|
+
.wp-checkbox:where(.is-disabled) { cursor: not-allowed; }
|
|
168
|
+
.wp-checkbox:where(.is-disabled) .wp-checkbox__label { opacity: 0.5; }
|
|
169
|
+
|
|
170
|
+
.wp-check {
|
|
171
|
+
appearance: none;
|
|
172
|
+
box-sizing: border-box;
|
|
173
|
+
width: 14px;
|
|
174
|
+
height: 14px;
|
|
175
|
+
flex-shrink: 0;
|
|
176
|
+
display: inline-flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
padding: 0;
|
|
180
|
+
margin: 0;
|
|
181
|
+
/* A visible edge on the empty box: the hairline border tokens vanish on a dark row. */
|
|
182
|
+
border: 1.5px solid color-mix(in srgb, var(--wp-text-dim) 70%, transparent);
|
|
183
|
+
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
|
184
|
+
background: var(--wp-bg);
|
|
185
|
+
color: transparent;
|
|
186
|
+
font: inherit;
|
|
187
|
+
line-height: 0;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
transition: background var(--wp-motion-quick), border-color var(--wp-motion-quick), color var(--wp-motion-quick);
|
|
190
|
+
}
|
|
191
|
+
.wp-check svg { width: 9px; height: 9px; display: block; pointer-events: none; }
|
|
192
|
+
.wp-check--sm { width: 12px; height: 12px; border-width: 1px; border-radius: min(2px, var(--wp-radius-cap, 999px)); }
|
|
193
|
+
.wp-check--sm svg { width: 7px; height: 7px; }
|
|
194
|
+
.wp-check:focus-visible { outline: none; box-shadow: var(--wp-focus-ring); }
|
|
195
|
+
.wp-check:where(:not(:disabled, [aria-checked="true"], [aria-checked="mixed"]):hover) { border-color: var(--wp-accent-500); }
|
|
196
|
+
.wp-check:where([aria-invalid="true"]) { border-color: var(--wp-error-border); }
|
|
197
|
+
/* accent-600 under a white mark: ≥ 4.5:1 in both themes. */
|
|
198
|
+
.wp-check:where([aria-checked="true"], [aria-checked="mixed"]) {
|
|
199
|
+
background: var(--check-on, var(--wp-accent-600));
|
|
200
|
+
border-color: var(--check-on, var(--wp-accent-500));
|
|
201
|
+
color: var(--wp-on-accent);
|
|
202
|
+
}
|
|
203
|
+
/* Disabled: grey, so a ticked disabled box doesn't read as a live one. */
|
|
204
|
+
.wp-check:where(:disabled) {
|
|
205
|
+
cursor: not-allowed;
|
|
206
|
+
background: var(--wp-bg-3);
|
|
207
|
+
border-color: var(--wp-border);
|
|
208
|
+
opacity: 0.6;
|
|
209
|
+
}
|
|
210
|
+
.wp-check:where(:disabled[aria-checked="true"], :disabled[aria-checked="mixed"]) {
|
|
211
|
+
background: var(--wp-text-dim);
|
|
212
|
+
border-color: var(--wp-text-dim);
|
|
213
|
+
color: var(--wp-bg-1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Field look: an input-height box in a form grid. The label fills it, so a
|
|
217
|
+
* click anywhere toggles once (the label toggles, the box toggles). */
|
|
218
|
+
.wp-checkbox--field {
|
|
219
|
+
display: flex;
|
|
220
|
+
width: 100%;
|
|
221
|
+
height: var(--wp-control-h-md);
|
|
222
|
+
box-sizing: border-box;
|
|
223
|
+
gap: 0;
|
|
224
|
+
background: var(--wp-bg-2);
|
|
225
|
+
border: 1px solid var(--wp-border);
|
|
226
|
+
border-radius: var(--wp-radius-sm);
|
|
227
|
+
transition: border-color var(--wp-motion-quick);
|
|
228
|
+
}
|
|
229
|
+
.wp-checkbox--field.wp-checkbox--sm { height: var(--wp-control-h-sm); }
|
|
230
|
+
.wp-checkbox--field:hover { border-color: var(--wp-border-strong); }
|
|
231
|
+
.wp-checkbox--field:focus-within { border-color: var(--wp-accent-500); }
|
|
232
|
+
.wp-checkbox--field .wp-check { margin-left: 10px; }
|
|
233
|
+
.wp-checkbox--field .wp-checkbox__label {
|
|
234
|
+
flex: 1;
|
|
235
|
+
align-self: stretch;
|
|
236
|
+
display: flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
padding: 0 10px 0 8px;
|
|
239
|
+
white-space: nowrap;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
text-overflow: ellipsis;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@media (prefers-reduced-motion: reduce) {
|
|
245
|
+
.wp-check, .wp-checkbox--field { transition: none; }
|
|
246
|
+
}
|
|
247
|
+
</style>
|
package/src/Chip.vue
CHANGED
|
@@ -94,7 +94,7 @@ function onRemove(e: MouseEvent) {
|
|
|
94
94
|
<span v-if="dot" class="wp-chip__dot" aria-hidden="true" />
|
|
95
95
|
<!-- 10px sub-ICON_SM: the chip font is 10.5px and a 12px icon read
|
|
96
96
|
oversized next to it. Larger chips get a larger icon. -->
|
|
97
|
-
<Icon v-if="icon" class="wp-chip__icon" :name="icon" :size="size === 'xs' ? 10 :
|
|
97
|
+
<Icon v-if="icon" class="wp-chip__icon" :name="icon" :size="size === 'xs' ? 10 : 12" />
|
|
98
98
|
<slot />
|
|
99
99
|
<span v-if="count !== undefined" class="wp-chip__count">{{ count }}</span>
|
|
100
100
|
<button
|
|
@@ -113,7 +113,10 @@ function onRemove(e: MouseEvent) {
|
|
|
113
113
|
</template>
|
|
114
114
|
|
|
115
115
|
<style scoped>
|
|
116
|
-
/* xs is the base `.wp-chip` in theme/tokens.css
|
|
116
|
+
/* xs is the base `.wp-chip` in theme/tokens.css, held at 20px here: its
|
|
117
|
+
content height was 19.2px, which put every icon half a pixel off. */
|
|
118
|
+
.wp-chip--xs { box-sizing: border-box; height: 20px; }
|
|
119
|
+
/* xs is the base `.wp-chip` in theme/tokens.css. sm and md sit on the
|
|
117
120
|
control height scale so a filter row lines up with the fields beside it. */
|
|
118
121
|
.wp-chip--sm, .wp-chip--md {
|
|
119
122
|
height: var(--wp-control-h-sm);
|
package/src/CodeBlock.vue
CHANGED
|
@@ -107,7 +107,7 @@ const name = computed(() => props.title ?? props.language ?? "Code");
|
|
|
107
107
|
margin: 0; padding: 0; border: 0; border-radius: var(--wp-radius-sm);
|
|
108
108
|
background: transparent; color: var(--wp-text-muted); cursor: pointer;
|
|
109
109
|
}
|
|
110
|
-
.wp-code__tool .pi { font-size:
|
|
110
|
+
.wp-code__tool .pi { font-size: 12px; }
|
|
111
111
|
.wp-code__tool:hover { background: var(--wp-row-hover); color: var(--wp-text); }
|
|
112
112
|
.wp-code__tool[aria-pressed="true"] { color: var(--wp-accent-text); background: var(--wp-bg-3); }
|
|
113
113
|
.wp-code__tool:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
|