@dumiflex/wp-kit 0.3.0 → 0.5.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 +92 -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 +146 -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 +293 -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 +127 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +176 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +217 -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 +98 -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/highlight.ts +199 -0
- 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/loading-mark-motion.ts +249 -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/SplitPane.vue
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Split pane: two panels side by side (or one above the other) with a
|
|
4
|
-
* handle between them that drags, takes focus and moves with the arrow
|
|
5
|
-
* keys. v-model is the first panel's size in px; the second panel takes the
|
|
6
|
-
* rest. Works on the canvas and in the manager.
|
|
7
|
-
*
|
|
8
|
-
* <SplitPane v-model="listWidth" :min="200" :max="520" :default-size="280" label="List width">
|
|
9
|
-
* <template #start>…list…</template>
|
|
10
|
-
* <template #end>…detail…</template>
|
|
11
|
-
* </SplitPane>
|
|
12
|
-
*
|
|
13
|
-
* The parent decides the pane's own height (or width, when stacked). Keep
|
|
14
|
-
* the size in a ref, or in the user's settings to remember it.
|
|
15
|
-
*/
|
|
16
|
-
import { useId } from "vue";
|
|
17
|
-
import ResizeHandle from "./ResizeHandle.vue";
|
|
18
|
-
|
|
19
|
-
const props = withDefaults(defineProps<{
|
|
20
|
-
/** The first panel's size in px. */
|
|
21
|
-
modelValue: number;
|
|
22
|
-
/** horizontal: side by side. vertical: one above the other. */
|
|
23
|
-
orientation?: "horizontal" | "vertical" | undefined;
|
|
24
|
-
min?: number | undefined;
|
|
25
|
-
max?: number | undefined;
|
|
26
|
-
defaultSize?: number | undefined;
|
|
27
|
-
/** What the handle resizes, for screen readers. */
|
|
28
|
-
label?: string | undefined;
|
|
29
|
-
disabled?: boolean | undefined;
|
|
30
|
-
}>(), { orientation: "horizontal", min: 120, max: Number.POSITIVE_INFINITY, defaultSize: undefined, label: "Resize panels" });
|
|
31
|
-
|
|
32
|
-
const emit = defineEmits<{ (e: "update:modelValue", v: number): void; (e: "end", v: number): void }>();
|
|
33
|
-
const startId = `wp-split-${useId()}`;
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<template>
|
|
37
|
-
<div class="wp-split" :data-orientation="orientation">
|
|
38
|
-
<div
|
|
39
|
-
:id="startId"
|
|
40
|
-
class="wp-split__start"
|
|
41
|
-
:style="orientation === 'horizontal' ? { width: `${modelValue}px` } : { height: `${modelValue}px` }"
|
|
42
|
-
>
|
|
43
|
-
<slot name="start" />
|
|
44
|
-
</div>
|
|
45
|
-
<ResizeHandle
|
|
46
|
-
:model-value="modelValue"
|
|
47
|
-
:axis="orientation === 'horizontal' ? 'x' : 'y'"
|
|
48
|
-
:min="min"
|
|
49
|
-
:max="max"
|
|
50
|
-
:default-size="defaultSize"
|
|
51
|
-
:label="props.label"
|
|
52
|
-
:controls="startId"
|
|
53
|
-
:disabled="disabled"
|
|
54
|
-
@update:model-value="emit('update:modelValue', $event)"
|
|
55
|
-
@end="emit('end', $event)"
|
|
56
|
-
/>
|
|
57
|
-
<div class="wp-split__end"><slot name="end" /></div>
|
|
58
|
-
</div>
|
|
59
|
-
</template>
|
|
60
|
-
|
|
61
|
-
<style scoped>
|
|
62
|
-
.wp-split { display: flex; width: 100%; height: 100%; min-width: 0; min-height: 0; }
|
|
63
|
-
.wp-split[data-orientation="vertical"] { flex-direction: column; }
|
|
64
|
-
.wp-split__start { flex: none; min-width: 0; min-height: 0; overflow: auto; }
|
|
65
|
-
.wp-split__end { flex: 1; min-width: 0; min-height: 0; overflow: auto; }
|
|
66
|
-
.wp-split[data-orientation="horizontal"] > .wp-split__start { border-right: 1px solid var(--wp-border); }
|
|
67
|
-
.wp-split[data-orientation="vertical"] > .wp-split__start { border-bottom: 1px solid var(--wp-border); }
|
|
68
|
-
</style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Split pane: two panels side by side (or one above the other) with a
|
|
4
|
+
* handle between them that drags, takes focus and moves with the arrow
|
|
5
|
+
* keys. v-model is the first panel's size in px; the second panel takes the
|
|
6
|
+
* rest. Works on the canvas and in the manager.
|
|
7
|
+
*
|
|
8
|
+
* <SplitPane v-model="listWidth" :min="200" :max="520" :default-size="280" label="List width">
|
|
9
|
+
* <template #start>…list…</template>
|
|
10
|
+
* <template #end>…detail…</template>
|
|
11
|
+
* </SplitPane>
|
|
12
|
+
*
|
|
13
|
+
* The parent decides the pane's own height (or width, when stacked). Keep
|
|
14
|
+
* the size in a ref, or in the user's settings to remember it.
|
|
15
|
+
*/
|
|
16
|
+
import { useId } from "vue";
|
|
17
|
+
import ResizeHandle from "./ResizeHandle.vue";
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
/** The first panel's size in px. */
|
|
21
|
+
modelValue: number;
|
|
22
|
+
/** horizontal: side by side. vertical: one above the other. */
|
|
23
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
24
|
+
min?: number | undefined;
|
|
25
|
+
max?: number | undefined;
|
|
26
|
+
defaultSize?: number | undefined;
|
|
27
|
+
/** What the handle resizes, for screen readers. */
|
|
28
|
+
label?: string | undefined;
|
|
29
|
+
disabled?: boolean | undefined;
|
|
30
|
+
}>(), { orientation: "horizontal", min: 120, max: Number.POSITIVE_INFINITY, defaultSize: undefined, label: "Resize panels" });
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{ (e: "update:modelValue", v: number): void; (e: "end", v: number): void }>();
|
|
33
|
+
const startId = `wp-split-${useId()}`;
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="wp-split" :data-orientation="orientation">
|
|
38
|
+
<div
|
|
39
|
+
:id="startId"
|
|
40
|
+
class="wp-split__start"
|
|
41
|
+
:style="orientation === 'horizontal' ? { width: `${modelValue}px` } : { height: `${modelValue}px` }"
|
|
42
|
+
>
|
|
43
|
+
<slot name="start" />
|
|
44
|
+
</div>
|
|
45
|
+
<ResizeHandle
|
|
46
|
+
:model-value="modelValue"
|
|
47
|
+
:axis="orientation === 'horizontal' ? 'x' : 'y'"
|
|
48
|
+
:min="min"
|
|
49
|
+
:max="max"
|
|
50
|
+
:default-size="defaultSize"
|
|
51
|
+
:label="props.label"
|
|
52
|
+
:controls="startId"
|
|
53
|
+
:disabled="disabled"
|
|
54
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
55
|
+
@end="emit('end', $event)"
|
|
56
|
+
/>
|
|
57
|
+
<div class="wp-split__end"><slot name="end" /></div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
.wp-split { display: flex; width: 100%; height: 100%; min-width: 0; min-height: 0; }
|
|
63
|
+
.wp-split[data-orientation="vertical"] { flex-direction: column; }
|
|
64
|
+
.wp-split__start { flex: none; min-width: 0; min-height: 0; overflow: auto; }
|
|
65
|
+
.wp-split__end { flex: 1; min-width: 0; min-height: 0; overflow: auto; }
|
|
66
|
+
.wp-split[data-orientation="horizontal"] > .wp-split__start { border-right: 1px solid var(--wp-border); }
|
|
67
|
+
.wp-split[data-orientation="vertical"] > .wp-split__start { border-bottom: 1px solid var(--wp-border); }
|
|
68
|
+
</style>
|
package/src/StarButton.vue
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Star a post: a toggle Button with a filled star when on and the star count
|
|
4
|
-
* beside the label. The app owns the request; this only shows the state and
|
|
5
|
-
* emits `toggle`.
|
|
6
|
-
*
|
|
7
|
-
* <StarButton :starred="post.starred_by_me" :count="post.star_count" :disabled="pending" @toggle="star" />
|
|
8
|
-
*/
|
|
9
|
-
import Button from "./Button.vue";
|
|
10
|
-
|
|
11
|
-
withDefaults(defineProps<{
|
|
12
|
-
starred: boolean;
|
|
13
|
-
count?: number | undefined;
|
|
14
|
-
disabled?: boolean | undefined;
|
|
15
|
-
size?: "sm" | "md" | "lg" | undefined;
|
|
16
|
-
}>(), { count: undefined, disabled: false, size: "md" });
|
|
17
|
-
|
|
18
|
-
const emit = defineEmits<{ (e: "toggle"): void }>();
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
<template>
|
|
22
|
-
<Button
|
|
23
|
-
:icon="starred ? 'star-fill' : 'star'"
|
|
24
|
-
:pressed="starred"
|
|
25
|
-
:disabled="disabled"
|
|
26
|
-
:size="size"
|
|
27
|
-
@click="emit('toggle')"
|
|
28
|
-
>
|
|
29
|
-
{{ starred ? "Starred" : "Star" }}
|
|
30
|
-
<span v-if="count !== undefined" class="wp-star-button__count">{{ count.toLocaleString() }}</span>
|
|
31
|
-
</Button>
|
|
32
|
-
</template>
|
|
33
|
-
|
|
34
|
-
<style scoped>
|
|
35
|
-
.wp-star-button__count {
|
|
36
|
-
margin-left: 2px;
|
|
37
|
-
color: var(--wp-text-muted);
|
|
38
|
-
font-variant-numeric: tabular-nums;
|
|
39
|
-
}
|
|
40
|
-
</style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Star a post: a toggle Button with a filled star when on and the star count
|
|
4
|
+
* beside the label. The app owns the request; this only shows the state and
|
|
5
|
+
* emits `toggle`.
|
|
6
|
+
*
|
|
7
|
+
* <StarButton :starred="post.starred_by_me" :count="post.star_count" :disabled="pending" @toggle="star" />
|
|
8
|
+
*/
|
|
9
|
+
import Button from "./Button.vue";
|
|
10
|
+
|
|
11
|
+
withDefaults(defineProps<{
|
|
12
|
+
starred: boolean;
|
|
13
|
+
count?: number | undefined;
|
|
14
|
+
disabled?: boolean | undefined;
|
|
15
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
16
|
+
}>(), { count: undefined, disabled: false, size: "md" });
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{ (e: "toggle"): void }>();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<Button
|
|
23
|
+
:icon="starred ? 'star-fill' : 'star'"
|
|
24
|
+
:pressed="starred"
|
|
25
|
+
:disabled="disabled"
|
|
26
|
+
:size="size"
|
|
27
|
+
@click="emit('toggle')"
|
|
28
|
+
>
|
|
29
|
+
{{ starred ? "Starred" : "Star" }}
|
|
30
|
+
<span v-if="count !== undefined" class="wp-star-button__count">{{ count.toLocaleString() }}</span>
|
|
31
|
+
</Button>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.wp-star-button__count {
|
|
36
|
+
margin-left: 2px;
|
|
37
|
+
color: var(--wp-text-muted);
|
|
38
|
+
font-variant-numeric: tabular-nums;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
package/src/Stepper.vue
CHANGED
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
<script setup lang="ts" generic="T extends string">
|
|
2
|
-
/**
|
|
3
|
-
* Stepper: the "1 · 2 · 3" header of a job done in steps (the pack builder,
|
|
4
|
-
* an import, a setup). Done steps show a check, a step with a problem shows
|
|
5
|
-
* a mark, and the current one is lit. Works on the canvas and in the manager.
|
|
6
|
-
*
|
|
7
|
-
* <Stepper v-model="step" :steps="[{ id: 'describe', label: 'Describe' }, { id: 'plan', label: 'Plan', state: 'done' }]" clickable />
|
|
8
|
-
*
|
|
9
|
-
* It is an ordered list with `aria-current="step"` on the current step. With
|
|
10
|
-
* `clickable` each step is a button that emits `update:modelValue`; with
|
|
11
|
-
* `linear` only done steps and the next one can be picked.
|
|
12
|
-
*/
|
|
13
|
-
import { computed } from "vue";
|
|
14
|
-
|
|
15
|
-
export interface StepperStep<V extends string = string> {
|
|
16
|
-
id: V;
|
|
17
|
-
label: string;
|
|
18
|
-
/** A short line under the label. */
|
|
19
|
-
hint?: string | undefined;
|
|
20
|
-
/** done shows a check, error a mark. Steps before the current one count as done unless set. */
|
|
21
|
-
state?: "todo" | "done" | "error" | undefined;
|
|
22
|
-
disabled?: boolean | undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const props = withDefaults(defineProps<{
|
|
26
|
-
modelValue: T;
|
|
27
|
-
steps: StepperStep<T>[];
|
|
28
|
-
clickable?: boolean | undefined;
|
|
29
|
-
/** Only done steps and the one after the last done step can be picked. */
|
|
30
|
-
linear?: boolean | undefined;
|
|
31
|
-
orientation?: "horizontal" | "vertical" | undefined;
|
|
32
|
-
size?: "sm" | "md" | undefined;
|
|
33
|
-
ariaLabel?: string | undefined;
|
|
34
|
-
}>(), { orientation: "horizontal", size: "md", ariaLabel: "Steps" });
|
|
35
|
-
|
|
36
|
-
const emit = defineEmits<{ (e: "update:modelValue", v: T): void }>();
|
|
37
|
-
|
|
38
|
-
const current = computed(() => props.steps.findIndex((s) => s.id === props.modelValue));
|
|
39
|
-
|
|
40
|
-
function stateOf(i: number): "todo" | "done" | "error" | "current" {
|
|
41
|
-
const s = props.steps[i];
|
|
42
|
-
if (s.state === "error") return "error";
|
|
43
|
-
if (i === current.value) return "current";
|
|
44
|
-
if (s.state) return s.state;
|
|
45
|
-
return i < current.value ? "done" : "todo";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** The furthest step a linear stepper lets you reach. */
|
|
49
|
-
const reach = computed(() => {
|
|
50
|
-
let i = 0;
|
|
51
|
-
while (i < props.steps.length && (stateOf(i) === "done" || i <= current.value)) i++;
|
|
52
|
-
return i;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
function canPick(i: number): boolean {
|
|
56
|
-
const s = props.steps[i];
|
|
57
|
-
if (!props.clickable || s.disabled) return false;
|
|
58
|
-
return !props.linear || i <= reach.value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const STATE_WORD = { done: "done", error: "has a problem", current: "current step", todo: "" } as const;
|
|
62
|
-
</script>
|
|
63
|
-
|
|
64
|
-
<template>
|
|
65
|
-
<ol class="wp-stepper" :data-orientation="orientation" :data-size="size" :aria-label="ariaLabel">
|
|
66
|
-
<li
|
|
67
|
-
v-for="(s, i) in steps"
|
|
68
|
-
:key="s.id"
|
|
69
|
-
class="wp-stepper__step"
|
|
70
|
-
:data-state="stateOf(i)"
|
|
71
|
-
:aria-current="i === current ? 'step' : undefined"
|
|
72
|
-
>
|
|
73
|
-
<component
|
|
74
|
-
:is="clickable ? 'button' : 'div'"
|
|
75
|
-
class="wp-stepper__body"
|
|
76
|
-
:type="clickable ? 'button' : undefined"
|
|
77
|
-
:disabled="clickable ? !canPick(i) : undefined"
|
|
78
|
-
@click="canPick(i) && s.id !== modelValue && emit('update:modelValue', s.id)"
|
|
79
|
-
>
|
|
80
|
-
<span class="wp-stepper__mark" aria-hidden="true">
|
|
81
|
-
<i v-if="stateOf(i) === 'done'" class="pi pi-check" />
|
|
82
|
-
<i v-else-if="stateOf(i) === 'error'" class="pi pi-exclamation-triangle" />
|
|
83
|
-
<template v-else>{{ i + 1 }}</template>
|
|
84
|
-
</span>
|
|
85
|
-
<span class="wp-stepper__text">
|
|
86
|
-
<span class="wp-stepper__label">{{ s.label }}</span>
|
|
87
|
-
<span v-if="s.hint" class="wp-stepper__hint">{{ s.hint }}</span>
|
|
88
|
-
<span v-if="STATE_WORD[stateOf(i)]" class="wp-stepper__sr">, {{ STATE_WORD[stateOf(i)] }}</span>
|
|
89
|
-
</span>
|
|
90
|
-
</component>
|
|
91
|
-
<span v-if="i < steps.length - 1" class="wp-stepper__line" aria-hidden="true" />
|
|
92
|
-
</li>
|
|
93
|
-
</ol>
|
|
94
|
-
</template>
|
|
95
|
-
|
|
96
|
-
<style scoped>
|
|
97
|
-
.wp-stepper {
|
|
98
|
-
display: flex; align-items: flex-start; gap: var(--wp-space-3);
|
|
99
|
-
margin: 0; padding: 0; list-style: none; min-width: 0;
|
|
100
|
-
font-family: var(--wp-font-sans);
|
|
101
|
-
}
|
|
102
|
-
.wp-stepper[data-orientation="vertical"] { flex-direction: column; gap: 0; }
|
|
103
|
-
.wp-stepper__step { display: flex; align-items: center; gap: var(--wp-space-3); flex: 1 1 0; min-width: 0; }
|
|
104
|
-
.wp-stepper__step:last-child { flex: 0 1 auto; }
|
|
105
|
-
.wp-stepper[data-orientation="vertical"] .wp-stepper__step { flex-direction: column; align-items: flex-start; gap: 0; width: 100%; }
|
|
106
|
-
.wp-stepper__body {
|
|
107
|
-
display: inline-flex; align-items: center; gap: var(--wp-space-3); min-width: 0;
|
|
108
|
-
margin: 0; padding: 2px var(--wp-space-2) 2px 2px; border: 0; border-radius: var(--wp-radius);
|
|
109
|
-
background: transparent; color: var(--wp-text-muted); font: inherit; text-align: left;
|
|
110
|
-
}
|
|
111
|
-
button.wp-stepper__body { cursor: pointer; }
|
|
112
|
-
button.wp-stepper__body:hover:not(:disabled) { background: var(--wp-row-hover); color: var(--wp-text); }
|
|
113
|
-
button.wp-stepper__body:disabled { cursor: default; }
|
|
114
|
-
button.wp-stepper__body:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
115
|
-
.wp-stepper__mark {
|
|
116
|
-
display: inline-grid; place-items: center; flex: none; box-sizing: border-box;
|
|
117
|
-
width: 24px; height: 24px; border-radius: min(50%, var(--wp-radius-cap, 999px));
|
|
118
|
-
border: 1.5px solid var(--wp-border-strong); background: var(--wp-bg-1);
|
|
119
|
-
color: var(--wp-text-muted); font: 600 var(--wp-text-xs)/1 var(--wp-font-sans); font-variant-numeric: tabular-nums;
|
|
120
|
-
}
|
|
121
|
-
.wp-stepper[data-size="sm"] .wp-stepper__mark { width: 20px; height: 20px; font-size: 10px; }
|
|
122
|
-
.wp-stepper__mark .pi { font-size: 10px; }
|
|
123
|
-
.wp-stepper__text { display: flex; flex-direction: column; min-width: 0; }
|
|
124
|
-
.wp-stepper__label { font-size: var(--wp-text-sm); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
125
|
-
.wp-stepper[data-size="sm"] .wp-stepper__label { font-size: var(--wp-text-xs); }
|
|
126
|
-
.wp-stepper__hint { font-size: var(--wp-text-xs); color: var(--wp-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
127
|
-
.wp-stepper__sr {
|
|
128
|
-
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
|
|
129
|
-
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
130
|
-
}
|
|
131
|
-
.wp-stepper__line { flex: 1; min-width: 12px; height: 1.5px; border-radius: min(1px, var(--wp-radius-cap, 999px)); background: var(--wp-border); }
|
|
132
|
-
.wp-stepper[data-orientation="vertical"] .wp-stepper__line { flex: none; width: 1.5px; min-width: 0; height: 16px; margin: 2px 0 2px 13px; }
|
|
133
|
-
.wp-stepper[data-orientation="vertical"][data-size="sm"] .wp-stepper__line { margin-left: 11px; }
|
|
134
|
-
.wp-stepper__step[data-state="done"] .wp-stepper__mark { border-color: var(--wp-accent); background: var(--wp-accent); color: var(--wp-bg); }
|
|
135
|
-
.wp-stepper__step[data-state="done"] .wp-stepper__line { background: var(--wp-accent); }
|
|
136
|
-
.wp-stepper__step[data-state="current"] .wp-stepper__mark { border-color: var(--wp-accent); color: var(--wp-accent-text); box-shadow: 0 0 0 3px var(--wp-accent-glow); }
|
|
137
|
-
.wp-stepper__step[data-state="current"] .wp-stepper__body { color: var(--wp-text); }
|
|
138
|
-
.wp-stepper__step[data-state="current"] .wp-stepper__label { font-weight: 600; }
|
|
139
|
-
.wp-stepper__step[data-state="error"] .wp-stepper__mark { border-color: var(--wp-danger); color: var(--wp-danger); }
|
|
140
|
-
.wp-stepper__step[data-state="error"] .wp-stepper__label { color: var(--wp-danger-text); }
|
|
141
|
-
</style>
|
|
1
|
+
<script setup lang="ts" generic="T extends string">
|
|
2
|
+
/**
|
|
3
|
+
* Stepper: the "1 · 2 · 3" header of a job done in steps (the pack builder,
|
|
4
|
+
* an import, a setup). Done steps show a check, a step with a problem shows
|
|
5
|
+
* a mark, and the current one is lit. Works on the canvas and in the manager.
|
|
6
|
+
*
|
|
7
|
+
* <Stepper v-model="step" :steps="[{ id: 'describe', label: 'Describe' }, { id: 'plan', label: 'Plan', state: 'done' }]" clickable />
|
|
8
|
+
*
|
|
9
|
+
* It is an ordered list with `aria-current="step"` on the current step. With
|
|
10
|
+
* `clickable` each step is a button that emits `update:modelValue`; with
|
|
11
|
+
* `linear` only done steps and the next one can be picked.
|
|
12
|
+
*/
|
|
13
|
+
import { computed } from "vue";
|
|
14
|
+
|
|
15
|
+
export interface StepperStep<V extends string = string> {
|
|
16
|
+
id: V;
|
|
17
|
+
label: string;
|
|
18
|
+
/** A short line under the label. */
|
|
19
|
+
hint?: string | undefined;
|
|
20
|
+
/** done shows a check, error a mark. Steps before the current one count as done unless set. */
|
|
21
|
+
state?: "todo" | "done" | "error" | undefined;
|
|
22
|
+
disabled?: boolean | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<{
|
|
26
|
+
modelValue: T;
|
|
27
|
+
steps: StepperStep<T>[];
|
|
28
|
+
clickable?: boolean | undefined;
|
|
29
|
+
/** Only done steps and the one after the last done step can be picked. */
|
|
30
|
+
linear?: boolean | undefined;
|
|
31
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
32
|
+
size?: "sm" | "md" | undefined;
|
|
33
|
+
ariaLabel?: string | undefined;
|
|
34
|
+
}>(), { orientation: "horizontal", size: "md", ariaLabel: "Steps" });
|
|
35
|
+
|
|
36
|
+
const emit = defineEmits<{ (e: "update:modelValue", v: T): void }>();
|
|
37
|
+
|
|
38
|
+
const current = computed(() => props.steps.findIndex((s) => s.id === props.modelValue));
|
|
39
|
+
|
|
40
|
+
function stateOf(i: number): "todo" | "done" | "error" | "current" {
|
|
41
|
+
const s = props.steps[i];
|
|
42
|
+
if (s.state === "error") return "error";
|
|
43
|
+
if (i === current.value) return "current";
|
|
44
|
+
if (s.state) return s.state;
|
|
45
|
+
return i < current.value ? "done" : "todo";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The furthest step a linear stepper lets you reach. */
|
|
49
|
+
const reach = computed(() => {
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < props.steps.length && (stateOf(i) === "done" || i <= current.value)) i++;
|
|
52
|
+
return i;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function canPick(i: number): boolean {
|
|
56
|
+
const s = props.steps[i];
|
|
57
|
+
if (!props.clickable || s.disabled) return false;
|
|
58
|
+
return !props.linear || i <= reach.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const STATE_WORD = { done: "done", error: "has a problem", current: "current step", todo: "" } as const;
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<ol class="wp-stepper" :data-orientation="orientation" :data-size="size" :aria-label="ariaLabel">
|
|
66
|
+
<li
|
|
67
|
+
v-for="(s, i) in steps"
|
|
68
|
+
:key="s.id"
|
|
69
|
+
class="wp-stepper__step"
|
|
70
|
+
:data-state="stateOf(i)"
|
|
71
|
+
:aria-current="i === current ? 'step' : undefined"
|
|
72
|
+
>
|
|
73
|
+
<component
|
|
74
|
+
:is="clickable ? 'button' : 'div'"
|
|
75
|
+
class="wp-stepper__body"
|
|
76
|
+
:type="clickable ? 'button' : undefined"
|
|
77
|
+
:disabled="clickable ? !canPick(i) : undefined"
|
|
78
|
+
@click="canPick(i) && s.id !== modelValue && emit('update:modelValue', s.id)"
|
|
79
|
+
>
|
|
80
|
+
<span class="wp-stepper__mark" aria-hidden="true">
|
|
81
|
+
<i v-if="stateOf(i) === 'done'" class="pi pi-check" />
|
|
82
|
+
<i v-else-if="stateOf(i) === 'error'" class="pi pi-exclamation-triangle" />
|
|
83
|
+
<template v-else>{{ i + 1 }}</template>
|
|
84
|
+
</span>
|
|
85
|
+
<span class="wp-stepper__text">
|
|
86
|
+
<span class="wp-stepper__label">{{ s.label }}</span>
|
|
87
|
+
<span v-if="s.hint" class="wp-stepper__hint">{{ s.hint }}</span>
|
|
88
|
+
<span v-if="STATE_WORD[stateOf(i)]" class="wp-stepper__sr">, {{ STATE_WORD[stateOf(i)] }}</span>
|
|
89
|
+
</span>
|
|
90
|
+
</component>
|
|
91
|
+
<span v-if="i < steps.length - 1" class="wp-stepper__line" aria-hidden="true" />
|
|
92
|
+
</li>
|
|
93
|
+
</ol>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<style scoped>
|
|
97
|
+
.wp-stepper {
|
|
98
|
+
display: flex; align-items: flex-start; gap: var(--wp-space-3);
|
|
99
|
+
margin: 0; padding: 0; list-style: none; min-width: 0;
|
|
100
|
+
font-family: var(--wp-font-sans);
|
|
101
|
+
}
|
|
102
|
+
.wp-stepper[data-orientation="vertical"] { flex-direction: column; gap: 0; }
|
|
103
|
+
.wp-stepper__step { display: flex; align-items: center; gap: var(--wp-space-3); flex: 1 1 0; min-width: 0; }
|
|
104
|
+
.wp-stepper__step:last-child { flex: 0 1 auto; }
|
|
105
|
+
.wp-stepper[data-orientation="vertical"] .wp-stepper__step { flex-direction: column; align-items: flex-start; gap: 0; width: 100%; }
|
|
106
|
+
.wp-stepper__body {
|
|
107
|
+
display: inline-flex; align-items: center; gap: var(--wp-space-3); min-width: 0;
|
|
108
|
+
margin: 0; padding: 2px var(--wp-space-2) 2px 2px; border: 0; border-radius: var(--wp-radius);
|
|
109
|
+
background: transparent; color: var(--wp-text-muted); font: inherit; text-align: left;
|
|
110
|
+
}
|
|
111
|
+
button.wp-stepper__body { cursor: pointer; }
|
|
112
|
+
button.wp-stepper__body:hover:not(:disabled) { background: var(--wp-row-hover); color: var(--wp-text); }
|
|
113
|
+
button.wp-stepper__body:disabled { cursor: default; }
|
|
114
|
+
button.wp-stepper__body:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
|
|
115
|
+
.wp-stepper__mark {
|
|
116
|
+
display: inline-grid; place-items: center; flex: none; box-sizing: border-box;
|
|
117
|
+
width: 24px; height: 24px; border-radius: min(50%, var(--wp-radius-cap, 999px));
|
|
118
|
+
border: 1.5px solid var(--wp-border-strong); background: var(--wp-bg-1);
|
|
119
|
+
color: var(--wp-text-muted); font: 600 var(--wp-text-xs)/1 var(--wp-font-sans); font-variant-numeric: tabular-nums;
|
|
120
|
+
}
|
|
121
|
+
.wp-stepper[data-size="sm"] .wp-stepper__mark { width: 20px; height: 20px; font-size: 10px; }
|
|
122
|
+
.wp-stepper__mark .pi { font-size: 10px; }
|
|
123
|
+
.wp-stepper__text { display: flex; flex-direction: column; min-width: 0; }
|
|
124
|
+
.wp-stepper__label { font-size: var(--wp-text-sm); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
125
|
+
.wp-stepper[data-size="sm"] .wp-stepper__label { font-size: var(--wp-text-xs); }
|
|
126
|
+
.wp-stepper__hint { font-size: var(--wp-text-xs); color: var(--wp-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
127
|
+
.wp-stepper__sr {
|
|
128
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
|
|
129
|
+
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
130
|
+
}
|
|
131
|
+
.wp-stepper__line { flex: 1; min-width: 12px; height: 1.5px; border-radius: min(1px, var(--wp-radius-cap, 999px)); background: var(--wp-border); }
|
|
132
|
+
.wp-stepper[data-orientation="vertical"] .wp-stepper__line { flex: none; width: 1.5px; min-width: 0; height: 16px; margin: 2px 0 2px 13px; }
|
|
133
|
+
.wp-stepper[data-orientation="vertical"][data-size="sm"] .wp-stepper__line { margin-left: 11px; }
|
|
134
|
+
.wp-stepper__step[data-state="done"] .wp-stepper__mark { border-color: var(--wp-accent); background: var(--wp-accent); color: var(--wp-bg); }
|
|
135
|
+
.wp-stepper__step[data-state="done"] .wp-stepper__line { background: var(--wp-accent); }
|
|
136
|
+
.wp-stepper__step[data-state="current"] .wp-stepper__mark { border-color: var(--wp-accent); color: var(--wp-accent-text); box-shadow: 0 0 0 3px var(--wp-accent-glow); }
|
|
137
|
+
.wp-stepper__step[data-state="current"] .wp-stepper__body { color: var(--wp-text); }
|
|
138
|
+
.wp-stepper__step[data-state="current"] .wp-stepper__label { font-weight: 600; }
|
|
139
|
+
.wp-stepper__step[data-state="error"] .wp-stepper__mark { border-color: var(--wp-danger); color: var(--wp-danger); }
|
|
140
|
+
.wp-stepper__step[data-state="error"] .wp-stepper__label { color: var(--wp-danger-text); }
|
|
141
|
+
</style>
|