srcdev-nuxt-components 9.6.19 → 9.6.20
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/rules/consumer/ids-from-useid.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +5 -0
- package/.claude/skills/components/auto-grid.md +3 -1
- package/.claude/skills/components/capture-qr-code.md +9 -5
- package/.claude/skills/components/carousel-flip.md +41 -30
- package/.claude/skills/components/column-flow-grid.md +2 -0
- package/.claude/skills/components/dashboard-quad-grid.md +7 -2
- package/.claude/skills/components/dashboard-stats-grid.md +4 -0
- package/.claude/skills/components/decode-qr-code.md +7 -2
- package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
- package/.claude/skills/components/deep-expanding-menu.md +22 -21
- package/.claude/skills/components/grid-stack.md +2 -0
- package/.claude/skills/components/layout-grid-by-cols.md +2 -0
- package/.claude/skills/components/masonry-grid.md +9 -1
- package/.claude/skills/components/page-row.md +3 -0
- package/.claude/skills/components/samaritan-prompt.md +24 -8
- package/.claude/skills/components/site-header.md +16 -2
- package/.claude/skills/index.md +12 -12
- package/.claude/skills/storybook-add-story.md +25 -0
- package/.claude/skills/testing-add-unit-test.md +26 -0
- package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
- package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
- package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
- package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
- package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
- package/.vscode/srcdev-component-site-header.code-snippets +13 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
- package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
- package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
- package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
- package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
- package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
- package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
- package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
- package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
- package/app/components/01.atoms/page-row/PageRow.vue +16 -15
- package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
- package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
- package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
- package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
- package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
- package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
- package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
- package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
- package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
- package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
- package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
- package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
- package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
- package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
- package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
- package/app/types/components/qr-code.d.ts +12 -0
- package/app/utils/graphemes.ts +8 -0
- package/package.json +1 -1
- package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
- package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
- package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
- package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
- package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
- package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
- package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
- package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { nextTick } from "vue";
|
|
4
4
|
import CaptureQrCode from "../CaptureQrCode.vue";
|
|
5
5
|
|
|
6
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
7
|
+
|
|
8
|
+
type CaptureMountOptions = Parameters<typeof mountSuspended<typeof CaptureQrCode>>[1];
|
|
9
|
+
|
|
10
|
+
// The real QrcodeStream queues camera start/stop on a module-level promise chain shared by every
|
|
11
|
+
// instance, so a start left over from an earlier test can fail mid-test and flip state.error.
|
|
12
|
+
const qrcodeStreamStub = { name: "QrcodeStream", emits: ["detect", "error"], template: `<div class="qrcode-stream-stub"></div>` };
|
|
13
|
+
|
|
14
|
+
const mountCapture = async (options: CaptureMountOptions = {}) => {
|
|
15
|
+
const wrapper = await mountSuspended(CaptureQrCode, {
|
|
16
|
+
...options,
|
|
17
|
+
global: { ...options?.global, stubs: { QrcodeStream: qrcodeStreamStub, ...options?.global?.stubs } },
|
|
18
|
+
});
|
|
19
|
+
mounted.push(wrapper);
|
|
20
|
+
return wrapper;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
6
28
|
interface CaptureVM {
|
|
7
29
|
state: { error: boolean; errorMsg: string; cameraOn: boolean };
|
|
8
30
|
result: string[] | undefined;
|
|
@@ -12,63 +34,52 @@ interface CaptureVM {
|
|
|
12
34
|
}
|
|
13
35
|
|
|
14
36
|
describe("CaptureQrCode", () => {
|
|
15
|
-
// QrcodeStream (from nuxt-qrcode) runs canvas-based detection internally.
|
|
16
|
-
// jsdom defines HTMLCanvasElement but returns null for getContext, which
|
|
17
|
-
// causes the library to throw. We provide a minimal non-null mock.
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({} as unknown as RenderingContext);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
vi.restoreAllMocks();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
37
|
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
27
38
|
|
|
28
39
|
it("mounts without error", async () => {
|
|
29
|
-
const wrapper = await
|
|
40
|
+
const wrapper = await mountCapture();
|
|
30
41
|
expect(wrapper.vm).toBeTruthy();
|
|
31
42
|
});
|
|
32
43
|
|
|
33
44
|
// ─── Root element ─────────────────────────────────────────────────────────
|
|
34
45
|
|
|
35
46
|
it("renders as a div", async () => {
|
|
36
|
-
const wrapper = await
|
|
47
|
+
const wrapper = await mountCapture();
|
|
37
48
|
expect(wrapper.element.tagName).toBe("DIV");
|
|
38
49
|
});
|
|
39
50
|
|
|
40
51
|
it("always has the capture-qr-code class", async () => {
|
|
41
|
-
const wrapper = await
|
|
52
|
+
const wrapper = await mountCapture();
|
|
42
53
|
expect(wrapper.classes()).toContain("capture-qr-code");
|
|
43
54
|
});
|
|
44
55
|
|
|
45
56
|
// ─── Initial state ────────────────────────────────────────────────────────
|
|
46
57
|
|
|
47
58
|
it("has camera on by default", async () => {
|
|
48
|
-
const wrapper = await
|
|
59
|
+
const wrapper = await mountCapture();
|
|
49
60
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
50
61
|
expect(vm.state.cameraOn).toBe(true);
|
|
51
62
|
});
|
|
52
63
|
|
|
53
64
|
it("does not show camera-stopped on mount", async () => {
|
|
54
|
-
const wrapper = await
|
|
65
|
+
const wrapper = await mountCapture();
|
|
55
66
|
expect(wrapper.find(".capture-qr-code-stopped").exists()).toBe(false);
|
|
56
67
|
});
|
|
57
68
|
|
|
58
69
|
it("does not show scanned results initially", async () => {
|
|
59
|
-
const wrapper = await
|
|
70
|
+
const wrapper = await mountCapture();
|
|
60
71
|
expect(wrapper.find(".capture-qr-code-results ul").exists()).toBe(false);
|
|
61
72
|
});
|
|
62
73
|
|
|
63
74
|
it("does not show the error state initially", async () => {
|
|
64
|
-
const wrapper = await
|
|
75
|
+
const wrapper = await mountCapture();
|
|
65
76
|
expect(wrapper.find(".capture-qr-code-error").exists()).toBe(false);
|
|
66
77
|
});
|
|
67
78
|
|
|
68
79
|
// ─── Camera off ───────────────────────────────────────────────────────────
|
|
69
80
|
|
|
70
81
|
it("hides the stream and shows camera-stopped when cameraOn is false", async () => {
|
|
71
|
-
const wrapper = await
|
|
82
|
+
const wrapper = await mountCapture();
|
|
72
83
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
73
84
|
vm.state.cameraOn = false;
|
|
74
85
|
await nextTick();
|
|
@@ -79,23 +90,49 @@ describe("CaptureQrCode", () => {
|
|
|
79
90
|
// ─── Error state ──────────────────────────────────────────────────────────
|
|
80
91
|
|
|
81
92
|
it("shows camera-error when onError is called", async () => {
|
|
82
|
-
const wrapper = await
|
|
93
|
+
const wrapper = await mountCapture();
|
|
83
94
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
84
95
|
vm.onError(new Error("Permission denied"));
|
|
85
96
|
await nextTick();
|
|
86
97
|
expect(wrapper.find(".capture-qr-code-error").exists()).toBe(true);
|
|
87
98
|
});
|
|
88
99
|
|
|
89
|
-
it("shows
|
|
90
|
-
const wrapper = await
|
|
100
|
+
it("shows a plain-language message for a known error", async () => {
|
|
101
|
+
const wrapper = await mountCapture();
|
|
102
|
+
const err = new Error("Permission denied");
|
|
103
|
+
err.name = "NotAllowedError";
|
|
104
|
+
(wrapper.vm as unknown as CaptureVM).onError(err);
|
|
105
|
+
await nextTick();
|
|
106
|
+
expect(wrapper.find(".capture-qr-code-error-message").text()).toBe(
|
|
107
|
+
"Camera access was blocked. Allow it in your browser settings, then try again."
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("falls back to the default message for an unknown error", async () => {
|
|
112
|
+
const wrapper = await mountCapture();
|
|
113
|
+
(wrapper.vm as unknown as CaptureVM).onError(new Error("Something odd"));
|
|
114
|
+
await nextTick();
|
|
115
|
+
expect(wrapper.find(".capture-qr-code-error-message").text()).toBe("The camera couldn't be started. Try again.");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("uses errorMessages overrides, per error and as the default", async () => {
|
|
119
|
+
const wrapper = await mountCapture({
|
|
120
|
+
props: { errorMessages: { NotFoundError: "Aucune caméra", default: "Erreur" } },
|
|
121
|
+
});
|
|
91
122
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
92
|
-
|
|
123
|
+
const notFound = new Error("x");
|
|
124
|
+
notFound.name = "NotFoundError";
|
|
125
|
+
vm.onError(notFound);
|
|
93
126
|
await nextTick();
|
|
94
|
-
expect(wrapper.find(".capture-qr-code-error
|
|
127
|
+
expect(wrapper.find(".capture-qr-code-error-message").text()).toBe("Aucune caméra");
|
|
128
|
+
|
|
129
|
+
vm.onError(new Error("unknown"));
|
|
130
|
+
await nextTick();
|
|
131
|
+
expect(wrapper.find(".capture-qr-code-error-message").text()).toBe("Erreur");
|
|
95
132
|
});
|
|
96
133
|
|
|
97
134
|
it("sets state.error when onError is called, hiding the stream", async () => {
|
|
98
|
-
const wrapper = await
|
|
135
|
+
const wrapper = await mountCapture();
|
|
99
136
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
100
137
|
vm.onError(new Error("test"));
|
|
101
138
|
await nextTick();
|
|
@@ -104,7 +141,7 @@ describe("CaptureQrCode", () => {
|
|
|
104
141
|
});
|
|
105
142
|
|
|
106
143
|
it("shows a reset button in the error state", async () => {
|
|
107
|
-
const wrapper = await
|
|
144
|
+
const wrapper = await mountCapture();
|
|
108
145
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
109
146
|
vm.onError(new Error("test"));
|
|
110
147
|
await nextTick();
|
|
@@ -114,7 +151,7 @@ describe("CaptureQrCode", () => {
|
|
|
114
151
|
// ─── Reset camera ─────────────────────────────────────────────────────────
|
|
115
152
|
|
|
116
153
|
it("clears the error state when resetCamera is called", async () => {
|
|
117
|
-
const wrapper = await
|
|
154
|
+
const wrapper = await mountCapture();
|
|
118
155
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
119
156
|
vm.onError(new Error("test"));
|
|
120
157
|
await nextTick();
|
|
@@ -125,7 +162,7 @@ describe("CaptureQrCode", () => {
|
|
|
125
162
|
});
|
|
126
163
|
|
|
127
164
|
it("clears the error state when the reset button is clicked", async () => {
|
|
128
|
-
const wrapper = await
|
|
165
|
+
const wrapper = await mountCapture();
|
|
129
166
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
130
167
|
vm.onError(new Error("test"));
|
|
131
168
|
await nextTick();
|
|
@@ -136,7 +173,7 @@ describe("CaptureQrCode", () => {
|
|
|
136
173
|
// ─── Detect ───────────────────────────────────────────────────────────────
|
|
137
174
|
|
|
138
175
|
it("shows scanned results after onDetect is called", async () => {
|
|
139
|
-
const wrapper = await
|
|
176
|
+
const wrapper = await mountCapture();
|
|
140
177
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
141
178
|
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
142
179
|
await nextTick();
|
|
@@ -144,7 +181,7 @@ describe("CaptureQrCode", () => {
|
|
|
144
181
|
});
|
|
145
182
|
|
|
146
183
|
it("shows the detected value in the results list", async () => {
|
|
147
|
-
const wrapper = await
|
|
184
|
+
const wrapper = await mountCapture();
|
|
148
185
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
149
186
|
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
150
187
|
await nextTick();
|
|
@@ -152,7 +189,7 @@ describe("CaptureQrCode", () => {
|
|
|
152
189
|
});
|
|
153
190
|
|
|
154
191
|
it("shows all detected values when multiple codes are scanned", async () => {
|
|
155
|
-
const wrapper = await
|
|
192
|
+
const wrapper = await mountCapture();
|
|
156
193
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
157
194
|
vm.onDetect([{ rawValue: "https://one.com" }, { rawValue: "https://two.com" }]);
|
|
158
195
|
await nextTick();
|
|
@@ -163,7 +200,7 @@ describe("CaptureQrCode", () => {
|
|
|
163
200
|
});
|
|
164
201
|
|
|
165
202
|
it("replaces results on subsequent scans", async () => {
|
|
166
|
-
const wrapper = await
|
|
203
|
+
const wrapper = await mountCapture();
|
|
167
204
|
const vm = wrapper.vm as unknown as CaptureVM;
|
|
168
205
|
vm.onDetect([{ rawValue: "first" }]);
|
|
169
206
|
await nextTick();
|
|
@@ -177,25 +214,69 @@ describe("CaptureQrCode", () => {
|
|
|
177
214
|
// ─── Visibility change ────────────────────────────────────────────────────
|
|
178
215
|
|
|
179
216
|
it("stops the camera when the page becomes hidden", async () => {
|
|
180
|
-
const wrapper = await
|
|
217
|
+
const wrapper = await mountCapture();
|
|
218
|
+
expect(wrapper.find(".qrcode-stream-stub").exists()).toBe(true);
|
|
181
219
|
Object.defineProperty(document, "hidden", { get: () => true, configurable: true });
|
|
182
220
|
document.dispatchEvent(new Event("visibilitychange"));
|
|
183
221
|
await nextTick();
|
|
184
|
-
expect(wrapper.find("
|
|
222
|
+
expect(wrapper.find(".qrcode-stream-stub").exists()).toBe(false);
|
|
185
223
|
Object.defineProperty(document, "hidden", { get: () => false, configurable: true });
|
|
186
224
|
});
|
|
187
225
|
|
|
226
|
+
it("turns the camera back on when the page becomes visible again", async () => {
|
|
227
|
+
const wrapper = await mountCapture();
|
|
228
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
229
|
+
Object.defineProperty(document, "hidden", { get: () => true, configurable: true });
|
|
230
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
231
|
+
await nextTick();
|
|
232
|
+
expect(vm.state.cameraOn).toBe(false);
|
|
233
|
+
|
|
234
|
+
Object.defineProperty(document, "hidden", { get: () => false, configurable: true });
|
|
235
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
236
|
+
await nextTick();
|
|
237
|
+
expect(vm.state.cameraOn).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("stays in the error state when the page becomes visible again", async () => {
|
|
241
|
+
const wrapper = await mountCapture();
|
|
242
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
243
|
+
vm.onError(new Error("test"));
|
|
244
|
+
Object.defineProperty(document, "hidden", { get: () => true, configurable: true });
|
|
245
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
246
|
+
Object.defineProperty(document, "hidden", { get: () => false, configurable: true });
|
|
247
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
248
|
+
await nextTick();
|
|
249
|
+
expect(wrapper.find(".capture-qr-code-error").exists()).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("only stops camera streams inside its own root", async () => {
|
|
253
|
+
const outside = document.createElement("video");
|
|
254
|
+
const stop = vi.fn();
|
|
255
|
+
Object.defineProperty(outside, "srcObject", {
|
|
256
|
+
value: { getTracks: () => [{ stop }] },
|
|
257
|
+
writable: true,
|
|
258
|
+
configurable: true,
|
|
259
|
+
});
|
|
260
|
+
document.body.appendChild(outside);
|
|
261
|
+
|
|
262
|
+
const wrapper = await mountCapture();
|
|
263
|
+
wrapper.unmount();
|
|
264
|
+
mounted.splice(mounted.indexOf(wrapper), 1);
|
|
265
|
+
expect(stop).not.toHaveBeenCalled();
|
|
266
|
+
outside.remove();
|
|
267
|
+
});
|
|
268
|
+
|
|
188
269
|
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
189
270
|
|
|
190
271
|
it("applies a single styleClassPassthrough string", async () => {
|
|
191
|
-
const wrapper = await
|
|
272
|
+
const wrapper = await mountCapture({
|
|
192
273
|
props: { styleClassPassthrough: "custom-class" },
|
|
193
274
|
});
|
|
194
275
|
expect(wrapper.classes()).toContain("custom-class");
|
|
195
276
|
});
|
|
196
277
|
|
|
197
278
|
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
198
|
-
const wrapper = await
|
|
279
|
+
const wrapper = await mountCapture({
|
|
199
280
|
props: { styleClassPassthrough: ["class-a", "class-b"] },
|
|
200
281
|
});
|
|
201
282
|
expect(wrapper.classes()).toContain("class-a");
|
|
@@ -203,7 +284,7 @@ describe("CaptureQrCode", () => {
|
|
|
203
284
|
});
|
|
204
285
|
|
|
205
286
|
it("retains capture-qr-code class alongside styleClassPassthrough", async () => {
|
|
206
|
-
const wrapper = await
|
|
287
|
+
const wrapper = await mountCapture({
|
|
207
288
|
props: { styleClassPassthrough: "extra" },
|
|
208
289
|
});
|
|
209
290
|
expect(wrapper.classes()).toContain("capture-qr-code");
|
|
@@ -212,23 +293,15 @@ describe("CaptureQrCode", () => {
|
|
|
212
293
|
});
|
|
213
294
|
|
|
214
295
|
describe("CaptureQrCode labels and a11y", () => {
|
|
215
|
-
beforeEach(() => {
|
|
216
|
-
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({} as unknown as RenderingContext);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
afterEach(() => {
|
|
220
|
-
vi.restoreAllMocks();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
296
|
it("uses cameraStoppedLabel for the stopped message", async () => {
|
|
224
|
-
const wrapper = await
|
|
297
|
+
const wrapper = await mountCapture({ props: { cameraStoppedLabel: "Caméra arrêtée" } });
|
|
225
298
|
(wrapper.vm as unknown as CaptureVM).state.cameraOn = false;
|
|
226
299
|
await nextTick();
|
|
227
300
|
expect(wrapper.find(".capture-qr-code-stopped").text()).toBe("Caméra arrêtée");
|
|
228
301
|
});
|
|
229
302
|
|
|
230
303
|
it("uses resetCameraLabel for the reset button and marks the error as an alert", async () => {
|
|
231
|
-
const wrapper = await
|
|
304
|
+
const wrapper = await mountCapture({ props: { resetCameraLabel: "Réinitialiser" } });
|
|
232
305
|
(wrapper.vm as unknown as CaptureVM).onError(new Error("test"));
|
|
233
306
|
await nextTick();
|
|
234
307
|
expect(wrapper.find(".capture-qr-code-error").attributes("role")).toBe("alert");
|
|
@@ -236,7 +309,7 @@ describe("CaptureQrCode labels and a11y", () => {
|
|
|
236
309
|
});
|
|
237
310
|
|
|
238
311
|
it("lets the error slot replace the default error content", async () => {
|
|
239
|
-
const wrapper = await
|
|
312
|
+
const wrapper = await mountCapture({
|
|
240
313
|
slots: { error: ({ error }: { error: string }) => `Custom: ${error}` },
|
|
241
314
|
});
|
|
242
315
|
const err = new Error("denied");
|
|
@@ -247,12 +320,12 @@ describe("CaptureQrCode labels and a11y", () => {
|
|
|
247
320
|
});
|
|
248
321
|
|
|
249
322
|
it("keeps a polite live region for results", async () => {
|
|
250
|
-
const wrapper = await
|
|
323
|
+
const wrapper = await mountCapture();
|
|
251
324
|
expect(wrapper.find(".capture-qr-code-results").attributes("aria-live")).toBe("polite");
|
|
252
325
|
});
|
|
253
326
|
|
|
254
327
|
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
255
|
-
const wrapper = await
|
|
328
|
+
const wrapper = await mountCapture({ props: { styleClassPassthrough: ["original"] } });
|
|
256
329
|
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
257
330
|
expect(wrapper.classes()).not.toContain("original");
|
|
258
331
|
expect(wrapper.classes()).toContain("updated");
|
|
@@ -1,56 +1,70 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { nextTick } from "vue";
|
|
4
4
|
import DecodeQrCode from "../DecodeQrCode.vue";
|
|
5
5
|
|
|
6
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
7
|
+
|
|
8
|
+
const mountDecode = async (options: { props?: Record<string, unknown> } = {}) => {
|
|
9
|
+
const wrapper = await mountSuspended(DecodeQrCode, options);
|
|
10
|
+
mounted.push(wrapper);
|
|
11
|
+
return wrapper;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
6
19
|
interface DecodeVM {
|
|
7
20
|
result: string[] | undefined;
|
|
8
21
|
isDropping: boolean;
|
|
9
22
|
onDetect: (codes: { rawValue: string }[]) => void;
|
|
10
23
|
onDropping: (dropping: boolean) => void;
|
|
24
|
+
onDropError: () => void;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
describe("DecodeQrCode", () => {
|
|
14
28
|
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
15
29
|
|
|
16
30
|
it("mounts without error", async () => {
|
|
17
|
-
const wrapper = await
|
|
31
|
+
const wrapper = await mountDecode();
|
|
18
32
|
expect(wrapper.vm).toBeTruthy();
|
|
19
33
|
});
|
|
20
34
|
|
|
21
35
|
// ─── Root element ─────────────────────────────────────────────────────────
|
|
22
36
|
|
|
23
37
|
it("renders as a div", async () => {
|
|
24
|
-
const wrapper = await
|
|
38
|
+
const wrapper = await mountDecode();
|
|
25
39
|
expect(wrapper.element.tagName).toBe("DIV");
|
|
26
40
|
});
|
|
27
41
|
|
|
28
42
|
it("always has the decode-qr-code class", async () => {
|
|
29
|
-
const wrapper = await
|
|
43
|
+
const wrapper = await mountDecode();
|
|
30
44
|
expect(wrapper.classes()).toContain("decode-qr-code");
|
|
31
45
|
});
|
|
32
46
|
|
|
33
47
|
// ─── Initial DOM structure ─────────────────────────────────────────────────
|
|
34
48
|
|
|
35
49
|
it("renders the file capture element", async () => {
|
|
36
|
-
const wrapper = await
|
|
50
|
+
const wrapper = await mountDecode();
|
|
37
51
|
expect(wrapper.find(".decode-qr-code-capture").exists()).toBe(true);
|
|
38
52
|
});
|
|
39
53
|
|
|
40
54
|
it("renders the drop zone element", async () => {
|
|
41
|
-
const wrapper = await
|
|
55
|
+
const wrapper = await mountDecode();
|
|
42
56
|
expect(wrapper.find(".decode-qr-code-dropzone").exists()).toBe(true);
|
|
43
57
|
});
|
|
44
58
|
|
|
45
59
|
it("does not show scanned results initially", async () => {
|
|
46
|
-
const wrapper = await
|
|
60
|
+
const wrapper = await mountDecode();
|
|
47
61
|
expect(wrapper.find(".decode-qr-code-results ul").exists()).toBe(false);
|
|
48
62
|
});
|
|
49
63
|
|
|
50
64
|
// ─── Detect ───────────────────────────────────────────────────────────────
|
|
51
65
|
|
|
52
66
|
it("shows scanned results after onDetect is called", async () => {
|
|
53
|
-
const wrapper = await
|
|
67
|
+
const wrapper = await mountDecode();
|
|
54
68
|
const vm = wrapper.vm as unknown as DecodeVM;
|
|
55
69
|
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
56
70
|
await nextTick();
|
|
@@ -58,7 +72,7 @@ describe("DecodeQrCode", () => {
|
|
|
58
72
|
});
|
|
59
73
|
|
|
60
74
|
it("shows the detected value in the results list", async () => {
|
|
61
|
-
const wrapper = await
|
|
75
|
+
const wrapper = await mountDecode();
|
|
62
76
|
const vm = wrapper.vm as unknown as DecodeVM;
|
|
63
77
|
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
64
78
|
await nextTick();
|
|
@@ -66,7 +80,7 @@ describe("DecodeQrCode", () => {
|
|
|
66
80
|
});
|
|
67
81
|
|
|
68
82
|
it("shows all detected values when multiple codes are decoded", async () => {
|
|
69
|
-
const wrapper = await
|
|
83
|
+
const wrapper = await mountDecode();
|
|
70
84
|
const vm = wrapper.vm as unknown as DecodeVM;
|
|
71
85
|
vm.onDetect([{ rawValue: "https://one.com" }, { rawValue: "https://two.com" }]);
|
|
72
86
|
await nextTick();
|
|
@@ -77,7 +91,7 @@ describe("DecodeQrCode", () => {
|
|
|
77
91
|
});
|
|
78
92
|
|
|
79
93
|
it("replaces results on subsequent detections", async () => {
|
|
80
|
-
const wrapper = await
|
|
94
|
+
const wrapper = await mountDecode();
|
|
81
95
|
const vm = wrapper.vm as unknown as DecodeVM;
|
|
82
96
|
vm.onDetect([{ rawValue: "first" }]);
|
|
83
97
|
await nextTick();
|
|
@@ -89,7 +103,7 @@ describe("DecodeQrCode", () => {
|
|
|
89
103
|
});
|
|
90
104
|
|
|
91
105
|
it("hides results when detecting an empty array", async () => {
|
|
92
|
-
const wrapper = await
|
|
106
|
+
const wrapper = await mountDecode();
|
|
93
107
|
const vm = wrapper.vm as unknown as DecodeVM;
|
|
94
108
|
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
95
109
|
await nextTick();
|
|
@@ -101,7 +115,7 @@ describe("DecodeQrCode", () => {
|
|
|
101
115
|
// ─── Dropping state ───────────────────────────────────────────────────────
|
|
102
116
|
|
|
103
117
|
it("sets isDropping to true when onDropping is called with true", async () => {
|
|
104
|
-
const wrapper = await
|
|
118
|
+
const wrapper = await mountDecode();
|
|
105
119
|
const vm = wrapper.vm as unknown as DecodeVM;
|
|
106
120
|
vm.onDropping(true);
|
|
107
121
|
await nextTick();
|
|
@@ -109,7 +123,7 @@ describe("DecodeQrCode", () => {
|
|
|
109
123
|
});
|
|
110
124
|
|
|
111
125
|
it("sets isDropping to false when onDropping is called with false", async () => {
|
|
112
|
-
const wrapper = await
|
|
126
|
+
const wrapper = await mountDecode();
|
|
113
127
|
const vm = wrapper.vm as unknown as DecodeVM;
|
|
114
128
|
vm.onDropping(true);
|
|
115
129
|
await nextTick();
|
|
@@ -121,14 +135,14 @@ describe("DecodeQrCode", () => {
|
|
|
121
135
|
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
122
136
|
|
|
123
137
|
it("applies a single styleClassPassthrough string", async () => {
|
|
124
|
-
const wrapper = await
|
|
138
|
+
const wrapper = await mountDecode({
|
|
125
139
|
props: { styleClassPassthrough: "custom-class" },
|
|
126
140
|
});
|
|
127
141
|
expect(wrapper.classes()).toContain("custom-class");
|
|
128
142
|
});
|
|
129
143
|
|
|
130
144
|
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
131
|
-
const wrapper = await
|
|
145
|
+
const wrapper = await mountDecode({
|
|
132
146
|
props: { styleClassPassthrough: ["class-a", "class-b"] },
|
|
133
147
|
});
|
|
134
148
|
expect(wrapper.classes()).toContain("class-a");
|
|
@@ -136,7 +150,7 @@ describe("DecodeQrCode", () => {
|
|
|
136
150
|
});
|
|
137
151
|
|
|
138
152
|
it("retains decode-qr-code class alongside styleClassPassthrough", async () => {
|
|
139
|
-
const wrapper = await
|
|
153
|
+
const wrapper = await mountDecode({
|
|
140
154
|
props: { styleClassPassthrough: "extra" },
|
|
141
155
|
});
|
|
142
156
|
expect(wrapper.classes()).toContain("decode-qr-code");
|
|
@@ -146,34 +160,69 @@ describe("DecodeQrCode", () => {
|
|
|
146
160
|
|
|
147
161
|
describe("DecodeQrCode labels, drop state and a11y", () => {
|
|
148
162
|
it("wraps the file input in a label with the default upload text", async () => {
|
|
149
|
-
const wrapper = await
|
|
163
|
+
const wrapper = await mountDecode();
|
|
150
164
|
const label = wrapper.find("label.decode-qr-code-upload");
|
|
151
165
|
expect(label.text()).toContain("Upload a QR code image");
|
|
152
166
|
expect(label.find(".decode-qr-code-capture").exists()).toBe(true);
|
|
153
167
|
});
|
|
154
168
|
|
|
155
169
|
it("uses uploadLabel and dropLabel when given", async () => {
|
|
156
|
-
const wrapper = await
|
|
170
|
+
const wrapper = await mountDecode({
|
|
157
171
|
props: { uploadLabel: "Téléverser", dropLabel: "Déposer ici" },
|
|
158
172
|
});
|
|
159
173
|
expect(wrapper.find(".decode-qr-code-upload-label").text()).toBe("Téléverser");
|
|
160
174
|
expect(wrapper.find(".decode-qr-code-dropzone-label").text()).toBe("Déposer ici");
|
|
161
175
|
});
|
|
162
176
|
|
|
163
|
-
it("
|
|
164
|
-
const wrapper = await
|
|
165
|
-
|
|
177
|
+
it("sets data-dropping on the drop zone only while an image is dragged over it", async () => {
|
|
178
|
+
const wrapper = await mountDecode();
|
|
179
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
180
|
+
expect(wrapper.find(".decode-qr-code-dropzone").attributes("data-dropping")).toBeUndefined();
|
|
181
|
+
vm.onDropping(true);
|
|
182
|
+
await nextTick();
|
|
183
|
+
expect(wrapper.find(".decode-qr-code-dropzone").attributes("data-dropping")).toBe("");
|
|
184
|
+
vm.onDropping(false);
|
|
185
|
+
await nextTick();
|
|
186
|
+
expect(wrapper.find(".decode-qr-code-dropzone").attributes("data-dropping")).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("says no code was found when an image decodes to nothing", async () => {
|
|
190
|
+
const wrapper = await mountDecode({ props: { noResultLabel: "Aucun code" } });
|
|
191
|
+
expect(wrapper.find(".decode-qr-code-status").exists()).toBe(false);
|
|
192
|
+
(wrapper.vm as unknown as DecodeVM).onDetect([]);
|
|
193
|
+
await nextTick();
|
|
194
|
+
expect(wrapper.find(".decode-qr-code-status").text()).toBe("Aucun code");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("says the file couldn't be read when the drop zone reports an error, and clears old results", async () => {
|
|
198
|
+
const wrapper = await mountDecode();
|
|
199
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
200
|
+
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
201
|
+
await nextTick();
|
|
202
|
+
vm.onDropError();
|
|
203
|
+
await nextTick();
|
|
204
|
+
expect(wrapper.find(".decode-qr-code-results ul").exists()).toBe(false);
|
|
205
|
+
expect(wrapper.find(".decode-qr-code-status").text()).toBe("That file couldn't be read as an image.");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("replaces a status message with results on the next successful decode", async () => {
|
|
209
|
+
const wrapper = await mountDecode();
|
|
210
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
211
|
+
vm.onDetect([]);
|
|
212
|
+
await nextTick();
|
|
213
|
+
vm.onDetect([{ rawValue: "found" }]);
|
|
166
214
|
await nextTick();
|
|
167
|
-
expect(wrapper.find(".decode-qr-code-
|
|
215
|
+
expect(wrapper.find(".decode-qr-code-status").exists()).toBe(false);
|
|
216
|
+
expect(wrapper.find(".decode-qr-code-result").text()).toBe("found");
|
|
168
217
|
});
|
|
169
218
|
|
|
170
219
|
it("keeps a polite live region for results", async () => {
|
|
171
|
-
const wrapper = await
|
|
220
|
+
const wrapper = await mountDecode();
|
|
172
221
|
expect(wrapper.find(".decode-qr-code-results").attributes("aria-live")).toBe("polite");
|
|
173
222
|
});
|
|
174
223
|
|
|
175
224
|
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
176
|
-
const wrapper = await
|
|
225
|
+
const wrapper = await mountDecode({ props: { styleClassPassthrough: ["original"] } });
|
|
177
226
|
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
178
227
|
expect(wrapper.classes()).not.toContain("original");
|
|
179
228
|
expect(wrapper.classes()).toContain("updated");
|
|
@@ -14,13 +14,15 @@ class set and one token API, so everything here applies to both.
|
|
|
14
14
|
| `--samaritan-prompt-font-size` | `2rem` | Text size (the underline and gaps don't scale with it) |
|
|
15
15
|
| `--samaritan-prompt-letter-spacing` | `0.08em` | Letter spacing |
|
|
16
16
|
| `--samaritan-prompt-text-transform` | `uppercase` | Text case (`none` keeps messages as written) |
|
|
17
|
-
| `--samaritan-prompt-text-colour` |
|
|
17
|
+
| `--samaritan-prompt-text-colour` | `var(--theme-text)` | Text colour |
|
|
18
|
+
| `--samaritan-prompt-text-white-space` | `normal` | `nowrap` keeps each message on one line (it can then overflow a narrow container) |
|
|
19
|
+
| `--samaritan-prompt-text-line-clamp` | `none` | Lines before an ellipsis. `1` is single-line ellipsis, `none` shows everything |
|
|
18
20
|
|
|
19
21
|
### Underline
|
|
20
22
|
|
|
21
23
|
| Token | Default | Controls |
|
|
22
24
|
|---|---|---|
|
|
23
|
-
| `--samaritan-prompt-underline-colour` |
|
|
25
|
+
| `--samaritan-prompt-underline-colour` | `var(--theme-text)` | Underline colour |
|
|
24
26
|
| `--samaritan-prompt-underline-height` | `0.15rem` | Underline thickness |
|
|
25
27
|
| `--samaritan-prompt-underline-gap` | `0.6rem` | Space between text and underline |
|
|
26
28
|
|
|
@@ -28,7 +30,7 @@ class set and one token API, so everything here applies to both.
|
|
|
28
30
|
|
|
29
31
|
| Token | Default | Controls |
|
|
30
32
|
|---|---|---|
|
|
31
|
-
| `--samaritan-prompt-cursor-colour` |
|
|
33
|
+
| `--samaritan-prompt-cursor-colour` | `var(--theme-accent)` | Cursor colour at the peak of the pulse |
|
|
32
34
|
| `--samaritan-prompt-cursor-colour-off` | `transparent` | Cursor colour at the trough of the pulse |
|
|
33
35
|
| `--samaritan-prompt-cursor-size` | `2.4rem` | Cursor glyph size |
|
|
34
36
|
| `--samaritan-prompt-cursor-pulse-duration` | `2.5s` | One full pulse cycle |
|
|
@@ -37,6 +39,12 @@ class set and one token API, so everything here applies to both.
|
|
|
37
39
|
Private, not public API: `--_fade-duration` (set inline on the root from the `fadeDuration` prop,
|
|
38
40
|
or the current message's `fadeDuration` in `SamaritanPromptMixed`).
|
|
39
41
|
|
|
42
|
+
> **Changed 2026-10-21**: colour defaults follow the theme (`--theme-text` for text and underline,
|
|
43
|
+
> `--theme-accent` for the cursor) instead of assuming a dark page (`#ffffff`, `#cc0000`); set the
|
|
44
|
+
> three colour tokens for the classic white-on-black look. Text now wraps (`overflow-wrap: anywhere`)
|
|
45
|
+
> instead of `nowrap`, so a long message stays inside its container; `--samaritan-prompt-text-white-space`
|
|
46
|
+
> and `--samaritan-prompt-text-line-clamp` are new.
|
|
47
|
+
>
|
|
40
48
|
> **Changed 2026-09-27**: all tokens were renamed from `--samaritan-*` to `--samaritan-prompt-*`,
|
|
41
49
|
> and `color` became `colour` (`--samaritan-color-text` is now `--samaritan-prompt-text-colour`,
|
|
42
50
|
> `--samaritan-color-cursor-off` is now `--samaritan-prompt-cursor-colour-off`, and so on). The
|
|
@@ -69,20 +77,22 @@ Inner element classes, safe to target:
|
|
|
69
77
|
|
|
70
78
|
- The cursor pulses between `--samaritan-prompt-cursor-colour` and `-colour-off`; set both to the
|
|
71
79
|
same value for a steady cursor.
|
|
72
|
-
- `prefers-reduced-motion: reduce` removes the cursor pulse and the word-pulse fade transition
|
|
73
|
-
|
|
80
|
+
- `prefers-reduced-motion: reduce` removes the cursor pulse and the word-pulse fade transition, and
|
|
81
|
+
the typewriter shows each message whole instead of typing and deleting it character by
|
|
82
|
+
character. The message sequence itself keeps running (it is the content), and pauses on hover
|
|
83
|
+
like always.
|
|
74
84
|
|
|
75
85
|
---
|
|
76
86
|
|
|
77
87
|
## Global theming
|
|
78
88
|
|
|
79
|
-
The
|
|
80
|
-
theme tokens once:
|
|
89
|
+
The colours follow the theme by default. For the classic white-on-black look across a site:
|
|
81
90
|
|
|
82
91
|
```css
|
|
83
92
|
:where(html) {
|
|
84
|
-
--samaritan-prompt-text-colour:
|
|
85
|
-
--samaritan-prompt-underline-colour:
|
|
93
|
+
--samaritan-prompt-text-colour: #fff;
|
|
94
|
+
--samaritan-prompt-underline-colour: #fff;
|
|
95
|
+
--samaritan-prompt-cursor-colour: #cc0000;
|
|
86
96
|
}
|
|
87
97
|
```
|
|
88
98
|
|
|
@@ -115,4 +125,6 @@ element. Reactive: changing the prop replaces the previous classes.
|
|
|
115
125
|
> that ancestor. Its styles now apply wherever it's used, and `.samaritan-stage` means nothing
|
|
116
126
|
> to the library.
|
|
117
127
|
|
|
118
|
-
- The `Mono MMM 5` font loads from `/fonts/monoMMM_5.ttf`, shipped in this layer's `public/`.
|
|
128
|
+
- The `Mono MMM 5` font loads from `/fonts/monoMMM_5.ttf`, shipped in this layer's `public/`. Both components
|
|
129
|
+
declare it with `font-display: swap` (until 2026-10-21 `SamaritanPromptMixed` used `optional`, so
|
|
130
|
+
which one won depended on load order).
|