@dumiflex/wp-kit 0.3.0 → 0.4.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 +77 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +119 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +292 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +125 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +173 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +175 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +82 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- package/src/RatingBadge.vue +0 -42
package/src/WpTabs.vue
CHANGED
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
<script setup lang="ts" generic="T extends string">
|
|
2
|
-
/**
|
|
3
|
-
* Tabs: a row of tabs that switch the panel under them. Works on the canvas
|
|
4
|
-
* (`size="sm"`) and in the manager (`md`, the default).
|
|
5
|
-
*
|
|
6
|
-
* Keyboard follows the APG tabs pattern with automatic activation: only the
|
|
7
|
-
* current tab is in the Tab order, the arrow keys move to the next enabled
|
|
8
|
-
* tab and select it, Home/End jump to the ends. A row too wide for its box
|
|
9
|
-
* scrolls sideways and keeps the current tab in view.
|
|
10
|
-
*
|
|
11
|
-
* <WpTabs v-model="tab" :tabs="[{ id: 'options', label: 'Options', count: 12 }]" aria-label="Editor sections">
|
|
12
|
-
* <template #panel="{ id }">…</template>
|
|
13
|
-
* </WpTabs>
|
|
14
|
-
*
|
|
15
|
-
* Leave out `#panel` when the panels live elsewhere; give them
|
|
16
|
-
* `:id="panelId(tab)"`-style wiring yourself or let the tabs stand alone.
|
|
17
|
-
*/
|
|
18
|
-
import { nextTick, ref, useId, useSlots } from "vue";
|
|
19
|
-
|
|
20
|
-
export interface TabItem<I extends string = string> {
|
|
21
|
-
id: I;
|
|
22
|
-
label: string;
|
|
23
|
-
/** A PrimeIcons class, e.g. `pi-list`. */
|
|
24
|
-
icon?: string | undefined;
|
|
25
|
-
disabled?: boolean | undefined;
|
|
26
|
-
/** A small count after the label (rows, errors…). */
|
|
27
|
-
count?: number | string | undefined;
|
|
28
|
-
/** Colour of the count: `success` for all good, `warn` for problems. */
|
|
29
|
-
tone?: "neutral" | "success" | "warn" | "danger" | undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const props = withDefaults(defineProps<{
|
|
33
|
-
modelValue: T;
|
|
34
|
-
tabs: TabItem<T>[];
|
|
35
|
-
size?: "sm" | "md" | undefined;
|
|
36
|
-
ariaLabel?: string | undefined;
|
|
37
|
-
/** `data-test` of each tab is `<testPrefix>-<id>`. */
|
|
38
|
-
testPrefix?: string | undefined;
|
|
39
|
-
}>(), { size: "md" });
|
|
40
|
-
|
|
41
|
-
const emit = defineEmits<{ (e: "update:modelValue", id: T): void }>();
|
|
42
|
-
const slots = useSlots();
|
|
43
|
-
|
|
44
|
-
const uid = `wp-tabs-${useId()}`;
|
|
45
|
-
const tabId = (id: string): string => `${uid}-tab-${id}`;
|
|
46
|
-
const panelId = (id: string): string => `${uid}-panel-${id}`;
|
|
47
|
-
const listEl = ref<HTMLElement | null>(null);
|
|
48
|
-
|
|
49
|
-
function select(id: T): void {
|
|
50
|
-
if (id !== props.modelValue) emit("update:modelValue", id);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function focusTab(id: string): void {
|
|
54
|
-
void nextTick(() => {
|
|
55
|
-
const i = props.tabs.findIndex((t) => t.id === id);
|
|
56
|
-
const el = listEl.value?.querySelectorAll<HTMLElement>("[role=tab]")[i];
|
|
57
|
-
el?.focus();
|
|
58
|
-
el?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** The tab that takes Tab focus: the current one, else the first enabled. */
|
|
63
|
-
function tabStop(): T | undefined {
|
|
64
|
-
const enabled = props.tabs.filter((t) => !t.disabled);
|
|
65
|
-
return (enabled.find((t) => t.id === props.modelValue) ?? enabled[0])?.id;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function onKeydown(ev: KeyboardEvent): void {
|
|
69
|
-
const enabled = props.tabs.filter((t) => !t.disabled);
|
|
70
|
-
if (!enabled.length) return;
|
|
71
|
-
const at = enabled.findIndex((t) => t.id === props.modelValue);
|
|
72
|
-
let next: number;
|
|
73
|
-
switch (ev.key) {
|
|
74
|
-
case "ArrowRight": next = (at + 1) % enabled.length; break;
|
|
75
|
-
case "ArrowLeft": next = at < 0 ? enabled.length - 1 : (at - 1 + enabled.length) % enabled.length; break;
|
|
76
|
-
case "Home": next = 0; break;
|
|
77
|
-
case "End": next = enabled.length - 1; break;
|
|
78
|
-
default: return;
|
|
79
|
-
}
|
|
80
|
-
ev.preventDefault();
|
|
81
|
-
const id = enabled[next].id;
|
|
82
|
-
select(id);
|
|
83
|
-
focusTab(id);
|
|
84
|
-
}
|
|
85
|
-
</script>
|
|
86
|
-
|
|
87
|
-
<template>
|
|
88
|
-
<div class="wp-tabset" :data-size="size">
|
|
89
|
-
<div
|
|
90
|
-
ref="listEl"
|
|
91
|
-
class="wp-tabset__list"
|
|
92
|
-
role="tablist"
|
|
93
|
-
:aria-label="ariaLabel"
|
|
94
|
-
>
|
|
95
|
-
<button
|
|
96
|
-
v-for="t in tabs"
|
|
97
|
-
:id="tabId(t.id)"
|
|
98
|
-
:key="t.id"
|
|
99
|
-
type="button"
|
|
100
|
-
role="tab"
|
|
101
|
-
class="wp-tabset__tab"
|
|
102
|
-
:aria-selected="t.id === modelValue"
|
|
103
|
-
:aria-controls="slots.panel ? panelId(t.id) : undefined"
|
|
104
|
-
:tabindex="t.id === tabStop() ? 0 : -1"
|
|
105
|
-
:disabled="t.disabled"
|
|
106
|
-
:data-active="t.id === modelValue"
|
|
107
|
-
:data-test="testPrefix ? `${testPrefix}-${t.id}` : undefined"
|
|
108
|
-
@click="select(t.id)"
|
|
109
|
-
@keydown="onKeydown"
|
|
110
|
-
>
|
|
111
|
-
<slot name="tab" :tab="t" :active="t.id === modelValue">
|
|
112
|
-
<i v-if="t.icon" class="pi" :class="t.icon" aria-hidden="true" />
|
|
113
|
-
<span>{{ t.label }}</span>
|
|
114
|
-
<span v-if="t.count !== undefined" class="wp-tabset__count" :data-tone="t.tone">{{ t.count }}</span>
|
|
115
|
-
</slot>
|
|
116
|
-
</button>
|
|
117
|
-
</div>
|
|
118
|
-
<div
|
|
119
|
-
v-if="slots.panel"
|
|
120
|
-
:id="panelId(modelValue)"
|
|
121
|
-
class="wp-tabset__panel"
|
|
122
|
-
role="tabpanel"
|
|
123
|
-
:aria-labelledby="tabId(modelValue)"
|
|
124
|
-
tabindex="0"
|
|
125
|
-
>
|
|
126
|
-
<slot :id="modelValue" name="panel" />
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
</template>
|
|
130
|
-
|
|
131
|
-
<style scoped>
|
|
132
|
-
.wp-tabset { display: flex; flex-direction: column; min-width: 0; }
|
|
133
|
-
.wp-tabset__list {
|
|
134
|
-
display: flex;
|
|
135
|
-
gap: 2px;
|
|
136
|
-
border-bottom: 1px solid var(--wp-border);
|
|
137
|
-
overflow-x: auto;
|
|
138
|
-
scrollbar-width: thin;
|
|
139
|
-
scrollbar-color: var(--wp-scrollbar-thumb) transparent;
|
|
140
|
-
}
|
|
141
|
-
.wp-tabset__tab {
|
|
142
|
-
display: inline-flex;
|
|
143
|
-
align-items: center;
|
|
144
|
-
gap: 6px;
|
|
145
|
-
flex: none;
|
|
146
|
-
height: var(--wp-control-h-md);
|
|
147
|
-
margin: 0 0 -1px;
|
|
148
|
-
padding: 0 var(--wp-space-5);
|
|
149
|
-
border: 0;
|
|
150
|
-
border-bottom: 2px solid transparent;
|
|
151
|
-
background: none;
|
|
152
|
-
color: var(--wp-text-muted);
|
|
153
|
-
font: 500 var(--wp-text-sm)/1 var(--wp-font-sans);
|
|
154
|
-
white-space: nowrap;
|
|
155
|
-
cursor: pointer;
|
|
156
|
-
transition: color var(--wp-motion-hover), border-color var(--wp-motion-hover);
|
|
157
|
-
}
|
|
158
|
-
.wp-tabset[data-size="sm"] .wp-tabset__tab {
|
|
159
|
-
height: var(--wp-control-h-sm);
|
|
160
|
-
padding: 0 var(--wp-space-4);
|
|
161
|
-
font-size: var(--wp-text-xs);
|
|
162
|
-
gap: 4px;
|
|
163
|
-
}
|
|
164
|
-
.wp-tabset__tab:hover:not(:disabled) { color: var(--wp-text); }
|
|
165
|
-
.wp-tabset__tab[data-active="true"] { color: var(--wp-text); border-bottom-color: var(--wp-accent-400); }
|
|
166
|
-
.wp-tabset__tab:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--wp-border-focus); border-radius: var(--wp-radius-sm); }
|
|
167
|
-
.wp-tabset__tab:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
168
|
-
.wp-tabset__tab .pi { font-size: 0.95em; }
|
|
169
|
-
.wp-tabset__count {
|
|
170
|
-
min-width: 16px;
|
|
171
|
-
padding: 1px 5px;
|
|
172
|
-
border-radius: var(--wp-radius-cap, 999px);
|
|
173
|
-
background: var(--wp-bg-3);
|
|
174
|
-
color: var(--wp-text-muted);
|
|
175
|
-
font-size: var(--wp-text-xs);
|
|
176
|
-
font-variant-numeric: tabular-nums;
|
|
177
|
-
text-align: center;
|
|
178
|
-
}
|
|
179
|
-
.wp-tabset__tab[data-active="true"] .wp-tabset__count { background: var(--wp-accent-glow); color: var(--wp-accent-text); }
|
|
180
|
-
.wp-tabset__count[data-tone="success"] { background: color-mix(in srgb, var(--wp-success) 16%, transparent); color: var(--wp-success); }
|
|
181
|
-
.wp-tabset__count[data-tone="warn"] { background: color-mix(in srgb, var(--wp-warn) 16%, transparent); color: var(--wp-warn); }
|
|
182
|
-
.wp-tabset__count[data-tone="danger"] { background: color-mix(in srgb, var(--wp-danger) 16%, transparent); color: var(--wp-danger); }
|
|
183
|
-
.wp-tabset__panel { min-width: 0; }
|
|
184
|
-
.wp-tabset__panel:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); border-radius: var(--wp-radius-sm); }
|
|
185
|
-
@media (prefers-reduced-motion: reduce) {
|
|
186
|
-
.wp-tabset__tab { transition: none; }
|
|
187
|
-
}
|
|
188
|
-
</style>
|
|
1
|
+
<script setup lang="ts" generic="T extends string">
|
|
2
|
+
/**
|
|
3
|
+
* Tabs: a row of tabs that switch the panel under them. Works on the canvas
|
|
4
|
+
* (`size="sm"`) and in the manager (`md`, the default).
|
|
5
|
+
*
|
|
6
|
+
* Keyboard follows the APG tabs pattern with automatic activation: only the
|
|
7
|
+
* current tab is in the Tab order, the arrow keys move to the next enabled
|
|
8
|
+
* tab and select it, Home/End jump to the ends. A row too wide for its box
|
|
9
|
+
* scrolls sideways and keeps the current tab in view.
|
|
10
|
+
*
|
|
11
|
+
* <WpTabs v-model="tab" :tabs="[{ id: 'options', label: 'Options', count: 12 }]" aria-label="Editor sections">
|
|
12
|
+
* <template #panel="{ id }">…</template>
|
|
13
|
+
* </WpTabs>
|
|
14
|
+
*
|
|
15
|
+
* Leave out `#panel` when the panels live elsewhere; give them
|
|
16
|
+
* `:id="panelId(tab)"`-style wiring yourself or let the tabs stand alone.
|
|
17
|
+
*/
|
|
18
|
+
import { nextTick, ref, useId, useSlots } from "vue";
|
|
19
|
+
|
|
20
|
+
export interface TabItem<I extends string = string> {
|
|
21
|
+
id: I;
|
|
22
|
+
label: string;
|
|
23
|
+
/** A PrimeIcons class, e.g. `pi-list`. */
|
|
24
|
+
icon?: string | undefined;
|
|
25
|
+
disabled?: boolean | undefined;
|
|
26
|
+
/** A small count after the label (rows, errors…). */
|
|
27
|
+
count?: number | string | undefined;
|
|
28
|
+
/** Colour of the count: `success` for all good, `warn` for problems. */
|
|
29
|
+
tone?: "neutral" | "success" | "warn" | "danger" | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const props = withDefaults(defineProps<{
|
|
33
|
+
modelValue: T;
|
|
34
|
+
tabs: TabItem<T>[];
|
|
35
|
+
size?: "sm" | "md" | undefined;
|
|
36
|
+
ariaLabel?: string | undefined;
|
|
37
|
+
/** `data-test` of each tab is `<testPrefix>-<id>`. */
|
|
38
|
+
testPrefix?: string | undefined;
|
|
39
|
+
}>(), { size: "md" });
|
|
40
|
+
|
|
41
|
+
const emit = defineEmits<{ (e: "update:modelValue", id: T): void }>();
|
|
42
|
+
const slots = useSlots();
|
|
43
|
+
|
|
44
|
+
const uid = `wp-tabs-${useId()}`;
|
|
45
|
+
const tabId = (id: string): string => `${uid}-tab-${id}`;
|
|
46
|
+
const panelId = (id: string): string => `${uid}-panel-${id}`;
|
|
47
|
+
const listEl = ref<HTMLElement | null>(null);
|
|
48
|
+
|
|
49
|
+
function select(id: T): void {
|
|
50
|
+
if (id !== props.modelValue) emit("update:modelValue", id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function focusTab(id: string): void {
|
|
54
|
+
void nextTick(() => {
|
|
55
|
+
const i = props.tabs.findIndex((t) => t.id === id);
|
|
56
|
+
const el = listEl.value?.querySelectorAll<HTMLElement>("[role=tab]")[i];
|
|
57
|
+
el?.focus();
|
|
58
|
+
el?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The tab that takes Tab focus: the current one, else the first enabled. */
|
|
63
|
+
function tabStop(): T | undefined {
|
|
64
|
+
const enabled = props.tabs.filter((t) => !t.disabled);
|
|
65
|
+
return (enabled.find((t) => t.id === props.modelValue) ?? enabled[0])?.id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function onKeydown(ev: KeyboardEvent): void {
|
|
69
|
+
const enabled = props.tabs.filter((t) => !t.disabled);
|
|
70
|
+
if (!enabled.length) return;
|
|
71
|
+
const at = enabled.findIndex((t) => t.id === props.modelValue);
|
|
72
|
+
let next: number;
|
|
73
|
+
switch (ev.key) {
|
|
74
|
+
case "ArrowRight": next = (at + 1) % enabled.length; break;
|
|
75
|
+
case "ArrowLeft": next = at < 0 ? enabled.length - 1 : (at - 1 + enabled.length) % enabled.length; break;
|
|
76
|
+
case "Home": next = 0; break;
|
|
77
|
+
case "End": next = enabled.length - 1; break;
|
|
78
|
+
default: return;
|
|
79
|
+
}
|
|
80
|
+
ev.preventDefault();
|
|
81
|
+
const id = enabled[next].id;
|
|
82
|
+
select(id);
|
|
83
|
+
focusTab(id);
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<div class="wp-tabset" :data-size="size">
|
|
89
|
+
<div
|
|
90
|
+
ref="listEl"
|
|
91
|
+
class="wp-tabset__list"
|
|
92
|
+
role="tablist"
|
|
93
|
+
:aria-label="ariaLabel"
|
|
94
|
+
>
|
|
95
|
+
<button
|
|
96
|
+
v-for="t in tabs"
|
|
97
|
+
:id="tabId(t.id)"
|
|
98
|
+
:key="t.id"
|
|
99
|
+
type="button"
|
|
100
|
+
role="tab"
|
|
101
|
+
class="wp-tabset__tab"
|
|
102
|
+
:aria-selected="t.id === modelValue"
|
|
103
|
+
:aria-controls="slots.panel ? panelId(t.id) : undefined"
|
|
104
|
+
:tabindex="t.id === tabStop() ? 0 : -1"
|
|
105
|
+
:disabled="t.disabled"
|
|
106
|
+
:data-active="t.id === modelValue"
|
|
107
|
+
:data-test="testPrefix ? `${testPrefix}-${t.id}` : undefined"
|
|
108
|
+
@click="select(t.id)"
|
|
109
|
+
@keydown="onKeydown"
|
|
110
|
+
>
|
|
111
|
+
<slot name="tab" :tab="t" :active="t.id === modelValue">
|
|
112
|
+
<i v-if="t.icon" class="pi" :class="t.icon" aria-hidden="true" />
|
|
113
|
+
<span>{{ t.label }}</span>
|
|
114
|
+
<span v-if="t.count !== undefined" class="wp-tabset__count" :data-tone="t.tone">{{ t.count }}</span>
|
|
115
|
+
</slot>
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
<div
|
|
119
|
+
v-if="slots.panel"
|
|
120
|
+
:id="panelId(modelValue)"
|
|
121
|
+
class="wp-tabset__panel"
|
|
122
|
+
role="tabpanel"
|
|
123
|
+
:aria-labelledby="tabId(modelValue)"
|
|
124
|
+
tabindex="0"
|
|
125
|
+
>
|
|
126
|
+
<slot :id="modelValue" name="panel" />
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
.wp-tabset { display: flex; flex-direction: column; min-width: 0; }
|
|
133
|
+
.wp-tabset__list {
|
|
134
|
+
display: flex;
|
|
135
|
+
gap: 2px;
|
|
136
|
+
border-bottom: 1px solid var(--wp-border);
|
|
137
|
+
overflow-x: auto;
|
|
138
|
+
scrollbar-width: thin;
|
|
139
|
+
scrollbar-color: var(--wp-scrollbar-thumb) transparent;
|
|
140
|
+
}
|
|
141
|
+
.wp-tabset__tab {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 6px;
|
|
145
|
+
flex: none;
|
|
146
|
+
height: var(--wp-control-h-md);
|
|
147
|
+
margin: 0 0 -1px;
|
|
148
|
+
padding: 0 var(--wp-space-5);
|
|
149
|
+
border: 0;
|
|
150
|
+
border-bottom: 2px solid transparent;
|
|
151
|
+
background: none;
|
|
152
|
+
color: var(--wp-text-muted);
|
|
153
|
+
font: 500 var(--wp-text-sm)/1 var(--wp-font-sans);
|
|
154
|
+
white-space: nowrap;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
transition: color var(--wp-motion-hover), border-color var(--wp-motion-hover);
|
|
157
|
+
}
|
|
158
|
+
.wp-tabset[data-size="sm"] .wp-tabset__tab {
|
|
159
|
+
height: var(--wp-control-h-sm);
|
|
160
|
+
padding: 0 var(--wp-space-4);
|
|
161
|
+
font-size: var(--wp-text-xs);
|
|
162
|
+
gap: 4px;
|
|
163
|
+
}
|
|
164
|
+
.wp-tabset__tab:hover:not(:disabled) { color: var(--wp-text); }
|
|
165
|
+
.wp-tabset__tab[data-active="true"] { color: var(--wp-text); border-bottom-color: var(--wp-accent-400); }
|
|
166
|
+
.wp-tabset__tab:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--wp-border-focus); border-radius: var(--wp-radius-sm); }
|
|
167
|
+
.wp-tabset__tab:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
168
|
+
.wp-tabset__tab .pi { font-size: 0.95em; }
|
|
169
|
+
.wp-tabset__count {
|
|
170
|
+
min-width: 16px;
|
|
171
|
+
padding: 1px 5px;
|
|
172
|
+
border-radius: var(--wp-radius-cap, 999px);
|
|
173
|
+
background: var(--wp-bg-3);
|
|
174
|
+
color: var(--wp-text-muted);
|
|
175
|
+
font-size: var(--wp-text-xs);
|
|
176
|
+
font-variant-numeric: tabular-nums;
|
|
177
|
+
text-align: center;
|
|
178
|
+
}
|
|
179
|
+
.wp-tabset__tab[data-active="true"] .wp-tabset__count { background: var(--wp-accent-glow); color: var(--wp-accent-text); }
|
|
180
|
+
.wp-tabset__count[data-tone="success"] { background: color-mix(in srgb, var(--wp-success) 16%, transparent); color: var(--wp-success); }
|
|
181
|
+
.wp-tabset__count[data-tone="warn"] { background: color-mix(in srgb, var(--wp-warn) 16%, transparent); color: var(--wp-warn); }
|
|
182
|
+
.wp-tabset__count[data-tone="danger"] { background: color-mix(in srgb, var(--wp-danger) 16%, transparent); color: var(--wp-danger); }
|
|
183
|
+
.wp-tabset__panel { min-width: 0; }
|
|
184
|
+
.wp-tabset__panel:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); border-radius: var(--wp-radius-sm); }
|
|
185
|
+
@media (prefers-reduced-motion: reduce) {
|
|
186
|
+
.wp-tabset__tab { transition: none; }
|
|
187
|
+
}
|
|
188
|
+
</style>
|
package/src/_modal-motion.css
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
/* Shared modal open/close transition — drives every consumer that
|
|
2
|
-
* wraps its modal in `<Transition name="wp-modal">`. Two simultaneous
|
|
3
|
-
* legs:
|
|
4
|
-
* 1. Overlay: opacity fade across `--wp-motion-swap` (200ms).
|
|
5
|
-
* 2. Panel: transform scale 0.96 → 1.0 with the decel curve so
|
|
6
|
-
* the dialog grows into place.
|
|
7
|
-
*
|
|
8
|
-
* Selector for the panel is `> *` (immediate child of the overlay
|
|
9
|
-
* element). Works for ModalShell's `.wp-modal-wrapper`, ConfirmDialog's
|
|
10
|
-
* `.wp-confirm`, InjectorBindingModal's `.ibm`, and any future modal
|
|
11
|
-
* that follows the overlay-with-one-child layout.
|
|
12
|
-
*
|
|
13
|
-
* Imported globally via ContextWidget's unscoped style block — class
|
|
14
|
-
* names are namespaced (`wp-modal-*` for the transition, scoped per
|
|
15
|
-
* Vue's named-transition convention) so no leakage.
|
|
16
|
-
*/
|
|
17
|
-
.wp-modal-enter-active,
|
|
18
|
-
.wp-modal-leave-active {
|
|
19
|
-
transition: opacity var(--wp-motion-swap) ease;
|
|
20
|
-
}
|
|
21
|
-
.wp-modal-enter-from,
|
|
22
|
-
.wp-modal-leave-to {
|
|
23
|
-
opacity: 0;
|
|
24
|
-
}
|
|
25
|
-
.wp-modal-enter-active > *,
|
|
26
|
-
.wp-modal-leave-active > * {
|
|
27
|
-
transition: transform var(--wp-motion-swap) var(--wp-motion-curve-flip);
|
|
28
|
-
transform-origin: center;
|
|
29
|
-
}
|
|
30
|
-
.wp-modal-enter-from > *,
|
|
31
|
-
.wp-modal-leave-to > * {
|
|
32
|
-
transform: scale(0.96);
|
|
33
|
-
}
|
|
1
|
+
/* Shared modal open/close transition — drives every consumer that
|
|
2
|
+
* wraps its modal in `<Transition name="wp-modal">`. Two simultaneous
|
|
3
|
+
* legs:
|
|
4
|
+
* 1. Overlay: opacity fade across `--wp-motion-swap` (200ms).
|
|
5
|
+
* 2. Panel: transform scale 0.96 → 1.0 with the decel curve so
|
|
6
|
+
* the dialog grows into place.
|
|
7
|
+
*
|
|
8
|
+
* Selector for the panel is `> *` (immediate child of the overlay
|
|
9
|
+
* element). Works for ModalShell's `.wp-modal-wrapper`, ConfirmDialog's
|
|
10
|
+
* `.wp-confirm`, InjectorBindingModal's `.ibm`, and any future modal
|
|
11
|
+
* that follows the overlay-with-one-child layout.
|
|
12
|
+
*
|
|
13
|
+
* Imported globally via ContextWidget's unscoped style block — class
|
|
14
|
+
* names are namespaced (`wp-modal-*` for the transition, scoped per
|
|
15
|
+
* Vue's named-transition convention) so no leakage.
|
|
16
|
+
*/
|
|
17
|
+
.wp-modal-enter-active,
|
|
18
|
+
.wp-modal-leave-active {
|
|
19
|
+
transition: opacity var(--wp-motion-swap) ease;
|
|
20
|
+
}
|
|
21
|
+
.wp-modal-enter-from,
|
|
22
|
+
.wp-modal-leave-to {
|
|
23
|
+
opacity: 0;
|
|
24
|
+
}
|
|
25
|
+
.wp-modal-enter-active > *,
|
|
26
|
+
.wp-modal-leave-active > * {
|
|
27
|
+
transition: transform var(--wp-motion-swap) var(--wp-motion-curve-flip);
|
|
28
|
+
transform-origin: center;
|
|
29
|
+
}
|
|
30
|
+
.wp-modal-enter-from > *,
|
|
31
|
+
.wp-modal-leave-to > * {
|
|
32
|
+
transform: scale(0.96);
|
|
33
|
+
}
|
package/src/_skeleton.css
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
/* Skeleton loading frames.
|
|
2
|
-
*
|
|
3
|
-
* Used by modals + widgets that surface async API loads (bundle picker,
|
|
4
|
-
* module picker). The flat base background is token-driven so the
|
|
5
|
-
* frames track theme changes; a moving highlight gradient overlays
|
|
6
|
-
* via the `::after` pseudo so consumers don't need to mark up two
|
|
7
|
-
* elements. Animation loops on `--wp-motion-shimmer` (1400ms).
|
|
8
|
-
*
|
|
9
|
-
* Imported by each consumer's scoped style block — the `wp-skel`
|
|
10
|
-
* class names are intentionally global so a parent's scoped wrapper
|
|
11
|
-
* (`wp-bp__skel-row` etc.) can lay out the shapes while the base
|
|
12
|
-
* `wp-skel` rules drive the look. `prefers-reduced-motion` kills the
|
|
13
|
-
* sweep so reduced-motion users still get a flat placeholder.
|
|
14
|
-
*/
|
|
15
|
-
.wp-skel {
|
|
16
|
-
background: var(--wp-bg2);
|
|
17
|
-
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
18
|
-
position: relative;
|
|
19
|
-
overflow: hidden;
|
|
20
|
-
}
|
|
21
|
-
.wp-skel::after {
|
|
22
|
-
content: "";
|
|
23
|
-
position: absolute;
|
|
24
|
-
inset: 0;
|
|
25
|
-
background: linear-gradient(
|
|
26
|
-
90deg,
|
|
27
|
-
transparent 0%,
|
|
28
|
-
color-mix(in srgb, var(--wp-text) 8%, transparent) 50%,
|
|
29
|
-
transparent 100%
|
|
30
|
-
);
|
|
31
|
-
transform: translateX(-100%);
|
|
32
|
-
animation: wp-skel-shimmer var(--wp-motion-shimmer) ease-in-out infinite;
|
|
33
|
-
}
|
|
34
|
-
.wp-skel--text {
|
|
35
|
-
height: 11px;
|
|
36
|
-
}
|
|
37
|
-
.wp-skel--circle {
|
|
38
|
-
border-radius: 50%; /* round: stands in for an avatar */
|
|
39
|
-
}
|
|
40
|
-
.wp-skel--pill {
|
|
41
|
-
border-radius: var(--wp-radius-cap, 999px);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@keyframes wp-skel-shimmer {
|
|
45
|
-
100% { transform: translateX(100%); }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
@media (prefers-reduced-motion: reduce) {
|
|
49
|
-
.wp-skel::after { animation: none; }
|
|
50
|
-
}
|
|
1
|
+
/* Skeleton loading frames.
|
|
2
|
+
*
|
|
3
|
+
* Used by modals + widgets that surface async API loads (bundle picker,
|
|
4
|
+
* module picker). The flat base background is token-driven so the
|
|
5
|
+
* frames track theme changes; a moving highlight gradient overlays
|
|
6
|
+
* via the `::after` pseudo so consumers don't need to mark up two
|
|
7
|
+
* elements. Animation loops on `--wp-motion-shimmer` (1400ms).
|
|
8
|
+
*
|
|
9
|
+
* Imported by each consumer's scoped style block — the `wp-skel`
|
|
10
|
+
* class names are intentionally global so a parent's scoped wrapper
|
|
11
|
+
* (`wp-bp__skel-row` etc.) can lay out the shapes while the base
|
|
12
|
+
* `wp-skel` rules drive the look. `prefers-reduced-motion` kills the
|
|
13
|
+
* sweep so reduced-motion users still get a flat placeholder.
|
|
14
|
+
*/
|
|
15
|
+
.wp-skel {
|
|
16
|
+
background: var(--wp-bg2);
|
|
17
|
+
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
18
|
+
position: relative;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
.wp-skel::after {
|
|
22
|
+
content: "";
|
|
23
|
+
position: absolute;
|
|
24
|
+
inset: 0;
|
|
25
|
+
background: linear-gradient(
|
|
26
|
+
90deg,
|
|
27
|
+
transparent 0%,
|
|
28
|
+
color-mix(in srgb, var(--wp-text) 8%, transparent) 50%,
|
|
29
|
+
transparent 100%
|
|
30
|
+
);
|
|
31
|
+
transform: translateX(-100%);
|
|
32
|
+
animation: wp-skel-shimmer var(--wp-motion-shimmer) ease-in-out infinite;
|
|
33
|
+
}
|
|
34
|
+
.wp-skel--text {
|
|
35
|
+
height: 11px;
|
|
36
|
+
}
|
|
37
|
+
.wp-skel--circle {
|
|
38
|
+
border-radius: 50%; /* round: stands in for an avatar */
|
|
39
|
+
}
|
|
40
|
+
.wp-skel--pill {
|
|
41
|
+
border-radius: var(--wp-radius-cap, 999px);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@keyframes wp-skel-shimmer {
|
|
45
|
+
100% { transform: translateX(100%); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@media (prefers-reduced-motion: reduce) {
|
|
49
|
+
.wp-skel::after { animation: none; }
|
|
50
|
+
}
|
package/src/axis-color.ts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-axis sub-category colours — one source of truth for the wildcard
|
|
3
|
-
* editor pills, the option-row tag chips, and the pool palette, so a tag
|
|
4
|
-
* reads with the same colour on every surface.
|
|
5
|
-
*
|
|
6
|
-
* Each sub-category GROUP (axis) gets a distinct hue by its position among
|
|
7
|
-
* its siblings. The old per-component copies shared TWO failings that both
|
|
8
|
-
* produced "two groups, same colour": the palette held two near-identical
|
|
9
|
-
* purples (`#a78bfa` and `#a970ff`), and assignment wrapped with
|
|
10
|
-
* `% length`, repeating once there were more groups than hues. This palette
|
|
11
|
-
* is de-duplicated and ordered for maximum separation between consecutive
|
|
12
|
-
* axes; past it, a golden-angle hue keeps every additional group distinct.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** Neutral hue for ungrouped / "other" tags. */
|
|
16
|
-
export const UNGROUPED_HUE = "var(--wp-text-dim, var(--wp-text3))";
|
|
17
|
-
|
|
18
|
-
/** Curated distinct hues (no two similar), ordered so consecutive axes land
|
|
19
|
-
* far apart on the wheel. Theme tokens where they exist, hex otherwise. */
|
|
20
|
-
const AXIS_HUES = [
|
|
21
|
-
"var(--wp-kind-wildcard, #a78bfa)", // purple
|
|
22
|
-
"var(--wp-teal, #33d6c6)", // teal
|
|
23
|
-
"var(--wp-status-modified, #fb923c)", // orange
|
|
24
|
-
"#60a5fa", // blue
|
|
25
|
-
"var(--wp-success, #22c55e)", // green
|
|
26
|
-
"#fb7185", // rose
|
|
27
|
-
"#fbbf24", // amber
|
|
28
|
-
"#e879f9", // magenta
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Distinct, non-repeating hue for the sub-category group (axis) at `index`
|
|
33
|
-
* among its siblings. `index < 0` (ungrouped) → the neutral token. Past the
|
|
34
|
-
* curated palette, a golden-angle hue keeps additional groups distinct
|
|
35
|
-
* rather than wrapping back onto an earlier colour.
|
|
36
|
-
*/
|
|
37
|
-
export function axisHueAt(index: number): string {
|
|
38
|
-
if (index < 0) return UNGROUPED_HUE;
|
|
39
|
-
if (index < AXIS_HUES.length) return AXIS_HUES[index];
|
|
40
|
-
const hue = ((index * 137.508) % 360).toFixed(1);
|
|
41
|
-
return `hsl(${hue} 68% 66%)`;
|
|
42
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Per-axis sub-category colours — one source of truth for the wildcard
|
|
3
|
+
* editor pills, the option-row tag chips, and the pool palette, so a tag
|
|
4
|
+
* reads with the same colour on every surface.
|
|
5
|
+
*
|
|
6
|
+
* Each sub-category GROUP (axis) gets a distinct hue by its position among
|
|
7
|
+
* its siblings. The old per-component copies shared TWO failings that both
|
|
8
|
+
* produced "two groups, same colour": the palette held two near-identical
|
|
9
|
+
* purples (`#a78bfa` and `#a970ff`), and assignment wrapped with
|
|
10
|
+
* `% length`, repeating once there were more groups than hues. This palette
|
|
11
|
+
* is de-duplicated and ordered for maximum separation between consecutive
|
|
12
|
+
* axes; past it, a golden-angle hue keeps every additional group distinct.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Neutral hue for ungrouped / "other" tags. */
|
|
16
|
+
export const UNGROUPED_HUE = "var(--wp-text-dim, var(--wp-text3))";
|
|
17
|
+
|
|
18
|
+
/** Curated distinct hues (no two similar), ordered so consecutive axes land
|
|
19
|
+
* far apart on the wheel. Theme tokens where they exist, hex otherwise. */
|
|
20
|
+
const AXIS_HUES = [
|
|
21
|
+
"var(--wp-kind-wildcard, #a78bfa)", // purple
|
|
22
|
+
"var(--wp-teal, #33d6c6)", // teal
|
|
23
|
+
"var(--wp-status-modified, #fb923c)", // orange
|
|
24
|
+
"#60a5fa", // blue
|
|
25
|
+
"var(--wp-success, #22c55e)", // green
|
|
26
|
+
"#fb7185", // rose
|
|
27
|
+
"#fbbf24", // amber
|
|
28
|
+
"#e879f9", // magenta
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Distinct, non-repeating hue for the sub-category group (axis) at `index`
|
|
33
|
+
* among its siblings. `index < 0` (ungrouped) → the neutral token. Past the
|
|
34
|
+
* curated palette, a golden-angle hue keeps additional groups distinct
|
|
35
|
+
* rather than wrapping back onto an earlier colour.
|
|
36
|
+
*/
|
|
37
|
+
export function axisHueAt(index: number): string {
|
|
38
|
+
if (index < 0) return UNGROUPED_HUE;
|
|
39
|
+
if (index < AXIS_HUES.length) return AXIS_HUES[index];
|
|
40
|
+
const hue = ((index * 137.508) % 360).toFixed(1);
|
|
41
|
+
return `hsl(${hue} 68% 66%)`;
|
|
42
|
+
}
|