@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/ConfirmDialog.vue
CHANGED
|
@@ -1,163 +1,165 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Themed confirmation dialog — replaces `window.confirm()` for any
|
|
4
|
-
* action that needs an in-app prompt (reset overrides, save-to-library,
|
|
5
|
-
* reset-from-library, etc.). Consumers control it via v-model:visible
|
|
6
|
-
* plus title/body/confirm-label slots; the dialog emits `confirm` /
|
|
7
|
-
* `cancel` and closes itself on Esc + overlay click + Cancel button.
|
|
8
|
-
*
|
|
9
|
-
* Variants:
|
|
10
|
-
* - default: accent-coloured Confirm button (constructive actions)
|
|
11
|
-
* - danger: red Confirm button (destructive actions, e.g. discard,
|
|
12
|
-
* reset overrides)
|
|
13
|
-
*/
|
|
14
|
-
import { onBeforeUnmount, watch } from "vue";
|
|
15
|
-
import WpButton from "./WpButton.vue";
|
|
16
|
-
|
|
17
|
-
const props = withDefaults(
|
|
18
|
-
defineProps<{
|
|
19
|
-
visible: boolean;
|
|
20
|
-
title: string;
|
|
21
|
-
body?: string | undefined;
|
|
22
|
-
confirmLabel?: string | undefined;
|
|
23
|
-
cancelLabel?: string | undefined;
|
|
24
|
-
variant?: "default" | "danger" | undefined;
|
|
25
|
-
}>(),
|
|
26
|
-
{
|
|
27
|
-
body: "",
|
|
28
|
-
confirmLabel: "Confirm",
|
|
29
|
-
cancelLabel: "Cancel",
|
|
30
|
-
variant: "default",
|
|
31
|
-
},
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const emit = defineEmits<{
|
|
35
|
-
"confirm": [];
|
|
36
|
-
"cancel": [];
|
|
37
|
-
}>();
|
|
38
|
-
|
|
39
|
-
function onConfirm(): void { emit("confirm"); }
|
|
40
|
-
function onCancel(): void { emit("cancel"); }
|
|
41
|
-
|
|
42
|
-
function onKeydown(ev: KeyboardEvent): void {
|
|
43
|
-
if (!props.visible) return;
|
|
44
|
-
if (ev.key === "Escape") {
|
|
45
|
-
ev.preventDefault();
|
|
46
|
-
emit("cancel");
|
|
47
|
-
} else if (ev.key === "Enter" && !ev.shiftKey) {
|
|
48
|
-
ev.preventDefault();
|
|
49
|
-
emit("confirm");
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
watch(
|
|
54
|
-
() => props.visible,
|
|
55
|
-
(visible) => {
|
|
56
|
-
if (visible) window.addEventListener("keydown", onKeydown);
|
|
57
|
-
else window.removeEventListener("keydown", onKeydown);
|
|
58
|
-
},
|
|
59
|
-
{ immediate: true },
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
|
|
63
|
-
</script>
|
|
64
|
-
|
|
65
|
-
<template>
|
|
66
|
-
<Teleport to="body">
|
|
67
|
-
<Transition name="wp-modal" appear>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
</
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Themed confirmation dialog — replaces `window.confirm()` for any
|
|
4
|
+
* action that needs an in-app prompt (reset overrides, save-to-library,
|
|
5
|
+
* reset-from-library, etc.). Consumers control it via v-model:visible
|
|
6
|
+
* plus title/body/confirm-label slots; the dialog emits `confirm` /
|
|
7
|
+
* `cancel` and closes itself on Esc + overlay click + Cancel button.
|
|
8
|
+
*
|
|
9
|
+
* Variants:
|
|
10
|
+
* - default: accent-coloured Confirm button (constructive actions)
|
|
11
|
+
* - danger: red Confirm button (destructive actions, e.g. discard,
|
|
12
|
+
* reset overrides)
|
|
13
|
+
*/
|
|
14
|
+
import { onBeforeUnmount, watch } from "vue";
|
|
15
|
+
import WpButton from "./WpButton.vue";
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
visible: boolean;
|
|
20
|
+
title: string;
|
|
21
|
+
body?: string | undefined;
|
|
22
|
+
confirmLabel?: string | undefined;
|
|
23
|
+
cancelLabel?: string | undefined;
|
|
24
|
+
variant?: "default" | "danger" | undefined;
|
|
25
|
+
}>(),
|
|
26
|
+
{
|
|
27
|
+
body: "",
|
|
28
|
+
confirmLabel: "Confirm",
|
|
29
|
+
cancelLabel: "Cancel",
|
|
30
|
+
variant: "default",
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<{
|
|
35
|
+
"confirm": [];
|
|
36
|
+
"cancel": [];
|
|
37
|
+
}>();
|
|
38
|
+
|
|
39
|
+
function onConfirm(): void { emit("confirm"); }
|
|
40
|
+
function onCancel(): void { emit("cancel"); }
|
|
41
|
+
|
|
42
|
+
function onKeydown(ev: KeyboardEvent): void {
|
|
43
|
+
if (!props.visible) return;
|
|
44
|
+
if (ev.key === "Escape") {
|
|
45
|
+
ev.preventDefault();
|
|
46
|
+
emit("cancel");
|
|
47
|
+
} else if (ev.key === "Enter" && !ev.shiftKey) {
|
|
48
|
+
ev.preventDefault();
|
|
49
|
+
emit("confirm");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
watch(
|
|
54
|
+
() => props.visible,
|
|
55
|
+
(visible) => {
|
|
56
|
+
if (visible) window.addEventListener("keydown", onKeydown);
|
|
57
|
+
else window.removeEventListener("keydown", onKeydown);
|
|
58
|
+
},
|
|
59
|
+
{ immediate: true },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<Teleport to="body">
|
|
67
|
+
<Transition name="wp-modal" appear>
|
|
68
|
+
<!-- The backdrop is a click target only; keyboard users close with Escape. -->
|
|
69
|
+
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
|
|
70
|
+
<div
|
|
71
|
+
v-if="visible"
|
|
72
|
+
class="wp-confirm-overlay"
|
|
73
|
+
data-test="confirm-overlay"
|
|
74
|
+
@click="onCancel"
|
|
75
|
+
>
|
|
76
|
+
<div
|
|
77
|
+
class="wp-confirm"
|
|
78
|
+
role="alertdialog"
|
|
79
|
+
aria-modal="true"
|
|
80
|
+
:class="`wp-confirm--${variant}`"
|
|
81
|
+
@click.stop
|
|
82
|
+
>
|
|
83
|
+
<div class="wp-confirm__head">
|
|
84
|
+
<i
|
|
85
|
+
class="pi"
|
|
86
|
+
:class="variant === 'danger' ? 'pi-exclamation-triangle' : 'pi-info-circle'"
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
/>
|
|
89
|
+
<h3 class="wp-confirm__title" data-test="confirm-title">{{ title }}</h3>
|
|
90
|
+
</div>
|
|
91
|
+
<p v-if="body" class="wp-confirm__body" data-test="confirm-body">{{ body }}</p>
|
|
92
|
+
<div class="wp-confirm__foot">
|
|
93
|
+
<WpButton data-test="confirm-cancel" @click="onCancel">{{ cancelLabel }}</WpButton>
|
|
94
|
+
<WpButton :variant="variant === 'danger' ? 'danger' : 'primary'" data-test="confirm-confirm" @click="onConfirm">{{ confirmLabel }}</WpButton>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</Transition>
|
|
99
|
+
</Teleport>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
/* Shared modal open/close transition — overlay fade + panel scale.
|
|
104
|
+
* See `_modal-motion.css` for rules. Imported unscoped so the
|
|
105
|
+
* `wp-modal-*` transition class names match the Vue-emitted classes
|
|
106
|
+
* cleanly (named transitions don't survive Vue's `data-v-...`
|
|
107
|
+
* scoping). */
|
|
108
|
+
@import "./_modal-motion.css";
|
|
109
|
+
</style>
|
|
110
|
+
|
|
111
|
+
<style scoped>
|
|
112
|
+
.wp-confirm-overlay {
|
|
113
|
+
position: fixed;
|
|
114
|
+
inset: 0;
|
|
115
|
+
z-index: 10000;
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
padding: 20px;
|
|
120
|
+
background: var(--wp-overlay-bg, rgba(0, 0, 0, 0.62));
|
|
121
|
+
font-family: var(--wp-font-sans, sans-serif);
|
|
122
|
+
}
|
|
123
|
+
.wp-confirm {
|
|
124
|
+
background: var(--wp-bg-2);
|
|
125
|
+
border: 1px solid var(--wp-border);
|
|
126
|
+
border-radius: var(--wp-radius);
|
|
127
|
+
width: 380px;
|
|
128
|
+
max-width: 100%;
|
|
129
|
+
color: var(--wp-text);
|
|
130
|
+
box-shadow: var(--wp-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.5));
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
}
|
|
133
|
+
.wp-confirm__head {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 10px;
|
|
137
|
+
padding: 14px 16px 8px;
|
|
138
|
+
}
|
|
139
|
+
.wp-confirm__head .pi {
|
|
140
|
+
font-size: 16px;
|
|
141
|
+
color: var(--wp-accent-text, var(--wp-text));
|
|
142
|
+
}
|
|
143
|
+
.wp-confirm--danger .wp-confirm__head .pi {
|
|
144
|
+
color: var(--wp-danger, var(--wp-red));
|
|
145
|
+
}
|
|
146
|
+
.wp-confirm__title {
|
|
147
|
+
font: 600 13px var(--wp-font-sans);
|
|
148
|
+
color: var(--wp-text);
|
|
149
|
+
margin: 0;
|
|
150
|
+
}
|
|
151
|
+
.wp-confirm__body {
|
|
152
|
+
font: 12px/1.5 var(--wp-font-sans);
|
|
153
|
+
color: var(--wp-text-muted);
|
|
154
|
+
margin: 0;
|
|
155
|
+
padding: 0 16px 14px;
|
|
156
|
+
}
|
|
157
|
+
.wp-confirm__foot {
|
|
158
|
+
display: flex;
|
|
159
|
+
justify-content: flex-end;
|
|
160
|
+
gap: 8px;
|
|
161
|
+
padding: 10px 14px;
|
|
162
|
+
background: var(--wp-bg-3);
|
|
163
|
+
border-top: 1px solid var(--wp-border);
|
|
164
|
+
}
|
|
165
|
+
</style>
|
package/src/ConfirmModal.vue
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Ask before an action, in the manager's own dialog.
|
|
4
|
-
*
|
|
5
|
-
* A thin wrapper on Modal: the same backdrop, focus trap, Escape and focus
|
|
6
|
-
* return as every other manager dialog, with a Cancel button and a confirm
|
|
7
|
-
* button that names the action. The canvas keeps `ConfirmDialog`, which
|
|
8
|
-
* wears the node colours; this one wears the manager's.
|
|
9
|
-
*
|
|
10
|
-
* Escape, the backdrop, the ✕ and Cancel all emit `cancel`. Enter confirms
|
|
11
|
-
* unless focus is on a button or a text box (which then gets the key itself).
|
|
12
|
-
* The `confirm-*` data-test names match ConfirmDialog's, so a test can drive
|
|
13
|
-
* either.
|
|
14
|
-
*
|
|
15
|
-
* `requireType` makes the user type that exact text before Confirm works
|
|
16
|
-
* (deleting an account); `confirmDisabled` holds Confirm off for a gate the
|
|
17
|
-
* page owns (a required reason in the slot). `warn` asks about something
|
|
18
|
-
* that can be undone but matters: an outlined button and an info mark.
|
|
19
|
-
*/
|
|
20
|
-
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
21
|
-
import Modal from "./Modal.vue";
|
|
22
|
-
import Button from "./Button.vue";
|
|
23
|
-
import Icon from "./Icon.vue";
|
|
24
|
-
import Input from "./Input.vue";
|
|
25
|
-
|
|
26
|
-
const props = withDefaults(
|
|
27
|
-
defineProps<{
|
|
28
|
-
open: boolean;
|
|
29
|
-
title: string;
|
|
30
|
-
body?: string | undefined;
|
|
31
|
-
confirmLabel?: string | undefined;
|
|
32
|
-
cancelLabel?: string | undefined;
|
|
33
|
-
/** `danger` for something that can't be undone: red button, warning
|
|
34
|
-
* mark. `warn` for something that matters but can be undone. */
|
|
35
|
-
tone?: "accent" | "warn" | "danger" | undefined;
|
|
36
|
-
loading?: boolean | undefined;
|
|
37
|
-
/** The text the user must type before Confirm works. */
|
|
38
|
-
requireType?: string | undefined;
|
|
39
|
-
/** Hold Confirm off (a required field in the slot is empty). */
|
|
40
|
-
confirmDisabled?: boolean | undefined;
|
|
41
|
-
}>(),
|
|
42
|
-
{ body: "", confirmLabel: "Confirm", cancelLabel: "Cancel", tone: "accent", loading: false, requireType: "", confirmDisabled: false },
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const emit = defineEmits<{
|
|
46
|
-
confirm: [];
|
|
47
|
-
cancel: [];
|
|
48
|
-
}>();
|
|
49
|
-
|
|
50
|
-
function onOpen(v: boolean): void {
|
|
51
|
-
if (!v) emit("cancel");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const typed = ref("");
|
|
55
|
-
const blocked = computed(
|
|
56
|
-
() => props.loading || props.confirmDisabled || (!!props.requireType && typed.value !== props.requireType),
|
|
57
|
-
);
|
|
58
|
-
const confirmVariant = computed(() => (props.tone === "danger" ? "danger" : props.tone === "warn" ? "outline" : "primary"));
|
|
59
|
-
|
|
60
|
-
function confirm(): void {
|
|
61
|
-
if (!blocked.value) emit("confirm");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function onKeydown(ev: KeyboardEvent): void {
|
|
65
|
-
if (ev.key !== "Enter" || ev.shiftKey || blocked.value) return;
|
|
66
|
-
// A focused button (Cancel, ✕) or a text box in the slot keeps its Enter.
|
|
67
|
-
const t = ev.target;
|
|
68
|
-
if (t instanceof HTMLButtonElement || t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) return;
|
|
69
|
-
ev.preventDefault();
|
|
70
|
-
emit("confirm");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
watch(
|
|
74
|
-
() => props.open,
|
|
75
|
-
(open) => {
|
|
76
|
-
if (open) {
|
|
77
|
-
typed.value = "";
|
|
78
|
-
window.addEventListener("keydown", onKeydown);
|
|
79
|
-
}
|
|
80
|
-
else window.removeEventListener("keydown", onKeydown);
|
|
81
|
-
},
|
|
82
|
-
{ immediate: true },
|
|
83
|
-
);
|
|
84
|
-
onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
|
|
85
|
-
</script>
|
|
86
|
-
|
|
87
|
-
<template>
|
|
88
|
-
<Modal :open="open" size="sm" @update:open="onOpen">
|
|
89
|
-
<template #header="{ titleId }">
|
|
90
|
-
<span class="wp-confirm-modal__head" :class="`wp-confirm-modal__head--${tone}`">
|
|
91
|
-
<Icon :name="tone === 'danger' ? 'exclamation-triangle' : 'info-circle'" />
|
|
92
|
-
<h3 :id="titleId" class="wp-modal__title" data-test="confirm-title">{{ title }}</h3>
|
|
93
|
-
</span>
|
|
94
|
-
</template>
|
|
95
|
-
<div class="wp-confirm-modal__body">
|
|
96
|
-
<p v-if="body" class="wp-confirm-modal__text" data-test="confirm-body">{{ body }}</p>
|
|
97
|
-
<slot />
|
|
98
|
-
<label v-if="requireType" class="wp-confirm-modal__gate">
|
|
99
|
-
<span>Type <code>{{ requireType }}</code> to confirm:</span>
|
|
100
|
-
<Input v-model="typed" autocomplete="off" spellcheck="false" data-test="confirm-type" />
|
|
101
|
-
</label>
|
|
102
|
-
</div>
|
|
103
|
-
<template #footer>
|
|
104
|
-
<Button variant="ghost" :disabled="loading" data-test="confirm-cancel" @click="emit('cancel')">{{ cancelLabel }}</Button>
|
|
105
|
-
<Button
|
|
106
|
-
:variant="confirmVariant"
|
|
107
|
-
:loading="loading"
|
|
108
|
-
:disabled="blocked && !loading"
|
|
109
|
-
data-test="confirm-confirm"
|
|
110
|
-
@click="confirm"
|
|
111
|
-
>{{ confirmLabel }}</Button>
|
|
112
|
-
</template>
|
|
113
|
-
</Modal>
|
|
114
|
-
</template>
|
|
115
|
-
|
|
116
|
-
<style scoped>
|
|
117
|
-
.wp-confirm-modal__head {
|
|
118
|
-
display: inline-flex;
|
|
119
|
-
align-items: center;
|
|
120
|
-
gap: var(--wp-space-3);
|
|
121
|
-
min-width: 0;
|
|
122
|
-
color: var(--wp-accent-text);
|
|
123
|
-
}
|
|
124
|
-
.wp-confirm-modal__head--danger { color: var(--wp-danger); }
|
|
125
|
-
.wp-confirm-modal__head--warn { color: var(--wp-warn); }
|
|
126
|
-
.wp-confirm-modal__head .wp-modal__title { color: var(--wp-text); }
|
|
127
|
-
.wp-confirm-modal__text {
|
|
128
|
-
margin: 0;
|
|
129
|
-
color: var(--wp-text-muted);
|
|
130
|
-
line-height: 1.5;
|
|
131
|
-
}
|
|
132
|
-
.wp-confirm-modal__gate {
|
|
133
|
-
display: flex;
|
|
134
|
-
flex-direction: column;
|
|
135
|
-
gap: var(--wp-space-2);
|
|
136
|
-
margin-top: var(--wp-space-5);
|
|
137
|
-
color: var(--wp-text-muted);
|
|
138
|
-
font-size: var(--wp-text-sm);
|
|
139
|
-
}
|
|
140
|
-
.wp-confirm-modal__gate code {
|
|
141
|
-
padding: 0 4px;
|
|
142
|
-
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
143
|
-
background: var(--wp-bg-3);
|
|
144
|
-
color: var(--wp-text);
|
|
145
|
-
font-family: var(--wp-font-mono);
|
|
146
|
-
}
|
|
147
|
-
</style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Ask before an action, in the manager's own dialog.
|
|
4
|
+
*
|
|
5
|
+
* A thin wrapper on Modal: the same backdrop, focus trap, Escape and focus
|
|
6
|
+
* return as every other manager dialog, with a Cancel button and a confirm
|
|
7
|
+
* button that names the action. The canvas keeps `ConfirmDialog`, which
|
|
8
|
+
* wears the node colours; this one wears the manager's.
|
|
9
|
+
*
|
|
10
|
+
* Escape, the backdrop, the ✕ and Cancel all emit `cancel`. Enter confirms
|
|
11
|
+
* unless focus is on a button or a text box (which then gets the key itself).
|
|
12
|
+
* The `confirm-*` data-test names match ConfirmDialog's, so a test can drive
|
|
13
|
+
* either.
|
|
14
|
+
*
|
|
15
|
+
* `requireType` makes the user type that exact text before Confirm works
|
|
16
|
+
* (deleting an account); `confirmDisabled` holds Confirm off for a gate the
|
|
17
|
+
* page owns (a required reason in the slot). `warn` asks about something
|
|
18
|
+
* that can be undone but matters: an outlined button and an info mark.
|
|
19
|
+
*/
|
|
20
|
+
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
21
|
+
import Modal from "./Modal.vue";
|
|
22
|
+
import Button from "./Button.vue";
|
|
23
|
+
import Icon from "./Icon.vue";
|
|
24
|
+
import Input from "./Input.vue";
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(
|
|
27
|
+
defineProps<{
|
|
28
|
+
open: boolean;
|
|
29
|
+
title: string;
|
|
30
|
+
body?: string | undefined;
|
|
31
|
+
confirmLabel?: string | undefined;
|
|
32
|
+
cancelLabel?: string | undefined;
|
|
33
|
+
/** `danger` for something that can't be undone: red button, warning
|
|
34
|
+
* mark. `warn` for something that matters but can be undone. */
|
|
35
|
+
tone?: "accent" | "warn" | "danger" | undefined;
|
|
36
|
+
loading?: boolean | undefined;
|
|
37
|
+
/** The text the user must type before Confirm works. */
|
|
38
|
+
requireType?: string | undefined;
|
|
39
|
+
/** Hold Confirm off (a required field in the slot is empty). */
|
|
40
|
+
confirmDisabled?: boolean | undefined;
|
|
41
|
+
}>(),
|
|
42
|
+
{ body: "", confirmLabel: "Confirm", cancelLabel: "Cancel", tone: "accent", loading: false, requireType: "", confirmDisabled: false },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{
|
|
46
|
+
confirm: [];
|
|
47
|
+
cancel: [];
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
function onOpen(v: boolean): void {
|
|
51
|
+
if (!v) emit("cancel");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const typed = ref("");
|
|
55
|
+
const blocked = computed(
|
|
56
|
+
() => props.loading || props.confirmDisabled || (!!props.requireType && typed.value !== props.requireType),
|
|
57
|
+
);
|
|
58
|
+
const confirmVariant = computed(() => (props.tone === "danger" ? "danger" : props.tone === "warn" ? "outline" : "primary"));
|
|
59
|
+
|
|
60
|
+
function confirm(): void {
|
|
61
|
+
if (!blocked.value) emit("confirm");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onKeydown(ev: KeyboardEvent): void {
|
|
65
|
+
if (ev.key !== "Enter" || ev.shiftKey || blocked.value) return;
|
|
66
|
+
// A focused button (Cancel, ✕) or a text box in the slot keeps its Enter.
|
|
67
|
+
const t = ev.target;
|
|
68
|
+
if (t instanceof HTMLButtonElement || t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) return;
|
|
69
|
+
ev.preventDefault();
|
|
70
|
+
emit("confirm");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
watch(
|
|
74
|
+
() => props.open,
|
|
75
|
+
(open) => {
|
|
76
|
+
if (open) {
|
|
77
|
+
typed.value = "";
|
|
78
|
+
window.addEventListener("keydown", onKeydown);
|
|
79
|
+
}
|
|
80
|
+
else window.removeEventListener("keydown", onKeydown);
|
|
81
|
+
},
|
|
82
|
+
{ immediate: true },
|
|
83
|
+
);
|
|
84
|
+
onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<Modal :open="open" size="sm" @update:open="onOpen">
|
|
89
|
+
<template #header="{ titleId }">
|
|
90
|
+
<span class="wp-confirm-modal__head" :class="`wp-confirm-modal__head--${tone}`">
|
|
91
|
+
<Icon :name="tone === 'danger' ? 'exclamation-triangle' : 'info-circle'" />
|
|
92
|
+
<h3 :id="titleId" class="wp-modal__title" data-test="confirm-title">{{ title }}</h3>
|
|
93
|
+
</span>
|
|
94
|
+
</template>
|
|
95
|
+
<div class="wp-confirm-modal__body">
|
|
96
|
+
<p v-if="body" class="wp-confirm-modal__text" data-test="confirm-body">{{ body }}</p>
|
|
97
|
+
<slot />
|
|
98
|
+
<label v-if="requireType" class="wp-confirm-modal__gate">
|
|
99
|
+
<span>Type <code>{{ requireType }}</code> to confirm:</span>
|
|
100
|
+
<Input v-model="typed" autocomplete="off" spellcheck="false" data-test="confirm-type" />
|
|
101
|
+
</label>
|
|
102
|
+
</div>
|
|
103
|
+
<template #footer>
|
|
104
|
+
<Button variant="ghost" :disabled="loading" data-test="confirm-cancel" @click="emit('cancel')">{{ cancelLabel }}</Button>
|
|
105
|
+
<Button
|
|
106
|
+
:variant="confirmVariant"
|
|
107
|
+
:loading="loading"
|
|
108
|
+
:disabled="blocked && !loading"
|
|
109
|
+
data-test="confirm-confirm"
|
|
110
|
+
@click="confirm"
|
|
111
|
+
>{{ confirmLabel }}</Button>
|
|
112
|
+
</template>
|
|
113
|
+
</Modal>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<style scoped>
|
|
117
|
+
.wp-confirm-modal__head {
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: var(--wp-space-3);
|
|
121
|
+
min-width: 0;
|
|
122
|
+
color: var(--wp-accent-text);
|
|
123
|
+
}
|
|
124
|
+
.wp-confirm-modal__head--danger { color: var(--wp-danger); }
|
|
125
|
+
.wp-confirm-modal__head--warn { color: var(--wp-warn); }
|
|
126
|
+
.wp-confirm-modal__head .wp-modal__title { color: var(--wp-text); }
|
|
127
|
+
.wp-confirm-modal__text {
|
|
128
|
+
margin: 0;
|
|
129
|
+
color: var(--wp-text-muted);
|
|
130
|
+
line-height: 1.5;
|
|
131
|
+
}
|
|
132
|
+
.wp-confirm-modal__gate {
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
gap: var(--wp-space-2);
|
|
136
|
+
margin-top: var(--wp-space-5);
|
|
137
|
+
color: var(--wp-text-muted);
|
|
138
|
+
font-size: var(--wp-text-sm);
|
|
139
|
+
}
|
|
140
|
+
.wp-confirm-modal__gate code {
|
|
141
|
+
padding: 0 4px;
|
|
142
|
+
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
143
|
+
background: var(--wp-bg-3);
|
|
144
|
+
color: var(--wp-text);
|
|
145
|
+
font-family: var(--wp-font-mono);
|
|
146
|
+
}
|
|
147
|
+
</style>
|