srcdev-nuxt-components 9.1.55 → 9.1.56
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/.claude/settings.json +29 -2
- package/.claude/skills/components/alert-content-inner.md +60 -0
- package/.claude/skills/components/display-dialog.md +80 -0
- package/.claude/skills/components/display-prompt.md +163 -0
- package/.claude/skills/components/display-toast.md +235 -0
- package/.claude/skills/index.md +7 -3
- package/.claude/skills/theming-colour-ramps.md +328 -0
- package/.claude/skills/theming-override-default.md +115 -186
- package/.claude/skills/theming-partial-override.md +88 -75
- package/README.md +166 -0
- package/app/app.config.ts +44 -0
- package/app/assets/styles/setup/01.config/_head.css +3 -3
- package/app/assets/styles/setup/02.colours/_amber.css +12 -10
- package/app/assets/styles/setup/02.colours/_blue.css +10 -9
- package/app/assets/styles/setup/02.colours/_green.css +12 -11
- package/app/assets/styles/setup/02.colours/_orange.css +12 -10
- package/app/assets/styles/setup/02.colours/_red.css +10 -9
- package/app/assets/styles/setup/02.colours/_slate.css +12 -11
- package/app/assets/styles/setup/02.colours/_sunset.css +12 -10
- package/app/assets/styles/setup/02.colours/_theme-params.css +18 -0
- package/app/assets/styles/setup/02.colours/index.css +1 -0
- package/app/assets/styles/setup/03.theming/_default.css +87 -0
- package/app/assets/styles/setup/03.theming/_error.css +14 -0
- package/app/assets/styles/setup/03.theming/_success.css +4 -0
- package/app/assets/styles/setup/03.theming/_theme-slots.css +16 -0
- package/app/assets/styles/setup/03.theming/_warning.css +13 -0
- package/app/assets/styles/setup/03.theming/index.css +6 -4
- package/app/assets/styles/setup/03.theming/theme-ramp.css +19 -0
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +0 -7
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/assets/styles/setup/index.css +1 -1
- package/app/components/01.atoms/display-dialog/DisplayDialog.vue +74 -24
- package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +148 -14
- package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +3 -3
- package/app/components/01.atoms/prompt/DisplayPrompt.vue +113 -0
- package/app/components/01.atoms/prompt/stories/DisplayPrompt.stories.ts +112 -0
- package/app/components/01.atoms/prompt/tests/DisplayPrompt.spec.ts +205 -0
- package/app/components/01.atoms/toast/DisplayToast.vue +327 -0
- package/app/components/01.atoms/toast/DisplayToastProvider.vue +362 -0
- package/app/components/01.atoms/toast/stories/DisplayToast.stories.ts +195 -0
- package/app/components/01.atoms/toast/stories/DisplayToastProvider.stories.ts +142 -0
- package/app/components/01.atoms/toast/tests/DisplayToast.spec.ts +266 -0
- package/app/components/01.atoms/toast/tests/DisplayToastProvider.spec.ts +256 -0
- package/app/components/02.molecules/action-menu/ActionMenu.vue +8 -1
- package/app/components/02.molecules/action-menu/ActionMenuItemCore.vue +11 -3
- package/app/components/02.molecules/alert-content/AlertContent.vue +67 -0
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +153 -0
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +131 -0
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +135 -0
- package/app/components/02.molecules/alert-content/tests/AlertContentInner.spec.ts +135 -0
- package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +78 -0
- package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +132 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +149 -0
- package/app/components/05.forms/input-button/InputButtonCore.vue +35 -39
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +8 -8
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioCore.vue +5 -5
- package/app/components/05.forms/input-copy/InputCopyCore.vue +7 -7
- package/app/components/05.forms/input-number/InputNumberCore.vue +5 -5
- package/app/components/05.forms/input-select/InputSelectCore.vue +6 -6
- package/app/components/05.forms/input-text/InputTextCore.vue +6 -6
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +2 -2
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +5 -5
- package/app/components/05.forms/pending-effect/PendingEffect.vue +60 -60
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +10 -10
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +4 -4
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +4 -4
- package/app/components/display-tooltip/DisplayTooltip.vue +4 -4
- package/app/composables/useBodyLock.ts +21 -0
- package/app/composables/useDialogControls.ts +11 -11
- package/app/composables/useToastQueue.ts +39 -0
- package/app/layouts/default.vue +1 -1
- package/app/pages/ui/display-dialog.vue +275 -127
- package/app/pages/ui/display-prompt.vue +255 -52
- package/app/pages/ui/display-toast.vue +278 -299
- package/app/pages/ui/mask-element.vue +25 -3
- package/app/pages/ui/settings.vue +9 -35
- package/app/types/app-config.d.ts +41 -0
- package/app/types/components/display-prompt.d.ts +11 -0
- package/app/types/components/display-toast.d.ts +11 -9
- package/app/types/components/index.ts +2 -0
- package/app/types/components/semantic-theme.d.ts +1 -0
- package/nuxt.config.ts +16 -0
- package/package.json +6 -4
- package/app/assets/styles/setup/03.theming/default/_dark.css +0 -156
- package/app/assets/styles/setup/03.theming/default/_light.css +0 -166
- package/app/assets/styles/setup/03.theming/default/index.css +0 -2
- package/app/assets/styles/setup/03.theming/error/_dark.css +0 -56
- package/app/assets/styles/setup/03.theming/error/_light.css +0 -74
- package/app/assets/styles/setup/03.theming/error/index.css +0 -2
- package/app/assets/styles/setup/03.theming/success/_dark.css +0 -50
- package/app/assets/styles/setup/03.theming/success/_light.css +0 -65
- package/app/assets/styles/setup/03.theming/success/index.css +0 -2
- package/app/assets/styles/setup/03.theming/warning/_dark.css +0 -50
- package/app/assets/styles/setup/03.theming/warning/_light.css +0 -64
- package/app/assets/styles/setup/03.theming/warning/index.css +0 -2
- package/app/components/display-prompt/DisplayPromptCore.vue +0 -228
- package/app/components/display-prompt/variants/DisplayPromptError.vue +0 -53
- package/app/components/display-toast/DisplayToast.vue +0 -447
- package/app/components/display-toast/molecules/DefaultToastContent.vue +0 -163
- package/app/components/display-toast/stories/DisplayToast.stories.ts +0 -380
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
<dialog
|
|
3
3
|
class="display-dialog"
|
|
4
4
|
:class="[elementClasses]"
|
|
5
|
-
:role="isAlert ? 'alertdialog' :
|
|
6
|
-
:
|
|
7
|
-
:
|
|
5
|
+
:role="isAlert ? 'alertdialog' : undefined"
|
|
6
|
+
:aria-modal="true"
|
|
7
|
+
:aria-labelledby="hasTitle ? dialogTitleId : undefined"
|
|
8
|
+
:align-dialog="resolved.alignDialog"
|
|
9
|
+
:justify-dialog="resolved.justifyDialog"
|
|
8
10
|
:open
|
|
9
11
|
:data-dialog-id="dataDialogId"
|
|
10
12
|
>
|
|
@@ -14,19 +16,19 @@
|
|
|
14
16
|
:click-outside-deactivates="!isAlert"
|
|
15
17
|
@deactivate="closeDialog()"
|
|
16
18
|
>
|
|
17
|
-
<div class="inner" :class="[variant]">
|
|
18
|
-
<div class="header">
|
|
19
|
-
<div v-if="hasTitle" class="col-left">
|
|
19
|
+
<div class="inner" :class="[resolved.variant]">
|
|
20
|
+
<div class="header" :data-theme="resolved.theme">
|
|
21
|
+
<div v-if="hasTitle" :id="dialogTitleId" class="col-left">
|
|
20
22
|
<slot name="dialogTitle"></slot>
|
|
21
23
|
</div>
|
|
22
24
|
|
|
23
25
|
<div class="col-right">
|
|
24
26
|
<button
|
|
25
27
|
data-test-id="display-dialog-header-close"
|
|
26
|
-
class="display-
|
|
28
|
+
class="display-dialog-close"
|
|
27
29
|
@click.prevent="closeDialog()"
|
|
28
30
|
>
|
|
29
|
-
<Icon name="
|
|
31
|
+
<Icon :name="resolved.closeIcon" class="icon" />
|
|
30
32
|
<span class="sr-only">Close</span>
|
|
31
33
|
</button>
|
|
32
34
|
</div>
|
|
@@ -34,7 +36,8 @@
|
|
|
34
36
|
<div
|
|
35
37
|
v-if="hasContent"
|
|
36
38
|
class="dialog-content"
|
|
37
|
-
:class="[{ 'allow-content-scroll': allowContentScroll }]"
|
|
39
|
+
:class="[{ 'allow-content-scroll': resolved.allowContentScroll }]"
|
|
40
|
+
:tabindex="resolved.allowContentScroll ? 0 : undefined"
|
|
38
41
|
>
|
|
39
42
|
<slot name="dialogContent"></slot>
|
|
40
43
|
</div>
|
|
@@ -49,6 +52,8 @@
|
|
|
49
52
|
|
|
50
53
|
<script setup lang="ts">
|
|
51
54
|
import { FocusTrap } from "focus-trap-vue";
|
|
55
|
+
import type { SemanticTheme } from "~/types/components";
|
|
56
|
+
|
|
52
57
|
interface Props {
|
|
53
58
|
styleClassPassthrough?: string | string[];
|
|
54
59
|
variant?: "dialog" | "modal" | "confirm" | "alert" | "fullscreen";
|
|
@@ -57,28 +62,45 @@ interface Props {
|
|
|
57
62
|
lockViewport?: boolean;
|
|
58
63
|
allowContentScroll?: boolean;
|
|
59
64
|
dataDialogId: string;
|
|
65
|
+
theme?: SemanticTheme;
|
|
66
|
+
closeIcon?: string;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
const props = withDefaults(defineProps<Props>(), {
|
|
63
70
|
styleClassPassthrough: () => [],
|
|
64
|
-
variant:
|
|
65
|
-
justifyDialog:
|
|
66
|
-
alignDialog:
|
|
67
|
-
lockViewport:
|
|
68
|
-
allowContentScroll:
|
|
71
|
+
variant: undefined,
|
|
72
|
+
justifyDialog: undefined,
|
|
73
|
+
alignDialog: undefined,
|
|
74
|
+
lockViewport: undefined,
|
|
75
|
+
allowContentScroll: undefined,
|
|
76
|
+
theme: undefined,
|
|
77
|
+
closeIcon: undefined,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const appConfig = useAppConfig();
|
|
81
|
+
|
|
82
|
+
const resolved = computed(() => {
|
|
83
|
+
const config = appConfig.srcdev?.displayDialog;
|
|
84
|
+
return {
|
|
85
|
+
variant: props.variant ?? config?.variant ?? "dialog",
|
|
86
|
+
justifyDialog: props.justifyDialog ?? config?.justifyDialog ?? "center",
|
|
87
|
+
alignDialog: props.alignDialog ?? config?.alignDialog ?? "center",
|
|
88
|
+
lockViewport: props.lockViewport ?? config?.lockViewport ?? true,
|
|
89
|
+
allowContentScroll: props.allowContentScroll ?? config?.allowContentScroll ?? false,
|
|
90
|
+
theme: props.theme ?? config?.theme,
|
|
91
|
+
closeIcon: props.closeIcon ?? config?.closeIcon ?? "bitcoin-icons:cross-filled",
|
|
92
|
+
} as const;
|
|
69
93
|
});
|
|
70
94
|
|
|
71
95
|
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
72
96
|
|
|
73
|
-
const isAlert = computed(() =>
|
|
97
|
+
const isAlert = computed(() => resolved.value.variant === "alert");
|
|
98
|
+
const dialogTitleId = useId();
|
|
74
99
|
|
|
75
100
|
const open = defineModel<boolean>();
|
|
76
101
|
|
|
77
102
|
const closeDialog = () => {
|
|
78
103
|
open.value = false;
|
|
79
|
-
if (props.lockViewport) {
|
|
80
|
-
document.body.classList.remove("lock");
|
|
81
|
-
}
|
|
82
104
|
};
|
|
83
105
|
|
|
84
106
|
const slots = useSlots();
|
|
@@ -86,9 +108,20 @@ const hasTitle = computed(() => !!slots.dialogTitle);
|
|
|
86
108
|
const hasContent = computed(() => !!slots.dialogContent);
|
|
87
109
|
const hasFooter = computed(() => !!(slots.actionButtonLeft || slots.actionButtonRight));
|
|
88
110
|
|
|
111
|
+
const { lock, unlock } = useBodyLock();
|
|
112
|
+
let hasLocked = false;
|
|
113
|
+
|
|
89
114
|
onMounted(() => {
|
|
90
|
-
if (
|
|
91
|
-
|
|
115
|
+
if (resolved.value.lockViewport) {
|
|
116
|
+
lock();
|
|
117
|
+
hasLocked = true;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
onUnmounted(() => {
|
|
122
|
+
if (hasLocked) {
|
|
123
|
+
unlock();
|
|
124
|
+
hasLocked = false;
|
|
92
125
|
}
|
|
93
126
|
});
|
|
94
127
|
</script>
|
|
@@ -112,9 +145,18 @@ onMounted(() => {
|
|
|
112
145
|
--_header-button-border: var(--display-dialog-header-button-border, 0.1rem solid transparent);
|
|
113
146
|
--_header-button-border-radius: var(--display-dialog-header-button-border-radius, 0.4rem);
|
|
114
147
|
--_header-button-outline: var(--display-dialog-header-button-outline, 0.1rem solid transparent);
|
|
115
|
-
--_header-button-border-hover: var(
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
--_header-button-border-hover: var(
|
|
149
|
+
--display-dialog-header-button-border-hover,
|
|
150
|
+
0.1rem solid light-dark(var(--slate-08), var(--slate-04))
|
|
151
|
+
);
|
|
152
|
+
--_header-button-outline-hover: var(
|
|
153
|
+
--display-dialog-header-button-outline-hover,
|
|
154
|
+
0.1rem solid light-dark(var(--slate-08), var(--slate-04))
|
|
155
|
+
);
|
|
156
|
+
--_header-button-icon-color: var(
|
|
157
|
+
--display-dialog-header-button-icon-color,
|
|
158
|
+
light-dark(var(--slate-09), var(--slate-02))
|
|
159
|
+
);
|
|
118
160
|
--_header-button-icon-size: var(--display-dialog-header-button-icon-size, 2.4rem);
|
|
119
161
|
|
|
120
162
|
--_content-padding: var(--display-dialog-content-padding, 1.2rem);
|
|
@@ -215,6 +257,14 @@ onMounted(() => {
|
|
|
215
257
|
align-items: center;
|
|
216
258
|
padding: var(--_header-padding);
|
|
217
259
|
|
|
260
|
+
&[data-theme] {
|
|
261
|
+
border-block-end: 0.2rem solid var(--theme-accent);
|
|
262
|
+
|
|
263
|
+
.display-dialog-close .icon {
|
|
264
|
+
color: var(--theme-accent);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
218
268
|
.col-left {
|
|
219
269
|
flex: 1;
|
|
220
270
|
}
|
|
@@ -222,7 +272,7 @@ onMounted(() => {
|
|
|
222
272
|
.col-right {
|
|
223
273
|
margin-inline-start: auto;
|
|
224
274
|
|
|
225
|
-
.display-
|
|
275
|
+
.display-dialog-close {
|
|
226
276
|
background-color: transparent;
|
|
227
277
|
display: block flex;
|
|
228
278
|
align-items: center;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
4
3
|
import DisplayDialog from "../DisplayDialog.vue";
|
|
4
|
+
import type { SemanticTheme } from "~/types/components";
|
|
5
|
+
|
|
6
|
+
const { useAppConfigMock } = vi.hoisted(() => ({
|
|
7
|
+
// icon: {} is required — @nuxt/icon reads useAppConfig().icon.collections internally.
|
|
8
|
+
useAppConfigMock: vi.fn(() => ({ srcdev: undefined as Record<string, unknown> | undefined, icon: {} as object })),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
mockNuxtImport("useAppConfig", () => useAppConfigMock);
|
|
5
12
|
|
|
6
13
|
vi.mock("focus-trap-vue", () => ({
|
|
7
14
|
FocusTrap: {
|
|
@@ -13,6 +20,11 @@ vi.mock("focus-trap-vue", () => ({
|
|
|
13
20
|
}));
|
|
14
21
|
|
|
15
22
|
describe("DisplayDialog", () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
document.body.classList.remove("lock");
|
|
25
|
+
delete document.body.dataset.lockCount;
|
|
26
|
+
});
|
|
27
|
+
|
|
16
28
|
// ─── Mount ────────────────────────────────────────────────────────────────
|
|
17
29
|
|
|
18
30
|
it("mounts without error", async () => {
|
|
@@ -80,20 +92,20 @@ describe("DisplayDialog", () => {
|
|
|
80
92
|
|
|
81
93
|
// ─── role ─────────────────────────────────────────────────────────────────
|
|
82
94
|
|
|
83
|
-
it("has role
|
|
95
|
+
it("has no explicit role by default — implicit from <dialog> element", async () => {
|
|
84
96
|
const wrapper = await mountSuspended(DisplayDialog, {
|
|
85
97
|
props: { dataDialogId: "test" },
|
|
86
98
|
});
|
|
87
|
-
expect(wrapper.attributes("role")).
|
|
99
|
+
expect(wrapper.attributes("role")).toBeUndefined();
|
|
88
100
|
});
|
|
89
101
|
|
|
90
102
|
it.each(["dialog", "modal", "confirm", "fullscreen"] as const)(
|
|
91
|
-
"has role
|
|
103
|
+
"has no explicit role for variant='%s' — implicit from <dialog> element",
|
|
92
104
|
async (variant) => {
|
|
93
105
|
const wrapper = await mountSuspended(DisplayDialog, {
|
|
94
106
|
props: { dataDialogId: "test", variant },
|
|
95
107
|
});
|
|
96
|
-
expect(wrapper.attributes("role")).
|
|
108
|
+
expect(wrapper.attributes("role")).toBeUndefined();
|
|
97
109
|
}
|
|
98
110
|
);
|
|
99
111
|
|
|
@@ -238,11 +250,11 @@ describe("DisplayDialog", () => {
|
|
|
238
250
|
// ─── Body viewport lock ───────────────────────────────────────────────────
|
|
239
251
|
|
|
240
252
|
it("adds lock class to body on mount when lockViewport=true", async () => {
|
|
241
|
-
await mountSuspended(DisplayDialog, {
|
|
253
|
+
const wrapper = await mountSuspended(DisplayDialog, {
|
|
242
254
|
props: { dataDialogId: "test", lockViewport: true },
|
|
243
255
|
});
|
|
244
256
|
expect(document.body.classList.contains("lock")).toBe(true);
|
|
245
|
-
|
|
257
|
+
wrapper.unmount();
|
|
246
258
|
});
|
|
247
259
|
|
|
248
260
|
it("does not add lock class to body when lockViewport=false", async () => {
|
|
@@ -252,13 +264,135 @@ describe("DisplayDialog", () => {
|
|
|
252
264
|
expect(document.body.classList.contains("lock")).toBe(false);
|
|
253
265
|
});
|
|
254
266
|
|
|
255
|
-
it("removes lock class from body
|
|
256
|
-
document.body.classList.add("lock");
|
|
267
|
+
it("removes lock class from body on unmount when lockViewport=true", async () => {
|
|
257
268
|
const wrapper = await mountSuspended(DisplayDialog, {
|
|
258
|
-
props: { dataDialogId: "test",
|
|
269
|
+
props: { dataDialogId: "test", lockViewport: true },
|
|
259
270
|
});
|
|
260
|
-
|
|
261
|
-
|
|
271
|
+
expect(document.body.classList.contains("lock")).toBe(true);
|
|
272
|
+
wrapper.unmount();
|
|
262
273
|
expect(document.body.classList.contains("lock")).toBe(false);
|
|
263
274
|
});
|
|
275
|
+
|
|
276
|
+
it("does not remove lock when a second locking dialog is still open", async () => {
|
|
277
|
+
const wrapper1 = await mountSuspended(DisplayDialog, {
|
|
278
|
+
props: { dataDialogId: "a", lockViewport: true },
|
|
279
|
+
});
|
|
280
|
+
const wrapper2 = await mountSuspended(DisplayDialog, {
|
|
281
|
+
props: { dataDialogId: "b", lockViewport: true },
|
|
282
|
+
});
|
|
283
|
+
expect(document.body.classList.contains("lock")).toBe(true);
|
|
284
|
+
wrapper2.unmount();
|
|
285
|
+
expect(document.body.classList.contains("lock")).toBe(true);
|
|
286
|
+
wrapper1.unmount();
|
|
287
|
+
expect(document.body.classList.contains("lock")).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ─── app.config resolution ────────────────────────────────────────────────
|
|
291
|
+
// Icon renders as <span class="iconify undefined{name} icon"> in jsdom.
|
|
292
|
+
// All assertions are DOM-based — setupState is private API in <script setup>.
|
|
293
|
+
|
|
294
|
+
describe("app.config resolution", () => {
|
|
295
|
+
beforeEach(() => {
|
|
296
|
+
useAppConfigMock.mockReturnValue({ srcdev: undefined, icon: {} });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
afterEach(() => {
|
|
300
|
+
document.body.classList.remove("lock");
|
|
301
|
+
delete document.body.dataset.lockCount;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const iconName = (wrapper: Awaited<ReturnType<typeof mountSuspended>>) =>
|
|
305
|
+
wrapper
|
|
306
|
+
.find('[data-test-id="display-dialog-header-close"] .iconify')
|
|
307
|
+
.classes()
|
|
308
|
+
.find((c: string) => c !== "iconify" && c !== "icon") ?? "";
|
|
309
|
+
|
|
310
|
+
it("uses hardcoded fallbacks when app.config has no displayDialog key", async () => {
|
|
311
|
+
const wrapper = await mountSuspended(DisplayDialog, {
|
|
312
|
+
props: { dataDialogId: "test" },
|
|
313
|
+
slots: { dialogContent: "<p>Content</p>" },
|
|
314
|
+
});
|
|
315
|
+
expect(wrapper.find(".inner").classes()).toContain("dialog");
|
|
316
|
+
expect(wrapper.attributes("align-dialog")).toBe("center");
|
|
317
|
+
expect(wrapper.attributes("justify-dialog")).toBe("center");
|
|
318
|
+
expect(document.body.classList.contains("lock")).toBe(true);
|
|
319
|
+
expect(wrapper.find(".dialog-content").classes()).not.toContain("allow-content-scroll");
|
|
320
|
+
expect(wrapper.find(".header").attributes("data-theme")).toBeUndefined();
|
|
321
|
+
expect(iconName(wrapper)).toContain("bitcoin-icons:cross-filled");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("uses app.config values when no props are supplied", async () => {
|
|
325
|
+
useAppConfigMock.mockReturnValue({
|
|
326
|
+
icon: {},
|
|
327
|
+
srcdev: {
|
|
328
|
+
displayDialog: {
|
|
329
|
+
variant: "modal",
|
|
330
|
+
justifyDialog: "start",
|
|
331
|
+
alignDialog: "end",
|
|
332
|
+
lockViewport: false,
|
|
333
|
+
allowContentScroll: true,
|
|
334
|
+
theme: "info" as SemanticTheme,
|
|
335
|
+
closeIcon: "heroicons:x-mark",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
const wrapper = await mountSuspended(DisplayDialog, {
|
|
340
|
+
props: { dataDialogId: "test" },
|
|
341
|
+
slots: { dialogContent: "<p>Content</p>" },
|
|
342
|
+
});
|
|
343
|
+
expect(wrapper.find(".inner").classes()).toContain("modal");
|
|
344
|
+
expect(wrapper.attributes("align-dialog")).toBe("end");
|
|
345
|
+
expect(wrapper.attributes("justify-dialog")).toBe("start");
|
|
346
|
+
expect(document.body.classList.contains("lock")).toBe(false);
|
|
347
|
+
expect(wrapper.find(".dialog-content").classes()).toContain("allow-content-scroll");
|
|
348
|
+
expect(wrapper.find(".header").attributes("data-theme")).toBe("info");
|
|
349
|
+
expect(iconName(wrapper)).toContain("heroicons:x-mark");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("explicit props take precedence over app.config", async () => {
|
|
353
|
+
useAppConfigMock.mockReturnValue({
|
|
354
|
+
icon: {},
|
|
355
|
+
srcdev: {
|
|
356
|
+
displayDialog: {
|
|
357
|
+
variant: "modal",
|
|
358
|
+
justifyDialog: "start",
|
|
359
|
+
alignDialog: "end",
|
|
360
|
+
lockViewport: false,
|
|
361
|
+
allowContentScroll: true,
|
|
362
|
+
theme: "info" as SemanticTheme,
|
|
363
|
+
closeIcon: "heroicons:x-mark",
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const wrapper = await mountSuspended(DisplayDialog, {
|
|
368
|
+
props: {
|
|
369
|
+
dataDialogId: "test",
|
|
370
|
+
variant: "fullscreen",
|
|
371
|
+
justifyDialog: "end",
|
|
372
|
+
alignDialog: "start",
|
|
373
|
+
lockViewport: true,
|
|
374
|
+
allowContentScroll: false,
|
|
375
|
+
theme: "error" as SemanticTheme,
|
|
376
|
+
closeIcon: "material-symbols:close",
|
|
377
|
+
},
|
|
378
|
+
slots: { dialogContent: "<p>Content</p>" },
|
|
379
|
+
});
|
|
380
|
+
expect(wrapper.find(".inner").classes()).toContain("fullscreen");
|
|
381
|
+
expect(wrapper.attributes("align-dialog")).toBe("start");
|
|
382
|
+
expect(wrapper.attributes("justify-dialog")).toBe("end");
|
|
383
|
+
expect(document.body.classList.contains("lock")).toBe(true);
|
|
384
|
+
expect(wrapper.find(".dialog-content").classes()).not.toContain("allow-content-scroll");
|
|
385
|
+
expect(wrapper.find(".header").attributes("data-theme")).toBe("error");
|
|
386
|
+
expect(iconName(wrapper)).toContain("material-symbols:close");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("lockViewport:false in app.config skips body lock on mount", async () => {
|
|
390
|
+
useAppConfigMock.mockReturnValue({
|
|
391
|
+
icon: {},
|
|
392
|
+
srcdev: { displayDialog: { lockViewport: false } },
|
|
393
|
+
});
|
|
394
|
+
await mountSuspended(DisplayDialog, { props: { dataDialogId: "test" } });
|
|
395
|
+
expect(document.body.classList.contains("lock")).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
264
398
|
});
|
package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`DisplayDialog > renders correct HTML structure with all slots 1`] = `
|
|
4
|
-
"<dialog class="display-dialog"
|
|
4
|
+
"<dialog class="display-dialog" aria-modal="true" aria-labelledby="v-0-0" align-dialog="center" justify-dialog="center" open="" data-dialog-id="test">
|
|
5
5
|
<div class="inner dialog">
|
|
6
6
|
<div class="header">
|
|
7
|
-
<div class="col-left">
|
|
7
|
+
<div id="v-0-0" class="col-left">
|
|
8
8
|
<p>Title</p>
|
|
9
9
|
</div>
|
|
10
|
-
<div class="col-right"><button data-test-id="display-dialog-header-close" class="display-
|
|
10
|
+
<div class="col-right"><button data-test-id="display-dialog-header-close" class="display-dialog-close"><span class="iconify undefinedbitcoin-icons:cross-filled icon"></span><span class="sr-only">Close</span></button></div>
|
|
11
11
|
</div>
|
|
12
12
|
<div class="dialog-content">
|
|
13
13
|
<p>Content</p>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="promptElementRef"
|
|
4
|
+
class="display-prompt-core"
|
|
5
|
+
:class="[{ closed: !componentOpen }]"
|
|
6
|
+
:data-test-id="`display-prompt-core-${resolved.theme}`"
|
|
7
|
+
tabindex="0"
|
|
8
|
+
>
|
|
9
|
+
<div class="display-prompt-wrapper" :data-theme="resolved.theme" :class="[elementClasses]" data-test-id="display-prompt">
|
|
10
|
+
<component
|
|
11
|
+
:is="contentComponent"
|
|
12
|
+
:theme="resolved.theme"
|
|
13
|
+
:dismissible="resolved.dismissible"
|
|
14
|
+
:aria-live="resolved.useAutoFocus ? 'polite' : undefined"
|
|
15
|
+
@dismiss="updateComponentState()"
|
|
16
|
+
>
|
|
17
|
+
<template v-if="slots.customDecoratorIcon" #icon>
|
|
18
|
+
<slot name="customDecoratorIcon"></slot>
|
|
19
|
+
</template>
|
|
20
|
+
<template v-if="slots.title" #title>
|
|
21
|
+
<slot name="title"></slot>
|
|
22
|
+
</template>
|
|
23
|
+
<template v-if="slots.content" #content>
|
|
24
|
+
<slot name="content"></slot>
|
|
25
|
+
</template>
|
|
26
|
+
<template v-if="slots.customCloseIcon" #dismissIcon>
|
|
27
|
+
<slot name="customCloseIcon"></slot>
|
|
28
|
+
</template>
|
|
29
|
+
<template #dismissLabel>
|
|
30
|
+
<slot name="customTitle">Close this prompt</slot>
|
|
31
|
+
</template>
|
|
32
|
+
</component>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import AlertContent from "~/components/02.molecules/alert-content/AlertContent.vue";
|
|
39
|
+
import AlertMaskedContent from "~/components/02.molecules/alert-masked-content/AlertMaskedContent.vue";
|
|
40
|
+
import type { DisplayPromptTheme } from "~/types/components";
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
theme?: DisplayPromptTheme;
|
|
44
|
+
dismissible?: boolean;
|
|
45
|
+
useAutoFocus?: boolean;
|
|
46
|
+
masked?: boolean;
|
|
47
|
+
styleClassPassthrough?: string | string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
51
|
+
styleClassPassthrough: () => [],
|
|
52
|
+
theme: undefined,
|
|
53
|
+
dismissible: undefined,
|
|
54
|
+
useAutoFocus: undefined,
|
|
55
|
+
masked: undefined,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const appConfig = useAppConfig();
|
|
59
|
+
|
|
60
|
+
const resolved = computed(() => {
|
|
61
|
+
const config = appConfig.srcdev?.displayPrompt;
|
|
62
|
+
return {
|
|
63
|
+
theme: props.theme ?? config?.theme ?? "info",
|
|
64
|
+
dismissible: props.dismissible ?? config?.dismissible ?? false,
|
|
65
|
+
useAutoFocus: props.useAutoFocus ?? config?.useAutoFocus ?? false,
|
|
66
|
+
masked: props.masked ?? config?.masked ?? false,
|
|
67
|
+
} as const;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const contentComponent = computed(() => (resolved.value.masked ? AlertMaskedContent : AlertContent));
|
|
71
|
+
|
|
72
|
+
const slots = useSlots();
|
|
73
|
+
const promptElementRef = useTemplateRef<HTMLElement>("promptElementRef");
|
|
74
|
+
const parentComponentState = defineModel<boolean>({ default: false });
|
|
75
|
+
const componentOpen = ref(true);
|
|
76
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
77
|
+
|
|
78
|
+
const updateComponentState = () => {
|
|
79
|
+
if (parentComponentState.value) {
|
|
80
|
+
parentComponentState.value = false;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
componentOpen.value = false;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
onMounted(async () => {
|
|
88
|
+
if (resolved.value.useAutoFocus && promptElementRef.value) {
|
|
89
|
+
promptElementRef.value.focus();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<style lang="css">
|
|
95
|
+
@layer components {
|
|
96
|
+
.display-prompt-core {
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-rows: 1fr;
|
|
99
|
+
opacity: 1;
|
|
100
|
+
transition: all 200ms ease-in-out;
|
|
101
|
+
|
|
102
|
+
&.closed {
|
|
103
|
+
grid-template-rows: 0fr;
|
|
104
|
+
opacity: 0;
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.display-prompt-wrapper {
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import StorybookComponent from "../DisplayPrompt.vue";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: "Atoms/DisplayPrompt",
|
|
7
|
+
component: StorybookComponent,
|
|
8
|
+
argTypes: {
|
|
9
|
+
theme: {
|
|
10
|
+
control: { type: "inline-radio" },
|
|
11
|
+
options: ["info", "success", "warning", "error"],
|
|
12
|
+
description: "Semantic theme for the prompt",
|
|
13
|
+
table: { category: "Appearance" },
|
|
14
|
+
},
|
|
15
|
+
dismissible: {
|
|
16
|
+
control: { type: "boolean" },
|
|
17
|
+
description: "Show a close button to dismiss the prompt",
|
|
18
|
+
table: { category: "Behaviour" },
|
|
19
|
+
},
|
|
20
|
+
useAutoFocus: {
|
|
21
|
+
control: { type: "boolean" },
|
|
22
|
+
description: "Focuses the prompt element on mount",
|
|
23
|
+
table: { category: "Behaviour" },
|
|
24
|
+
},
|
|
25
|
+
styleClassPassthrough: {
|
|
26
|
+
control: { type: "object" },
|
|
27
|
+
description: "Extra classes applied to the prompt wrapper",
|
|
28
|
+
table: { category: "Styling" },
|
|
29
|
+
},
|
|
30
|
+
modelValue: { table: { disable: true } },
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
theme: "info",
|
|
34
|
+
dismissible: false,
|
|
35
|
+
useAutoFocus: false,
|
|
36
|
+
styleClassPassthrough: [],
|
|
37
|
+
},
|
|
38
|
+
} as Meta<typeof StorybookComponent>;
|
|
39
|
+
|
|
40
|
+
const Template: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
41
|
+
components: { StorybookComponent },
|
|
42
|
+
setup() {
|
|
43
|
+
const model = ref(false);
|
|
44
|
+
return { args, model };
|
|
45
|
+
},
|
|
46
|
+
template: `
|
|
47
|
+
<div style="padding: 2rem; max-width: 640px;">
|
|
48
|
+
<StorybookComponent
|
|
49
|
+
v-model="model"
|
|
50
|
+
:theme="args.theme"
|
|
51
|
+
:dismissible="args.dismissible"
|
|
52
|
+
:use-auto-focus="args.useAutoFocus"
|
|
53
|
+
:style-class-passthrough="args.styleClassPassthrough"
|
|
54
|
+
>
|
|
55
|
+
<template #title>Prompt title</template>
|
|
56
|
+
<template #content>This is the prompt content. It can contain any information you want to communicate to the user.</template>
|
|
57
|
+
</StorybookComponent>
|
|
58
|
+
</div>
|
|
59
|
+
`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const Default = Template.bind({});
|
|
63
|
+
|
|
64
|
+
export const InfoTheme = Template.bind({});
|
|
65
|
+
InfoTheme.args = {
|
|
66
|
+
theme: "info",
|
|
67
|
+
dismissible: true,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const SuccessTheme = Template.bind({});
|
|
71
|
+
SuccessTheme.args = {
|
|
72
|
+
theme: "success",
|
|
73
|
+
dismissible: true,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const WarningTheme = Template.bind({});
|
|
77
|
+
WarningTheme.args = {
|
|
78
|
+
theme: "warning",
|
|
79
|
+
dismissible: true,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const ErrorTheme = Template.bind({});
|
|
83
|
+
ErrorTheme.args = {
|
|
84
|
+
theme: "error",
|
|
85
|
+
dismissible: true,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Dismissible = Template.bind({});
|
|
89
|
+
Dismissible.args = {
|
|
90
|
+
theme: "info",
|
|
91
|
+
dismissible: true,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const AllThemesDismissible: StoryFn<typeof StorybookComponent> = () => ({
|
|
95
|
+
components: { StorybookComponent },
|
|
96
|
+
setup() {
|
|
97
|
+
return { themes: ["info", "success", "warning", "error"] as const };
|
|
98
|
+
},
|
|
99
|
+
template: `
|
|
100
|
+
<div style="padding: 2rem; display: flex; flex-direction: column; gap: 1.6rem; max-width: 640px;">
|
|
101
|
+
<StorybookComponent
|
|
102
|
+
v-for="theme in themes"
|
|
103
|
+
:key="theme"
|
|
104
|
+
:theme="theme"
|
|
105
|
+
:dismissible="true"
|
|
106
|
+
>
|
|
107
|
+
<template #title>{{ theme.charAt(0).toUpperCase() + theme.slice(1) }} prompt</template>
|
|
108
|
+
<template #content>This is the {{ theme }} variant of the prompt component.</template>
|
|
109
|
+
</StorybookComponent>
|
|
110
|
+
</div>
|
|
111
|
+
`,
|
|
112
|
+
});
|