@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +92 -51
  2. package/LICENSE +674 -674
  3. package/README.md +73 -76
  4. package/package.json +78 -77
  5. package/src/Avatar.vue +83 -83
  6. package/src/Badge.vue +97 -97
  7. package/src/Banner.vue +118 -118
  8. package/src/BookmarkButton.vue +27 -27
  9. package/src/BundleMeta.vue +51 -51
  10. package/src/Button.vue +103 -103
  11. package/src/Card.vue +68 -68
  12. package/src/Checkbox.vue +109 -107
  13. package/src/Chip.vue +163 -97
  14. package/src/ChoiceCards.vue +133 -133
  15. package/src/CloseButton.vue +69 -69
  16. package/src/CodeBlock.vue +146 -119
  17. package/src/ColorField.vue +188 -206
  18. package/src/ColorPicker.vue +201 -201
  19. package/src/Combobox.vue +331 -334
  20. package/src/ConfirmDialog.vue +165 -163
  21. package/src/ConfirmModal.vue +147 -147
  22. package/src/ConstraintMatrixPreview.vue +708 -708
  23. package/src/ContextMenu.vue +386 -379
  24. package/src/CopyButton.vue +156 -156
  25. package/src/DataTable.vue +293 -292
  26. package/src/DatePicker.vue +234 -234
  27. package/src/DescriptionList.vue +73 -73
  28. package/src/DiffView.vue +68 -68
  29. package/src/Disclosure.vue +166 -166
  30. package/src/DragHandle.vue +49 -49
  31. package/src/Drawer.vue +131 -131
  32. package/src/DropZone.vue +196 -196
  33. package/src/DropdownMenu.vue +72 -72
  34. package/src/EmptyState.vue +111 -111
  35. package/src/Field.vue +28 -28
  36. package/src/HsvPicker.vue +208 -208
  37. package/src/Icon.vue +39 -39
  38. package/src/IconPicker.vue +209 -209
  39. package/src/ImageGrid.vue +127 -125
  40. package/src/InlineEdit.vue +183 -183
  41. package/src/Input.vue +99 -99
  42. package/src/Kbd.vue +56 -56
  43. package/src/Lightbox.vue +176 -173
  44. package/src/ListFilter.vue +85 -85
  45. package/src/ListRow.vue +204 -204
  46. package/src/LoadingMark.vue +217 -161
  47. package/src/Logo.vue +60 -60
  48. package/src/Modal.vue +78 -76
  49. package/src/ModalShell.vue +109 -107
  50. package/src/Pagination.vue +106 -106
  51. package/src/Popover.vue +180 -171
  52. package/src/PostCard.vue +210 -211
  53. package/src/ProgressBar.vue +90 -90
  54. package/src/RadioGroup.vue +177 -177
  55. package/src/RangeSlider.vue +183 -183
  56. package/src/RelativeDate.vue +35 -35
  57. package/src/ResizeHandle.vue +143 -143
  58. package/src/SearchInput.vue +192 -199
  59. package/src/Select.vue +644 -641
  60. package/src/Separator.vue +54 -54
  61. package/src/Skeleton.vue +98 -82
  62. package/src/Slider.vue +198 -198
  63. package/src/SortableList.vue +260 -260
  64. package/src/SplitPane.vue +68 -68
  65. package/src/StarButton.vue +40 -40
  66. package/src/Stepper.vue +141 -141
  67. package/src/SupportLinks.vue +93 -93
  68. package/src/TagInput.vue +297 -300
  69. package/src/Textarea.vue +57 -57
  70. package/src/Timeline.vue +92 -92
  71. package/src/ToastCard.vue +119 -119
  72. package/src/ToastHost.vue +40 -40
  73. package/src/Toggle.vue +36 -36
  74. package/src/TokenizedValue.vue +310 -310
  75. package/src/Toolbar.vue +110 -110
  76. package/src/VirtualList.vue +87 -87
  77. package/src/VisuallyHidden.vue +29 -29
  78. package/src/WpButton.vue +75 -75
  79. package/src/WpCheck.vue +104 -102
  80. package/src/WpIconButton.vue +107 -107
  81. package/src/WpImageZoom.vue +544 -544
  82. package/src/WpNumber.vue +178 -183
  83. package/src/WpSegmented.vue +175 -175
  84. package/src/WpSelect.vue +93 -89
  85. package/src/WpSwitch.vue +163 -163
  86. package/src/WpTabs.vue +188 -188
  87. package/src/_modal-motion.css +33 -33
  88. package/src/_skeleton.css +50 -50
  89. package/src/axis-color.ts +42 -42
  90. package/src/button.css +169 -169
  91. package/src/calendar.ts +59 -59
  92. package/src/clipboard.ts +50 -50
  93. package/src/colour-presets.ts +12 -12
  94. package/src/diff.ts +51 -51
  95. package/src/drop-zone.ts +41 -41
  96. package/src/field.css +50 -0
  97. package/src/flip.ts +237 -237
  98. package/src/floating-layer.ts +48 -0
  99. package/src/floating.ts +99 -99
  100. package/src/highlight.ts +199 -0
  101. package/src/hsv.ts +100 -100
  102. package/src/icon-catalog.ts +52 -52
  103. package/src/image-zoom.ts +93 -93
  104. package/src/kind-colour.ts +28 -0
  105. package/src/loading-mark-motion.ts +249 -0
  106. package/src/logo-paths.ts +21 -21
  107. package/src/matrix-axis.ts +91 -91
  108. package/src/pagination.ts +27 -27
  109. package/src/platform-keys.ts +29 -29
  110. package/src/rich-tokenize.ts +357 -357
  111. package/src/select-types.ts +32 -32
  112. package/src/slider-math.ts +45 -45
  113. package/src/sortable.ts +22 -22
  114. package/src/theme/fonts/OFL-Fraunces.txt +93 -93
  115. package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
  116. package/src/theme/fonts/OFL-Manrope.txt +93 -93
  117. package/src/theme/looks.css +428 -428
  118. package/src/theme/palettes.css +254 -254
  119. package/src/theme/tokens.css +2896 -2894
  120. package/src/tokens.ts +56 -56
  121. package/src/tooltip.css +43 -43
  122. package/src/tooltip.ts +146 -146
  123. package/src/useDialogFocus.ts +73 -73
  124. package/src/useGrowableField.ts +357 -357
  125. package/src/useModalKeyShield.ts +116 -116
  126. package/src/useToast.ts +48 -48
  127. package/src/wp-logo.svg +14 -14
  128. package/src/ChipToggle.vue +0 -92
  129. package/src/KindChip.vue +0 -53
  130. package/src/RatingBadge.vue +0 -42
@@ -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
- <div
69
- v-if="visible"
70
- class="wp-confirm-overlay"
71
- data-test="confirm-overlay"
72
- @click="onCancel"
73
- >
74
- <div
75
- class="wp-confirm"
76
- role="alertdialog"
77
- aria-modal="true"
78
- :class="`wp-confirm--${variant}`"
79
- @click.stop
80
- >
81
- <div class="wp-confirm__head">
82
- <i
83
- class="pi"
84
- :class="variant === 'danger' ? 'pi-exclamation-triangle' : 'pi-info-circle'"
85
- aria-hidden="true"
86
- />
87
- <h3 class="wp-confirm__title" data-test="confirm-title">{{ title }}</h3>
88
- </div>
89
- <p v-if="body" class="wp-confirm__body" data-test="confirm-body">{{ body }}</p>
90
- <div class="wp-confirm__foot">
91
- <WpButton data-test="confirm-cancel" @click="onCancel">{{ cancelLabel }}</WpButton>
92
- <WpButton :variant="variant === 'danger' ? 'danger' : 'primary'" data-test="confirm-confirm" @click="onConfirm">{{ confirmLabel }}</WpButton>
93
- </div>
94
- </div>
95
- </div>
96
- </Transition>
97
- </Teleport>
98
- </template>
99
-
100
- <style>
101
- /* Shared modal open/close transition — overlay fade + panel scale.
102
- * See `_modal-motion.css` for rules. Imported unscoped so the
103
- * `wp-modal-*` transition class names match the Vue-emitted classes
104
- * cleanly (named transitions don't survive Vue's `data-v-...`
105
- * scoping). */
106
- @import "./_modal-motion.css";
107
- </style>
108
-
109
- <style scoped>
110
- .wp-confirm-overlay {
111
- position: fixed;
112
- inset: 0;
113
- z-index: 10000;
114
- display: flex;
115
- align-items: center;
116
- justify-content: center;
117
- padding: 20px;
118
- background: var(--wp-overlay-bg, rgba(0, 0, 0, 0.62));
119
- font-family: var(--wp-font-sans, sans-serif);
120
- }
121
- .wp-confirm {
122
- background: var(--wp-bg-2);
123
- border: 1px solid var(--wp-border);
124
- border-radius: var(--wp-radius);
125
- width: 380px;
126
- max-width: 100%;
127
- color: var(--wp-text);
128
- box-shadow: var(--wp-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.5));
129
- overflow: hidden;
130
- }
131
- .wp-confirm__head {
132
- display: flex;
133
- align-items: center;
134
- gap: 10px;
135
- padding: 14px 16px 8px;
136
- }
137
- .wp-confirm__head .pi {
138
- font-size: 16px;
139
- color: var(--wp-accent-text, var(--wp-text));
140
- }
141
- .wp-confirm--danger .wp-confirm__head .pi {
142
- color: var(--wp-danger, var(--wp-red));
143
- }
144
- .wp-confirm__title {
145
- font: 600 13px var(--wp-font-sans);
146
- color: var(--wp-text);
147
- margin: 0;
148
- }
149
- .wp-confirm__body {
150
- font: 12px/1.5 var(--wp-font-sans);
151
- color: var(--wp-text-muted);
152
- margin: 0;
153
- padding: 0 16px 14px;
154
- }
155
- .wp-confirm__foot {
156
- display: flex;
157
- justify-content: flex-end;
158
- gap: 8px;
159
- padding: 10px 14px;
160
- background: var(--wp-bg-3);
161
- border-top: 1px solid var(--wp-border);
162
- }
163
- </style>
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>
@@ -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>