srcdev-nuxt-components 9.1.35 → 9.1.37
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 +1 -24
- package/.claude/settings.local.json +29 -1
- package/.claude/skills/components/capture-qr-code.md +84 -0
- package/.claude/skills/components/decode-qr-code.md +84 -0
- package/.claude/skills/components/display-qr-code.md +64 -0
- package/.claude/skills/index.md +4 -1
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +16 -14
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +4 -4
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +50 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +206 -0
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +139 -0
- package/app/components/{qr-code → 02.molecules/qr-code}/CaptureQrCode.vue +18 -59
- package/app/components/{qr-code → 02.molecules/qr-code}/DecodeQrCode.vue +11 -35
- package/app/components/02.molecules/qr-code/stories/QrCode.stories.ts +101 -0
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +212 -0
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +145 -0
- package/app/components/parallax/SectionParallax.vue +27 -33
- package/app/pages/ui/qr-code/[componentName].vue +3 -3
- package/package.json +1 -1
- package/app/components/qr-code/DisplayQrCode.vue +0 -53
- package/app/components/qr-code/stories/QrCode.stories.ts +0 -933
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import CaptureQrCodeComponent from "../CaptureQrCode.vue";
|
|
3
|
+
import DecodeQrCodeComponent from "../DecodeQrCode.vue";
|
|
4
|
+
|
|
5
|
+
// ===== CAPTURE QR CODE =====
|
|
6
|
+
|
|
7
|
+
const CaptureQrCodeMeta: Meta<typeof CaptureQrCodeComponent> = {
|
|
8
|
+
title: "Molecules/QR Code/CaptureQrCode",
|
|
9
|
+
component: CaptureQrCodeComponent,
|
|
10
|
+
argTypes: {
|
|
11
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
styleClassPassthrough: [],
|
|
15
|
+
},
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
"Interactive QR code scanner using device camera. Requires camera permissions to function properly. Features automatic detection, error handling, and camera management.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const CaptureTemplate: StoryFn<typeof CaptureQrCodeComponent> = (args) => ({
|
|
27
|
+
components: { CaptureQrCodeComponent },
|
|
28
|
+
setup() {
|
|
29
|
+
return { args };
|
|
30
|
+
},
|
|
31
|
+
template: `
|
|
32
|
+
<div style="padding: 40px; max-width: 600px; margin: 0 auto;">
|
|
33
|
+
<div style="background: #f8fafc; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
|
|
34
|
+
<h3 style="margin: 0 0 10px 0; color: #374151;">Live Camera QR Scanner</h3>
|
|
35
|
+
<p style="margin: 0 0 10px 0; color: #6b7280; font-size: 14px;">
|
|
36
|
+
This component uses your device camera to scan QR codes in real-time.
|
|
37
|
+
Make sure to allow camera access when prompted.
|
|
38
|
+
</p>
|
|
39
|
+
<div style="background: #fff3cd; padding: 12px; border-radius: 4px; border: 1px solid #ffeaa7;">
|
|
40
|
+
<p style="margin: 0; color: #856404; font-size: 12px;">
|
|
41
|
+
<strong>Features:</strong> Automatic detection, multiple code scanning, camera management, error recovery
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<CaptureQrCodeComponent :style-class-passthrough="args.styleClassPassthrough" />
|
|
46
|
+
</div>
|
|
47
|
+
`,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const CaptureDefault = CaptureTemplate.bind({});
|
|
51
|
+
export const CaptureDefaultMeta = CaptureQrCodeMeta;
|
|
52
|
+
|
|
53
|
+
// ===== DECODE QR CODE =====
|
|
54
|
+
|
|
55
|
+
const DecodeQrCodeMeta: Meta<typeof DecodeQrCodeComponent> = {
|
|
56
|
+
title: "Molecules/QR Code/DecodeQrCode",
|
|
57
|
+
component: DecodeQrCodeComponent,
|
|
58
|
+
argTypes: {
|
|
59
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
60
|
+
},
|
|
61
|
+
args: {
|
|
62
|
+
styleClassPassthrough: [],
|
|
63
|
+
},
|
|
64
|
+
parameters: {
|
|
65
|
+
docs: {
|
|
66
|
+
description: {
|
|
67
|
+
component:
|
|
68
|
+
"Upload or drag-and-drop QR code images to decode their content. Supports various image formats including PNG, JPEG, WEBP, and more.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DecodeTemplate: StoryFn<typeof DecodeQrCodeComponent> = (args) => ({
|
|
75
|
+
components: { DecodeQrCodeComponent },
|
|
76
|
+
setup() {
|
|
77
|
+
return { args };
|
|
78
|
+
},
|
|
79
|
+
template: `
|
|
80
|
+
<div style="padding: 40px; max-width: 600px; margin: 0 auto;">
|
|
81
|
+
<div style="background: #f8fafc; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
|
|
82
|
+
<h3 style="margin: 0 0 10px 0; color: #374151;">QR Image Decoder</h3>
|
|
83
|
+
<p style="margin: 0 0 10px 0; color: #6b7280; font-size: 14px;">
|
|
84
|
+
Upload or drag QR code images to extract their content.
|
|
85
|
+
Supports PNG, JPEG, WEBP, and other common image formats.
|
|
86
|
+
</p>
|
|
87
|
+
<div style="background: #d1ecf1; padding: 12px; border-radius: 4px; border: 1px solid #bee5eb;">
|
|
88
|
+
<p style="margin: 0; color: #0c5460; font-size: 12px;">
|
|
89
|
+
<strong>Supported formats:</strong> PNG, JPEG, WEBP, BMP, GIF • <strong>Methods:</strong> File upload, drag & drop
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<DecodeQrCodeComponent :style-class-passthrough="args.styleClassPassthrough" />
|
|
94
|
+
</div>
|
|
95
|
+
`,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const DecodeDefault = DecodeTemplate.bind({});
|
|
99
|
+
export const DecodeDefaultMeta = DecodeQrCodeMeta;
|
|
100
|
+
|
|
101
|
+
export default CaptureQrCodeMeta;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
import CaptureQrCode from "../CaptureQrCode.vue";
|
|
5
|
+
|
|
6
|
+
interface CaptureVM {
|
|
7
|
+
state: { error: boolean; errorMsg: string; cameraOn: boolean };
|
|
8
|
+
result: string[] | undefined;
|
|
9
|
+
onDetect: (codes: { rawValue: string }[]) => void;
|
|
10
|
+
onError: (err: Error) => void;
|
|
11
|
+
resetCamera: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
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 any);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
it("mounts without error", async () => {
|
|
29
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
30
|
+
expect(wrapper.vm).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
it("renders as a div", async () => {
|
|
36
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
37
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("always has the capture-qr-stream class", async () => {
|
|
41
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
42
|
+
expect(wrapper.classes()).toContain("capture-qr-stream");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ─── Initial state ────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
it("has camera on by default", async () => {
|
|
48
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
49
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
50
|
+
expect(vm.state.cameraOn).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not show camera-stopped on mount", async () => {
|
|
54
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
55
|
+
expect(wrapper.find(".camera-stopped").exists()).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does not show scanned results initially", async () => {
|
|
59
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
60
|
+
expect(wrapper.find(".scanned-results").exists()).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not show the error state initially", async () => {
|
|
64
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
65
|
+
expect(wrapper.find(".camera-error").exists()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ─── Camera off ───────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
it("hides the stream and shows camera-stopped when cameraOn is false", async () => {
|
|
71
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
72
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
73
|
+
vm.state.cameraOn = false;
|
|
74
|
+
await nextTick();
|
|
75
|
+
expect(vm.state.cameraOn).toBe(false);
|
|
76
|
+
expect(wrapper.find(".camera-stopped").exists()).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ─── Error state ──────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
it("shows camera-error when onError is called", async () => {
|
|
82
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
83
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
84
|
+
vm.onError(new Error("Permission denied"));
|
|
85
|
+
await nextTick();
|
|
86
|
+
expect(wrapper.find(".camera-error").exists()).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("shows the error message in the error state", async () => {
|
|
90
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
91
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
92
|
+
vm.onError(new Error("Permission denied"));
|
|
93
|
+
await nextTick();
|
|
94
|
+
expect(wrapper.find(".camera-error p").text()).toContain("Permission denied");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("sets state.error when onError is called, hiding the stream", async () => {
|
|
98
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
99
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
100
|
+
vm.onError(new Error("test"));
|
|
101
|
+
await nextTick();
|
|
102
|
+
expect(vm.state.error).toBe(true);
|
|
103
|
+
expect(wrapper.find(".camera-error").exists()).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("shows a reset button in the error state", async () => {
|
|
107
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
108
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
109
|
+
vm.onError(new Error("test"));
|
|
110
|
+
await nextTick();
|
|
111
|
+
expect(wrapper.find(".camera-error button").exists()).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ─── Reset camera ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
it("clears the error state when resetCamera is called", async () => {
|
|
117
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
118
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
119
|
+
vm.onError(new Error("test"));
|
|
120
|
+
await nextTick();
|
|
121
|
+
vm.resetCamera();
|
|
122
|
+
await nextTick();
|
|
123
|
+
expect(wrapper.find(".camera-error").exists()).toBe(false);
|
|
124
|
+
expect(vm.state.cameraOn).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("clears the error state when the reset button is clicked", async () => {
|
|
128
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
129
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
130
|
+
vm.onError(new Error("test"));
|
|
131
|
+
await nextTick();
|
|
132
|
+
await wrapper.find(".camera-error button").trigger("click");
|
|
133
|
+
expect(wrapper.find(".camera-error").exists()).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── Detect ───────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
it("shows scanned results after onDetect is called", async () => {
|
|
139
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
140
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
141
|
+
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
142
|
+
await nextTick();
|
|
143
|
+
expect(wrapper.find(".scanned-results").exists()).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("shows the detected value in the results list", async () => {
|
|
147
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
148
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
149
|
+
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
150
|
+
await nextTick();
|
|
151
|
+
expect(wrapper.find(".scanned-results").text()).toContain("https://example.com");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("shows all detected values when multiple codes are scanned", async () => {
|
|
155
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
156
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
157
|
+
vm.onDetect([{ rawValue: "https://one.com" }, { rawValue: "https://two.com" }]);
|
|
158
|
+
await nextTick();
|
|
159
|
+
const items = wrapper.findAll(".scanned-results li");
|
|
160
|
+
expect(items).toHaveLength(2);
|
|
161
|
+
expect(items[0]!.text()).toBe("https://one.com");
|
|
162
|
+
expect(items[1]!.text()).toBe("https://two.com");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("replaces results on subsequent scans", async () => {
|
|
166
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
167
|
+
const vm = wrapper.vm as unknown as CaptureVM;
|
|
168
|
+
vm.onDetect([{ rawValue: "first" }]);
|
|
169
|
+
await nextTick();
|
|
170
|
+
vm.onDetect([{ rawValue: "second" }]);
|
|
171
|
+
await nextTick();
|
|
172
|
+
const items = wrapper.findAll(".scanned-results li");
|
|
173
|
+
expect(items).toHaveLength(1);
|
|
174
|
+
expect(items[0]!.text()).toBe("second");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ─── Visibility change ────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
it("stops the camera when the page becomes hidden", async () => {
|
|
180
|
+
const wrapper = await mountSuspended(CaptureQrCode);
|
|
181
|
+
Object.defineProperty(document, "hidden", { get: () => true, configurable: true });
|
|
182
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
183
|
+
await nextTick();
|
|
184
|
+
expect(wrapper.find("qrcodestream").exists()).toBe(false);
|
|
185
|
+
Object.defineProperty(document, "hidden", { get: () => false, configurable: true });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
191
|
+
const wrapper = await mountSuspended(CaptureQrCode, {
|
|
192
|
+
props: { styleClassPassthrough: "custom-class" },
|
|
193
|
+
});
|
|
194
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
198
|
+
const wrapper = await mountSuspended(CaptureQrCode, {
|
|
199
|
+
props: { styleClassPassthrough: ["class-a", "class-b"] },
|
|
200
|
+
});
|
|
201
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
202
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("retains capture-qr-stream class alongside styleClassPassthrough", async () => {
|
|
206
|
+
const wrapper = await mountSuspended(CaptureQrCode, {
|
|
207
|
+
props: { styleClassPassthrough: "extra" },
|
|
208
|
+
});
|
|
209
|
+
expect(wrapper.classes()).toContain("capture-qr-stream");
|
|
210
|
+
expect(wrapper.classes()).toContain("extra");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
import DecodeQrCode from "../DecodeQrCode.vue";
|
|
5
|
+
|
|
6
|
+
interface DecodeVM {
|
|
7
|
+
result: string[] | undefined;
|
|
8
|
+
isDropping: boolean;
|
|
9
|
+
onDetect: (codes: { rawValue: string }[]) => void;
|
|
10
|
+
onDropping: (dropping: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("DecodeQrCode", () => {
|
|
14
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
it("mounts without error", async () => {
|
|
17
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
18
|
+
expect(wrapper.vm).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
it("renders as a div", async () => {
|
|
24
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
25
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("always has the decode-qr-code class", async () => {
|
|
29
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
30
|
+
expect(wrapper.classes()).toContain("decode-qr-code");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ─── Initial DOM structure ─────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
it("renders the file capture element", async () => {
|
|
36
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
37
|
+
expect(wrapper.find(".qr-code-capture").exists()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders the drop zone element", async () => {
|
|
41
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
42
|
+
expect(wrapper.find(".qr-code-dropzone").exists()).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not show scanned results initially", async () => {
|
|
46
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
47
|
+
expect(wrapper.find(".scanned-results").exists()).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── Detect ───────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
it("shows scanned results after onDetect is called", async () => {
|
|
53
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
54
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
55
|
+
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
56
|
+
await nextTick();
|
|
57
|
+
expect(wrapper.find(".scanned-results").exists()).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("shows the detected value in the results list", async () => {
|
|
61
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
62
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
63
|
+
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
64
|
+
await nextTick();
|
|
65
|
+
expect(wrapper.find(".scanned-results").text()).toContain("https://example.com");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("shows all detected values when multiple codes are decoded", async () => {
|
|
69
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
70
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
71
|
+
vm.onDetect([{ rawValue: "https://one.com" }, { rawValue: "https://two.com" }]);
|
|
72
|
+
await nextTick();
|
|
73
|
+
const items = wrapper.findAll(".scanned-results li");
|
|
74
|
+
expect(items).toHaveLength(2);
|
|
75
|
+
expect(items[0]!.text()).toBe("https://one.com");
|
|
76
|
+
expect(items[1]!.text()).toBe("https://two.com");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("replaces results on subsequent detections", async () => {
|
|
80
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
81
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
82
|
+
vm.onDetect([{ rawValue: "first" }]);
|
|
83
|
+
await nextTick();
|
|
84
|
+
vm.onDetect([{ rawValue: "second" }]);
|
|
85
|
+
await nextTick();
|
|
86
|
+
const items = wrapper.findAll(".scanned-results li");
|
|
87
|
+
expect(items).toHaveLength(1);
|
|
88
|
+
expect(items[0]!.text()).toBe("second");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("hides results when detecting an empty array", async () => {
|
|
92
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
93
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
94
|
+
vm.onDetect([{ rawValue: "https://example.com" }]);
|
|
95
|
+
await nextTick();
|
|
96
|
+
vm.onDetect([]);
|
|
97
|
+
await nextTick();
|
|
98
|
+
expect(wrapper.find(".scanned-results").exists()).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── Dropping state ───────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
it("sets isDropping to true when onDropping is called with true", async () => {
|
|
104
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
105
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
106
|
+
vm.onDropping(true);
|
|
107
|
+
await nextTick();
|
|
108
|
+
expect(vm.isDropping).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("sets isDropping to false when onDropping is called with false", async () => {
|
|
112
|
+
const wrapper = await mountSuspended(DecodeQrCode);
|
|
113
|
+
const vm = wrapper.vm as unknown as DecodeVM;
|
|
114
|
+
vm.onDropping(true);
|
|
115
|
+
await nextTick();
|
|
116
|
+
vm.onDropping(false);
|
|
117
|
+
await nextTick();
|
|
118
|
+
expect(vm.isDropping).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
124
|
+
const wrapper = await mountSuspended(DecodeQrCode, {
|
|
125
|
+
props: { styleClassPassthrough: "custom-class" },
|
|
126
|
+
});
|
|
127
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
131
|
+
const wrapper = await mountSuspended(DecodeQrCode, {
|
|
132
|
+
props: { styleClassPassthrough: ["class-a", "class-b"] },
|
|
133
|
+
});
|
|
134
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
135
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("retains decode-qr-code class alongside styleClassPassthrough", async () => {
|
|
139
|
+
const wrapper = await mountSuspended(DecodeQrCode, {
|
|
140
|
+
props: { styleClassPassthrough: "extra" },
|
|
141
|
+
});
|
|
142
|
+
expect(wrapper.classes()).toContain("decode-qr-code");
|
|
143
|
+
expect(wrapper.classes()).toContain("extra");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -5,21 +5,15 @@
|
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script setup lang="ts">
|
|
8
|
-
|
|
9
|
-
tag
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type: String,
|
|
18
|
-
},
|
|
19
|
-
styleClassPassthrough: {
|
|
20
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
21
|
-
default: () => [],
|
|
22
|
-
},
|
|
8
|
+
interface Props {
|
|
9
|
+
tag?: "div" | "section" | "article" | "aside";
|
|
10
|
+
backgroundImage: string;
|
|
11
|
+
styleClassPassthrough?: string | string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
15
|
+
tag: "div",
|
|
16
|
+
styleClassPassthrough: () => [],
|
|
23
17
|
});
|
|
24
18
|
|
|
25
19
|
const slots = useSlots();
|
|
@@ -38,25 +32,25 @@ watch(
|
|
|
38
32
|
|
|
39
33
|
<style lang="css">
|
|
40
34
|
@layer components {
|
|
41
|
-
.section-parallax {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
.section-parallax {
|
|
36
|
+
/* Component styles */
|
|
37
|
+
|
|
38
|
+
min-height: 100svh;
|
|
39
|
+
background-image: v-bind(backgroundImage);
|
|
40
|
+
background-position: center;
|
|
41
|
+
background-repeat: no-repeat;
|
|
42
|
+
background-size: cover;
|
|
43
|
+
position: relative;
|
|
44
|
+
|
|
45
|
+
background-color: light-dark(var(--slate-01), var(--slate-08));
|
|
46
|
+
width: 100%;
|
|
47
|
+
|
|
48
|
+
@media (hover: hover) and (pointer: fine) {
|
|
49
|
+
@supports (background-attachment: fixed) {
|
|
50
|
+
background-attachment: fixed;
|
|
51
|
+
min-height: 120vh;
|
|
52
|
+
}
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
|
-
}
|
|
62
56
|
</style>
|
|
@@ -121,9 +121,9 @@ type QrComponentName = "decode" | "capture" | "display";
|
|
|
121
121
|
const componentName = computed<QrComponentName>(() => route.params.componentName as QrComponentName);
|
|
122
122
|
|
|
123
123
|
// Component set up
|
|
124
|
-
const DecodeQrCode = defineAsyncComponent(() => import("~/components/qr-code/DecodeQrCode.vue"));
|
|
125
|
-
const CaptureQrCode = defineAsyncComponent(() => import("~/components/qr-code/CaptureQrCode.vue"));
|
|
126
|
-
const DisplayQrCode = defineAsyncComponent(() => import("~/components/qr-code/DisplayQrCode.vue"));
|
|
124
|
+
const DecodeQrCode = defineAsyncComponent(() => import("~/components/02.molecules/qr-code/DecodeQrCode.vue"));
|
|
125
|
+
const CaptureQrCode = defineAsyncComponent(() => import("~/components/02.molecules/qr-code/CaptureQrCode.vue"));
|
|
126
|
+
const DisplayQrCode = defineAsyncComponent(() => import("~/components/01.atoms/qr-code/DisplayQrCode.vue"));
|
|
127
127
|
|
|
128
128
|
const components: Record<
|
|
129
129
|
QrComponentName,
|
package/package.json
CHANGED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Qrcode :value="qrValue" class="display-qr-code" :variant :radius :blackColor :whiteColor :class="[elementClasses]" />
|
|
3
|
-
</template>
|
|
4
|
-
|
|
5
|
-
<script setup lang="ts">
|
|
6
|
-
import type { QrCodeVariant } from "../../types/components";
|
|
7
|
-
|
|
8
|
-
const props = defineProps({
|
|
9
|
-
qrValue: {
|
|
10
|
-
type: String,
|
|
11
|
-
required: true,
|
|
12
|
-
},
|
|
13
|
-
variant: {
|
|
14
|
-
type: Object as PropType<QrCodeVariant>,
|
|
15
|
-
default: () => ({
|
|
16
|
-
inner: "default",
|
|
17
|
-
marker: "default",
|
|
18
|
-
pixel: "default",
|
|
19
|
-
}),
|
|
20
|
-
},
|
|
21
|
-
radius: {
|
|
22
|
-
type: Number,
|
|
23
|
-
default: 0,
|
|
24
|
-
},
|
|
25
|
-
blackColor: {
|
|
26
|
-
type: String,
|
|
27
|
-
default: "currentColor",
|
|
28
|
-
},
|
|
29
|
-
whiteColor: {
|
|
30
|
-
type: String,
|
|
31
|
-
default: "transparent",
|
|
32
|
-
},
|
|
33
|
-
size: {
|
|
34
|
-
type: String,
|
|
35
|
-
default: "256px",
|
|
36
|
-
},
|
|
37
|
-
styleClassPassthrough: {
|
|
38
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
39
|
-
default: () => [],
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
44
|
-
</script>
|
|
45
|
-
|
|
46
|
-
<style lang="css">
|
|
47
|
-
@layer components {
|
|
48
|
-
.display-qr-code {
|
|
49
|
-
aspect-ratio: 1 / 1;
|
|
50
|
-
width: v-bind(size);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
</style>
|