@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.
- package/CHANGELOG.md +47 -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/BundlePreview.vue +254 -0
- 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/CombinePreview.vue +150 -0
- package/src/Combobox.vue +10 -14
- package/src/ConfirmModal.vue +12 -22
- package/src/ConstraintMatrixPreview.vue +6 -22
- package/src/ConstraintPreview.vue +259 -0
- 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/DerivationCondition.vue +68 -0
- package/src/DerivationPreview.vue +345 -0
- package/src/Disclosure.vue +2 -1
- package/src/Drawer.vue +2 -2
- package/src/DropZone.vue +2 -2
- package/src/FixedValuesPreview.vue +184 -0
- package/src/HsvPicker.vue +2 -2
- package/src/IconPicker.vue +32 -103
- package/src/IconPickerPanel.vue +132 -0
- package/src/ImageGrid.vue +2 -2
- package/src/Lightbox.vue +2 -2
- package/src/LoadingMark.vue +6 -61
- package/src/Modal.vue +109 -44
- package/src/ModalShell.vue +26 -5
- package/src/Pagination.vue +4 -4
- package/src/PayloadPreview.vue +154 -0
- package/src/Popover.vue +68 -12
- package/src/PostDetailSkeleton.vue +65 -0
- package/src/PostGridSkeleton.vue +61 -0
- package/src/PreviewGroup.vue +75 -0
- package/src/ProfileSkeleton.vue +73 -0
- package/src/RadioGroup.vue +5 -4
- package/src/RichText.vue +61 -0
- package/src/SearchInput.vue +1 -1
- package/src/Select.vue +53 -158
- package/src/SelectionBar.vue +90 -0
- package/src/SettingGroup.vue +57 -0
- package/src/SettingRow.vue +127 -0
- 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/WildcardPreview.vue +607 -0
- package/src/WorkingLine.vue +234 -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/canvas-motion.ts +251 -0
- package/src/floating.ts +5 -0
- package/src/hsv.ts +1 -1
- package/src/kind-colour.ts +24 -1
- package/src/linkify.ts +142 -0
- package/src/loading-mark-motion.ts +12 -155
- package/src/matrix-axis.ts +28 -0
- package/src/menu.css +51 -0
- package/src/motion.css +73 -0
- package/src/pi-icons.ts +51 -0
- package/src/preview-readers.ts +401 -0
- package/src/preview.css +194 -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 +15 -4
- package/src/tooltip.css +8 -5
- package/src/useDialogFocus.ts +11 -2
- package/src/working-line-motion.ts +135 -0
- 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/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); }
|
package/src/ColorField.vue
CHANGED
|
@@ -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
|
|
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
|
-
<
|
|
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</
|
|
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>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Combine: the template in its own block, its negative under it, and a
|
|
3
|
+
* list of the variables it reads with where each one is used. */
|
|
4
|
+
import { computed, ref } from "vue";
|
|
5
|
+
import TokenizedValue from "./TokenizedValue.vue";
|
|
6
|
+
import Button from "./Button.vue";
|
|
7
|
+
import { readString, readStrings, scanText, type Obj } from "./preview-readers";
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
inner: Obj | null;
|
|
11
|
+
refMap: ReadonlyMap<string, string>;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const PAGE = 6;
|
|
15
|
+
|
|
16
|
+
const template = computed(() => readString(props.inner, "template") ?? "");
|
|
17
|
+
const negative = computed(() => readString(props.inner, "negative") ?? "");
|
|
18
|
+
const output = computed(() => readString(props.inner, "output_var"));
|
|
19
|
+
const tplScan = computed(() => scanText(template.value));
|
|
20
|
+
const negScan = computed(() => scanText(negative.value));
|
|
21
|
+
|
|
22
|
+
interface InputRow {
|
|
23
|
+
name: string;
|
|
24
|
+
where: string;
|
|
25
|
+
}
|
|
26
|
+
const inputs = computed<InputRow[]>(() => {
|
|
27
|
+
const names = [
|
|
28
|
+
...new Set([
|
|
29
|
+
...readStrings(props.inner, "input_vars"),
|
|
30
|
+
...tplScan.value.vars,
|
|
31
|
+
...negScan.value.vars,
|
|
32
|
+
...negScan.value.negVars,
|
|
33
|
+
]),
|
|
34
|
+
];
|
|
35
|
+
return names.map((name) => {
|
|
36
|
+
const where: string[] = [];
|
|
37
|
+
if (tplScan.value.vars.includes(name)) where.push("template");
|
|
38
|
+
if (negScan.value.vars.includes(name)) where.push("negative");
|
|
39
|
+
if (negScan.value.negVars.includes(name) || tplScan.value.negVars.includes(name))
|
|
40
|
+
where.push("negative (.neg)");
|
|
41
|
+
return { name, where: where.join(", ") || "declared, not used" };
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
const showAll = ref(false);
|
|
45
|
+
const shown = computed(() => (showAll.value ? inputs.value : inputs.value.slice(0, PAGE)));
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="wp-pv">
|
|
50
|
+
<div class="wp-pv-stats">
|
|
51
|
+
<div class="wp-pv-stat">
|
|
52
|
+
<b>{{ inputs.length }}</b><span>input{{ inputs.length === 1 ? "" : "s" }}</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div
|
|
55
|
+
v-if="tplScan.picks"
|
|
56
|
+
class="wp-pv-stat"
|
|
57
|
+
>
|
|
58
|
+
<b>{{ tplScan.picks }}</b><span>inline pick{{ tplScan.picks === 1 ? "" : "s" }}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="wp-pv-stat">
|
|
61
|
+
<b>{{ negative ? "yes" : "no" }}</b><span>negative</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="wp-pv-sec">
|
|
65
|
+
Template<template v-if="output">
|
|
66
|
+
→ <span class="wp-cb__out">${{ output }}</span>
|
|
67
|
+
</template>
|
|
68
|
+
</div>
|
|
69
|
+
<div
|
|
70
|
+
class="wp-pv-block wp-combine__template"
|
|
71
|
+
data-test="combine-template"
|
|
72
|
+
>
|
|
73
|
+
<TokenizedValue
|
|
74
|
+
:value="template"
|
|
75
|
+
:uuid-to-name="refMap"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
<template v-if="negative">
|
|
79
|
+
<div class="wp-pv-sec wp-cb__neglabel">
|
|
80
|
+
Negative
|
|
81
|
+
</div>
|
|
82
|
+
<div
|
|
83
|
+
class="wp-pv-block wp-pv-block--neg"
|
|
84
|
+
data-test="combine-negative"
|
|
85
|
+
>
|
|
86
|
+
<TokenizedValue
|
|
87
|
+
:value="negative"
|
|
88
|
+
:uuid-to-name="refMap"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
<template v-if="inputs.length">
|
|
93
|
+
<div class="wp-pv-sec">
|
|
94
|
+
Inputs
|
|
95
|
+
</div>
|
|
96
|
+
<table class="wp-pv-table">
|
|
97
|
+
<colgroup>
|
|
98
|
+
<col style="width: 180px">
|
|
99
|
+
<col>
|
|
100
|
+
</colgroup>
|
|
101
|
+
<tbody>
|
|
102
|
+
<tr
|
|
103
|
+
v-for="i in shown"
|
|
104
|
+
:key="i.name"
|
|
105
|
+
data-test="combine-input"
|
|
106
|
+
>
|
|
107
|
+
<td class="wp-pv-var">
|
|
108
|
+
${{ i.name }}
|
|
109
|
+
</td>
|
|
110
|
+
<td :class="{ 'wp-cb__unused': i.where === 'declared, not used' }">
|
|
111
|
+
{{ i.where }}
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
<div
|
|
117
|
+
v-if="inputs.length > PAGE"
|
|
118
|
+
class="wp-pv-more"
|
|
119
|
+
>
|
|
120
|
+
<Button
|
|
121
|
+
variant="link"
|
|
122
|
+
size="sm"
|
|
123
|
+
@click="showAll = !showAll"
|
|
124
|
+
>
|
|
125
|
+
{{ showAll ? "Show fewer" : `Show ${inputs.length - PAGE} more` }}
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
<div class="wp-cb__pad" />
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<style scoped src="./preview.css"></style>
|
|
134
|
+
<style scoped>
|
|
135
|
+
.wp-cb__out {
|
|
136
|
+
font-family: var(--wp-font-mono);
|
|
137
|
+
color: var(--wp-success);
|
|
138
|
+
text-transform: none;
|
|
139
|
+
letter-spacing: 0;
|
|
140
|
+
}
|
|
141
|
+
.wp-cb__neglabel {
|
|
142
|
+
color: var(--wp-danger-text, var(--wp-danger));
|
|
143
|
+
}
|
|
144
|
+
.wp-cb__unused {
|
|
145
|
+
color: var(--wp-text-dim);
|
|
146
|
+
}
|
|
147
|
+
.wp-cb__pad {
|
|
148
|
+
height: 6px;
|
|
149
|
+
}
|
|
150
|
+
</style>
|