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,206 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import DisplayQrCodeComponent from "../DisplayQrCode.vue";
|
|
3
|
+
import type { QrCodeVariant } from "../../../../types/components";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DisplayQrCodeComponent> = {
|
|
6
|
+
title: "Atoms/QR Code/DisplayQrCode",
|
|
7
|
+
component: DisplayQrCodeComponent,
|
|
8
|
+
argTypes: {
|
|
9
|
+
qrValue: {
|
|
10
|
+
control: { type: "text" },
|
|
11
|
+
description: "The value/content to encode in the QR code",
|
|
12
|
+
table: { category: "Content" },
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
control: { type: "text" },
|
|
16
|
+
description: "Size of the QR code (CSS value like '256px', '10rem')",
|
|
17
|
+
table: { category: "Appearance" },
|
|
18
|
+
},
|
|
19
|
+
radius: {
|
|
20
|
+
control: { type: "range", min: 0, max: 20, step: 1 },
|
|
21
|
+
description: "Border radius of the QR code corners",
|
|
22
|
+
table: { category: "Appearance" },
|
|
23
|
+
},
|
|
24
|
+
blackColor: {
|
|
25
|
+
control: { type: "color" },
|
|
26
|
+
description: "Color of the QR code foreground elements",
|
|
27
|
+
table: { category: "Appearance" },
|
|
28
|
+
},
|
|
29
|
+
whiteColor: {
|
|
30
|
+
control: { type: "color" },
|
|
31
|
+
description: "Color of the QR code background",
|
|
32
|
+
table: { category: "Appearance" },
|
|
33
|
+
},
|
|
34
|
+
variant: { table: { disable: true } },
|
|
35
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
qrValue: "https://github.com/srcdev/nuxt-components",
|
|
39
|
+
size: "256px",
|
|
40
|
+
radius: 0,
|
|
41
|
+
blackColor: "#000000",
|
|
42
|
+
whiteColor: "transparent",
|
|
43
|
+
styleClassPassthrough: [],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
|
|
49
|
+
interface QrStoryArgs {
|
|
50
|
+
qrValue: string;
|
|
51
|
+
size: string;
|
|
52
|
+
radius: number;
|
|
53
|
+
blackColor: string;
|
|
54
|
+
whiteColor: string;
|
|
55
|
+
styleClassPassthrough: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DisplayTemplate: StoryFn<QrStoryArgs> = (args, { parameters }) => ({
|
|
59
|
+
components: { DisplayQrCodeComponent },
|
|
60
|
+
setup() {
|
|
61
|
+
const variant: QrCodeVariant = parameters?.variant || {
|
|
62
|
+
inner: "default",
|
|
63
|
+
marker: "default",
|
|
64
|
+
pixel: "default",
|
|
65
|
+
};
|
|
66
|
+
return { args, variant };
|
|
67
|
+
},
|
|
68
|
+
template: `
|
|
69
|
+
<div style="padding: 40px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border-radius: 8px;">
|
|
70
|
+
<DisplayQrCodeComponent
|
|
71
|
+
:qrValue="args.qrValue"
|
|
72
|
+
:variant="variant"
|
|
73
|
+
:radius="args.radius"
|
|
74
|
+
:blackColor="args.blackColor"
|
|
75
|
+
:whiteColor="args.whiteColor"
|
|
76
|
+
:size="args.size"
|
|
77
|
+
:style-class-passthrough="args.styleClassPassthrough"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const Default = DisplayTemplate.bind({});
|
|
84
|
+
|
|
85
|
+
export const Website = DisplayTemplate.bind({});
|
|
86
|
+
Website.args = { qrValue: "https://nuxt.com", radius: 8, blackColor: "#000000" };
|
|
87
|
+
Website.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
88
|
+
|
|
89
|
+
export const VariantDefault = DisplayTemplate.bind({});
|
|
90
|
+
VariantDefault.args = { qrValue: "Default variant style", radius: 4 };
|
|
91
|
+
VariantDefault.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
|
|
92
|
+
|
|
93
|
+
export const VariantCircle = DisplayTemplate.bind({});
|
|
94
|
+
VariantCircle.args = { qrValue: "Circle variant style", radius: 8, blackColor: "#1e40af" };
|
|
95
|
+
VariantCircle.parameters = { variant: { inner: "circle", marker: "circle", pixel: "circle" } };
|
|
96
|
+
|
|
97
|
+
export const VariantRounded = DisplayTemplate.bind({});
|
|
98
|
+
VariantRounded.args = { qrValue: "Rounded variant style", radius: 12, blackColor: "#059669" };
|
|
99
|
+
VariantRounded.parameters = { variant: { inner: "rounded", marker: "rounded", pixel: "rounded" } };
|
|
100
|
+
|
|
101
|
+
export const VariantDots = DisplayTemplate.bind({});
|
|
102
|
+
VariantDots.args = { qrValue: "Dots variant style", radius: 6, blackColor: "#dc2626" };
|
|
103
|
+
VariantDots.parameters = { variant: { inner: "dots", marker: "dots", pixel: "dots" } };
|
|
104
|
+
|
|
105
|
+
export const VariantMixed = DisplayTemplate.bind({});
|
|
106
|
+
VariantMixed.args = { qrValue: "Mixed variants for creative styling", radius: 10, blackColor: "#7c3aed" };
|
|
107
|
+
VariantMixed.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
108
|
+
|
|
109
|
+
export const WiFiConnection = DisplayTemplate.bind({});
|
|
110
|
+
WiFiConnection.args = { qrValue: "WIFI:T:WPA;S:MyHomeNetwork;P:mySecurePassword123;H:false;", blackColor: "#1e40af", radius: 4 };
|
|
111
|
+
WiFiConnection.parameters = { variant: { inner: "rounded", marker: "circle", pixel: "default" } };
|
|
112
|
+
|
|
113
|
+
export const VCardContact = DisplayTemplate.bind({});
|
|
114
|
+
VCardContact.args = {
|
|
115
|
+
qrValue: "BEGIN:VCARD\\nVERSION:3.0\\nFN:John Doe\\nORG:Acme Corp\\nTITLE:Software Engineer\\nTEL:+1234567890\\nEMAIL:john.doe@acme.com\\nURL:https://johndoe.dev\\nEND:VCARD",
|
|
116
|
+
blackColor: "#059669",
|
|
117
|
+
radius: 12,
|
|
118
|
+
};
|
|
119
|
+
VCardContact.parameters = { variant: { inner: "dots", marker: "rounded", pixel: "circle" } };
|
|
120
|
+
|
|
121
|
+
export const SmallSize = DisplayTemplate.bind({});
|
|
122
|
+
SmallSize.args = { qrValue: "Small QR for tight spaces", size: "128px", radius: 2, blackColor: "#f59e0b" };
|
|
123
|
+
SmallSize.parameters = { variant: { inner: "dots", marker: "circle", pixel: "rounded" } };
|
|
124
|
+
|
|
125
|
+
export const LargeSize = DisplayTemplate.bind({});
|
|
126
|
+
LargeSize.args = { qrValue: "Large QR for posters and signage", size: "400px", radius: 16, blackColor: "#0ea5e9" };
|
|
127
|
+
LargeSize.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
128
|
+
|
|
129
|
+
export const HighContrast = DisplayTemplate.bind({});
|
|
130
|
+
HighContrast.args = { qrValue: "High contrast for accessibility", blackColor: "#000000", whiteColor: "#ffffff", radius: 0, size: "256px" };
|
|
131
|
+
HighContrast.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
|
|
132
|
+
|
|
133
|
+
const PlaygroundTemplate: StoryFn<QrStoryArgs> = (args) => ({
|
|
134
|
+
components: { DisplayQrCodeComponent },
|
|
135
|
+
setup() {
|
|
136
|
+
const qrValue = ref(args.qrValue);
|
|
137
|
+
const variant = ref<QrCodeVariant>({ inner: "circle", marker: "rounded", pixel: "dots" });
|
|
138
|
+
const radius = ref(args.radius);
|
|
139
|
+
const blackColor = ref(args.blackColor);
|
|
140
|
+
const whiteColor = ref(args.whiteColor);
|
|
141
|
+
const sizeValue = ref(256);
|
|
142
|
+
const size = computed(() => `${sizeValue.value}px`);
|
|
143
|
+
const presetValues = [
|
|
144
|
+
{ label: "Website URL", value: "https://nuxt.com" },
|
|
145
|
+
{ label: "WiFi Network", value: "WIFI:T:WPA;S:MyNetwork;P:password123;" },
|
|
146
|
+
{ label: "Phone Number", value: "tel:+1234567890" },
|
|
147
|
+
{ label: "Email", value: "mailto:hello@example.com" },
|
|
148
|
+
{ label: "SMS", value: "sms:+1234567890?body=Hello!" },
|
|
149
|
+
{ label: "Location", value: "geo:40.7128,-74.0060" },
|
|
150
|
+
];
|
|
151
|
+
const variantOptions = ["default", "circle", "rounded", "dots"];
|
|
152
|
+
return { qrValue, variant, radius, blackColor, whiteColor, sizeValue, size, presetValues, variantOptions };
|
|
153
|
+
},
|
|
154
|
+
template: `
|
|
155
|
+
<div style="padding: 40px;">
|
|
156
|
+
<div style="display: grid; grid-template-columns: 1fr 300px; gap: 40px; align-items: start;">
|
|
157
|
+
<div style="display: flex; align-items: center; justify-content: center; min-height: 400px; background: #f8fafc; border-radius: 8px; padding: 40px;">
|
|
158
|
+
<DisplayQrCodeComponent :qrValue="qrValue" :variant="variant" :radius="radius" :blackColor="blackColor" :whiteColor="whiteColor" :size="size" />
|
|
159
|
+
</div>
|
|
160
|
+
<div style="background: white; padding: 24px; border-radius: 8px; border: 1px solid #e5e7eb;">
|
|
161
|
+
<h3 style="margin: 0 0 20px 0; color: #374151; font-size: 18px;">QR Code Playground</h3>
|
|
162
|
+
<div style="margin-bottom: 20px;">
|
|
163
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Quick Presets:</label>
|
|
164
|
+
<select @change="qrValue = $event.target.value" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
|
|
165
|
+
<option value="">Select a preset...</option>
|
|
166
|
+
<option v-for="preset in presetValues" :key="preset.label" :value="preset.value">{{ preset.label }}</option>
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
<div style="margin-bottom: 20px;">
|
|
170
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Custom Content:</label>
|
|
171
|
+
<textarea v-model="qrValue" rows="3" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-family: monospace; font-size: 12px;" placeholder="Enter QR code content..." />
|
|
172
|
+
</div>
|
|
173
|
+
<div style="margin-bottom: 20px;">
|
|
174
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Style Variants:</label>
|
|
175
|
+
<div style="display: grid; gap: 8px;">
|
|
176
|
+
<div><label style="font-size: 12px; color: #6b7280;">Inner:</label><select v-model="variant.inner" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
177
|
+
<div><label style="font-size: 12px; color: #6b7280;">Marker:</label><select v-model="variant.marker" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
178
|
+
<div><label style="font-size: 12px; color: #6b7280;">Pixel:</label><select v-model="variant.pixel" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div style="margin-bottom: 20px;">
|
|
182
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Appearance:</label>
|
|
183
|
+
<div style="display: grid; gap: 12px;">
|
|
184
|
+
<div><label style="font-size: 12px; color: #6b7280;">Size: {{ sizeValue }}px</label><input v-model.number="sizeValue" type="range" min="128" max="400" step="16" style="width: 100%;" /></div>
|
|
185
|
+
<div><label style="font-size: 12px; color: #6b7280;">Radius: {{ radius }}px</label><input v-model.number="radius" type="range" min="0" max="20" step="1" style="width: 100%;" /></div>
|
|
186
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
187
|
+
<div><label style="font-size: 12px; color: #6b7280;">Foreground:</label><input v-model="blackColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
188
|
+
<div><label style="font-size: 12px; color: #6b7280;">Background:</label><input v-model="whiteColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export const InteractivePlayground = PlaygroundTemplate.bind({});
|
|
199
|
+
InteractivePlayground.args = {
|
|
200
|
+
qrValue: "https://github.com/srcdev/nuxt-components",
|
|
201
|
+
radius: 8,
|
|
202
|
+
blackColor: "#000000",
|
|
203
|
+
whiteColor: "transparent",
|
|
204
|
+
size: "256px",
|
|
205
|
+
styleClassPassthrough: [],
|
|
206
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DisplayQrCode from "../DisplayQrCode.vue";
|
|
4
|
+
import type { QrCodeVariant } from "~/types/components";
|
|
5
|
+
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
qrValue: "https://example.com",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("DisplayQrCode", () => {
|
|
11
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
it("mounts without error", async () => {
|
|
14
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
15
|
+
expect(wrapper.vm).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
it("renders a single root element", async () => {
|
|
21
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
22
|
+
expect(wrapper.element).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("always has the display-qr-code class", async () => {
|
|
26
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
27
|
+
expect(wrapper.classes()).toContain("display-qr-code");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ─── Default props ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
it("defaults radius to 0", async () => {
|
|
33
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
34
|
+
interface VM { radius: number }
|
|
35
|
+
expect((wrapper.vm as unknown as VM).radius).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("defaults blackColor to currentColor", async () => {
|
|
39
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
40
|
+
interface VM { blackColor: string }
|
|
41
|
+
expect((wrapper.vm as unknown as VM).blackColor).toBe("currentColor");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("defaults whiteColor to transparent", async () => {
|
|
45
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
46
|
+
interface VM { whiteColor: string }
|
|
47
|
+
expect((wrapper.vm as unknown as VM).whiteColor).toBe("transparent");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("defaults size to 256px", async () => {
|
|
51
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
52
|
+
interface VM { size: string }
|
|
53
|
+
expect((wrapper.vm as unknown as VM).size).toBe("256px");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("defaults variant to all-default", async () => {
|
|
57
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
58
|
+
interface VM { variant: QrCodeVariant }
|
|
59
|
+
expect((wrapper.vm as unknown as VM).variant).toEqual({ inner: "default", marker: "default", pixel: "default" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── Custom props ─────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
it("reflects a custom radius prop", async () => {
|
|
65
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
66
|
+
props: { ...defaultProps, radius: 12 },
|
|
67
|
+
});
|
|
68
|
+
interface VM { radius: number }
|
|
69
|
+
expect((wrapper.vm as unknown as VM).radius).toBe(12);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("reflects a custom blackColor prop", async () => {
|
|
73
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
74
|
+
props: { ...defaultProps, blackColor: "#ff0000" },
|
|
75
|
+
});
|
|
76
|
+
interface VM { blackColor: string }
|
|
77
|
+
expect((wrapper.vm as unknown as VM).blackColor).toBe("#ff0000");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("reflects a custom whiteColor prop", async () => {
|
|
81
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
82
|
+
props: { ...defaultProps, whiteColor: "#ffffff" },
|
|
83
|
+
});
|
|
84
|
+
interface VM { whiteColor: string }
|
|
85
|
+
expect((wrapper.vm as unknown as VM).whiteColor).toBe("#ffffff");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reflects a custom size prop", async () => {
|
|
89
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
90
|
+
props: { ...defaultProps, size: "128px" },
|
|
91
|
+
});
|
|
92
|
+
interface VM { size: string }
|
|
93
|
+
expect((wrapper.vm as unknown as VM).size).toBe("128px");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("reflects a custom variant prop", async () => {
|
|
97
|
+
const variant: QrCodeVariant = { inner: "circle", marker: "rounded", pixel: "dots" };
|
|
98
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
99
|
+
props: { ...defaultProps, variant },
|
|
100
|
+
});
|
|
101
|
+
interface VM { variant: QrCodeVariant }
|
|
102
|
+
expect((wrapper.vm as unknown as VM).variant).toEqual(variant);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
108
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
109
|
+
props: { ...defaultProps, styleClassPassthrough: "custom-class" },
|
|
110
|
+
});
|
|
111
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
115
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
116
|
+
props: { ...defaultProps, styleClassPassthrough: ["class-a", "class-b"] },
|
|
117
|
+
});
|
|
118
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
119
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("retains display-qr-code class alongside styleClassPassthrough", async () => {
|
|
123
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
124
|
+
props: { ...defaultProps, styleClassPassthrough: "extra" },
|
|
125
|
+
});
|
|
126
|
+
expect(wrapper.classes()).toContain("display-qr-code");
|
|
127
|
+
expect(wrapper.classes()).toContain("extra");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
131
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
132
|
+
props: { ...defaultProps, styleClassPassthrough: ["original"] },
|
|
133
|
+
});
|
|
134
|
+
expect(wrapper.classes()).toContain("original");
|
|
135
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
136
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
137
|
+
expect(wrapper.classes()).toContain("updated");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="capture-qr-stream" :class="[elementClasses]">
|
|
3
|
-
<h2>Capture QR Code</h2>
|
|
4
3
|
<div v-if="!state.error">
|
|
5
4
|
<QrcodeStream v-if="state.cameraOn" ref="qrcodeStreamRef" @error="onError" @detect="onDetect" />
|
|
6
5
|
<div v-else class="camera-stopped">
|
|
7
6
|
<p>Camera stopped</p>
|
|
8
7
|
</div>
|
|
9
|
-
<div class="
|
|
10
|
-
<
|
|
11
|
-
<ul v-if="result" class="list-disc pl-4">
|
|
8
|
+
<div v-if="result?.length" class="scanned-results">
|
|
9
|
+
<ul>
|
|
12
10
|
<li v-for="(r, i) in result" :key="i">
|
|
13
|
-
<span
|
|
14
|
-
{{ r }}
|
|
15
|
-
</span>
|
|
11
|
+
<span>{{ r }}</span>
|
|
16
12
|
</li>
|
|
17
13
|
</ul>
|
|
18
14
|
</div>
|
|
19
15
|
</div>
|
|
20
|
-
<div v-else>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
</h3>
|
|
24
|
-
<button @click="resetCamera">reset</button>
|
|
16
|
+
<div v-else class="camera-error">
|
|
17
|
+
<p>{{ state.errorMsg }}</p>
|
|
18
|
+
<button @click="resetCamera">Reset camera</button>
|
|
25
19
|
</div>
|
|
26
20
|
</div>
|
|
27
21
|
</template>
|
|
@@ -29,12 +23,13 @@
|
|
|
29
23
|
<script setup lang="ts">
|
|
30
24
|
import type { DetectedBarcode } from "nuxt-qrcode"
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
styleClassPassthrough
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
interface Props {
|
|
27
|
+
styleClassPassthrough?: string | string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
31
|
+
styleClassPassthrough: () => [],
|
|
32
|
+
});
|
|
38
33
|
|
|
39
34
|
const qrcodeStreamRef = ref()
|
|
40
35
|
const result = ref<string[]>()
|
|
@@ -44,9 +39,7 @@ const state = reactive({
|
|
|
44
39
|
cameraOn: true,
|
|
45
40
|
})
|
|
46
41
|
|
|
47
|
-
// Reset camera state when component mounts
|
|
48
42
|
onMounted(() => {
|
|
49
|
-
// Reset to default state on mount
|
|
50
43
|
state.cameraOn = true
|
|
51
44
|
state.error = false
|
|
52
45
|
state.errorMsg = ""
|
|
@@ -61,28 +54,13 @@ onMounted(() => {
|
|
|
61
54
|
|
|
62
55
|
document.addEventListener("visibilitychange", handleVisibilityChange)
|
|
63
56
|
|
|
64
|
-
// Cleanup listener on unmount
|
|
65
57
|
onBeforeUnmount(() => {
|
|
66
58
|
document.removeEventListener("visibilitychange", handleVisibilityChange)
|
|
67
59
|
})
|
|
68
60
|
})
|
|
69
61
|
|
|
70
62
|
function onDetect(detectedCodes: DetectedBarcode[]) {
|
|
71
|
-
result.value = detectedCodes.map((code) =>
|
|
72
|
-
// toast.add({
|
|
73
|
-
// title: 'Detected',
|
|
74
|
-
// description: `Value: ${code.rawValue}`,
|
|
75
|
-
// actions: [
|
|
76
|
-
// {
|
|
77
|
-
// label: 'Copy',
|
|
78
|
-
// onClick: () => {
|
|
79
|
-
// navigator.clipboard.writeText(code.rawValue)
|
|
80
|
-
// },
|
|
81
|
-
// },
|
|
82
|
-
// ],
|
|
83
|
-
// })
|
|
84
|
-
return code.rawValue
|
|
85
|
-
})
|
|
63
|
+
result.value = detectedCodes.map((code) => code.rawValue)
|
|
86
64
|
}
|
|
87
65
|
|
|
88
66
|
function onError(err: Error) {
|
|
@@ -95,19 +73,13 @@ function resetCamera() {
|
|
|
95
73
|
state.cameraOn = true
|
|
96
74
|
}
|
|
97
75
|
|
|
98
|
-
// Function to stop all media streams
|
|
99
76
|
function stopAllMediaStreams() {
|
|
100
|
-
// Stop streams via the QrcodeStream component ref
|
|
101
77
|
if (qrcodeStreamRef.value) {
|
|
102
78
|
try {
|
|
103
|
-
// Try to access the video element and stop its tracks
|
|
104
79
|
const videoElement = qrcodeStreamRef.value.$el?.querySelector("video")
|
|
105
80
|
if (videoElement && videoElement.srcObject) {
|
|
106
81
|
const stream = videoElement.srcObject as MediaStream
|
|
107
|
-
|
|
108
|
-
tracks.forEach((track) => {
|
|
109
|
-
track.stop()
|
|
110
|
-
})
|
|
82
|
+
stream.getTracks().forEach((track) => track.stop())
|
|
111
83
|
videoElement.srcObject = null
|
|
112
84
|
}
|
|
113
85
|
} catch (error) {
|
|
@@ -115,16 +87,11 @@ function stopAllMediaStreams() {
|
|
|
115
87
|
}
|
|
116
88
|
}
|
|
117
89
|
|
|
118
|
-
// Global cleanup: Find all video elements and stop their streams
|
|
119
90
|
try {
|
|
120
|
-
|
|
121
|
-
allVideoElements.forEach((video) => {
|
|
91
|
+
document.querySelectorAll("video").forEach((video) => {
|
|
122
92
|
if (video.srcObject) {
|
|
123
93
|
const stream = video.srcObject as MediaStream
|
|
124
|
-
|
|
125
|
-
tracks.forEach((track) => {
|
|
126
|
-
track.stop()
|
|
127
|
-
})
|
|
94
|
+
stream.getTracks().forEach((track) => track.stop())
|
|
128
95
|
video.srcObject = null
|
|
129
96
|
}
|
|
130
97
|
})
|
|
@@ -133,39 +100,31 @@ function stopAllMediaStreams() {
|
|
|
133
100
|
}
|
|
134
101
|
}
|
|
135
102
|
|
|
136
|
-
// Watch for camera state changes
|
|
137
103
|
watch(
|
|
138
104
|
() => state.cameraOn,
|
|
139
105
|
(newValue) => {
|
|
140
106
|
if (!newValue) {
|
|
141
|
-
|
|
142
|
-
nextTick(() => {
|
|
143
|
-
stopAllMediaStreams()
|
|
144
|
-
})
|
|
107
|
+
nextTick(() => stopAllMediaStreams())
|
|
145
108
|
}
|
|
146
109
|
}
|
|
147
110
|
)
|
|
148
111
|
|
|
149
|
-
// Stop camera when component is unmounted (e.g., route change)
|
|
150
112
|
onBeforeUnmount(() => {
|
|
151
113
|
state.cameraOn = false
|
|
152
114
|
stopAllMediaStreams()
|
|
153
115
|
})
|
|
154
116
|
|
|
155
|
-
// Also handle dynamic component switching (like in [componentName].vue)
|
|
156
117
|
onDeactivated(() => {
|
|
157
118
|
state.cameraOn = false
|
|
158
119
|
stopAllMediaStreams()
|
|
159
120
|
})
|
|
160
121
|
|
|
161
122
|
onActivated(() => {
|
|
162
|
-
// Reset state when component becomes active again
|
|
163
123
|
state.cameraOn = true
|
|
164
124
|
state.error = false
|
|
165
125
|
state.errorMsg = ""
|
|
166
126
|
})
|
|
167
127
|
|
|
168
|
-
// Use Nuxt's navigation guard to stop camera before route changes
|
|
169
128
|
onBeforeRouteLeave(() => {
|
|
170
129
|
state.cameraOn = false
|
|
171
130
|
stopAllMediaStreams()
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="decode-qr-code" :class="[elementClasses]">
|
|
3
|
-
<h2>Upload QR Code</h2>
|
|
4
3
|
<QrcodeCapture class="qr-code-capture" @detect="onDetect" />
|
|
5
|
-
|
|
6
|
-
<h2>Drop QR Code</h2>
|
|
7
4
|
<QrcodeDropZone class="qr-code-dropzone" @detect="onDetect" @dragover="onDropping" />
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<h5>Scanned QRCodes (Dropped): {{ isDropping ? "Dropping..." : "" }}</h5>
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
<div class="pt-4">
|
|
14
|
-
<h5>Scanned QRCodes:</h5>
|
|
15
|
-
<ul v-if="result" class="list-disc pl-4">
|
|
5
|
+
<div v-if="result?.length" class="scanned-results">
|
|
6
|
+
<ul>
|
|
16
7
|
<li v-for="(r, i) in result" :key="i">
|
|
17
|
-
<span
|
|
18
|
-
{{ r }}
|
|
19
|
-
</span>
|
|
8
|
+
<span>{{ r }}</span>
|
|
20
9
|
</li>
|
|
21
10
|
</ul>
|
|
22
11
|
</div>
|
|
@@ -26,12 +15,13 @@
|
|
|
26
15
|
<script setup lang="ts">
|
|
27
16
|
import type { DetectedBarcode } from "nuxt-qrcode"
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
styleClassPassthrough
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
interface Props {
|
|
19
|
+
styleClassPassthrough?: string | string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
+
styleClassPassthrough: () => [],
|
|
24
|
+
});
|
|
35
25
|
|
|
36
26
|
const result = ref<string[]>()
|
|
37
27
|
const isDropping = ref(false)
|
|
@@ -41,21 +31,7 @@ function onDropping(dropping: boolean) {
|
|
|
41
31
|
}
|
|
42
32
|
|
|
43
33
|
function onDetect(detectedCodes: DetectedBarcode[]) {
|
|
44
|
-
result.value = detectedCodes.map((code) =>
|
|
45
|
-
// toast.add({
|
|
46
|
-
// title: 'Detected',
|
|
47
|
-
// description: `Value: ${code.rawValue}`,
|
|
48
|
-
// actions: [
|
|
49
|
-
// {
|
|
50
|
-
// label: 'Copy',
|
|
51
|
-
// onClick: () => {
|
|
52
|
-
// navigator.clipboard.writeText(code.rawValue)
|
|
53
|
-
// },
|
|
54
|
-
// },
|
|
55
|
-
// ],
|
|
56
|
-
// })
|
|
57
|
-
return code.rawValue
|
|
58
|
-
})
|
|
34
|
+
result.value = detectedCodes.map((code) => code.rawValue)
|
|
59
35
|
}
|
|
60
36
|
|
|
61
37
|
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|