srcdev-nuxt-components 9.1.36 → 9.1.38
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 +2 -1
- package/.claude/settings.local.json +2 -2
- package/.claude/skills/components/capture-qr-code.md +84 -0
- package/.claude/skills/components/data-grid.md +167 -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 +5 -2
- package/app/components/01.atoms/grids/data-grid/DataGrid.vue +39 -0
- package/app/components/01.atoms/grids/data-grid/stories/DataGrid.stories.ts +234 -0
- package/app/components/01.atoms/grids/data-grid/tests/DataGrid.spec.ts +140 -0
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +11 -0
- 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/02.molecules/qr-code/CaptureQrCode.vue +142 -0
- 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/layouts/default.vue +0 -1
- package/app/pages/ui/qr-code/[componentName].vue +3 -3
- package/app/pages/ui/simple-grid.vue +2 -2
- package/package.json +1 -1
- package/.claude/skills/components/scroll-parallax-section.md +0 -148
- package/app/components/01.atoms/scroll-parallax-section/ScrollParallaxSection.vue +0 -108
- package/app/components/01.atoms/scroll-parallax-section/stories/ScrollParallaxSection.stories.ts +0 -151
- package/app/components/01.atoms/scroll-parallax-section/tests/ScrollParallaxSection.spec.ts +0 -91
- package/app/components/display-grid/DisplayGridCore.vue +0 -22
- package/app/components/qr-code/CaptureQrCode.vue +0 -183
- package/app/components/qr-code/DisplayQrCode.vue +0 -53
- package/app/components/qr-code/stories/QrCode.stories.ts +0 -933
- package/app/pages/ui/scroll-parallax-section.vue +0 -65
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { nextTick } from "vue";
|
|
3
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
-
import ScrollParallaxSection from "../ScrollParallaxSection.vue";
|
|
5
|
-
|
|
6
|
-
describe("ScrollParallaxSection", () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
|
9
|
-
cb(0);
|
|
10
|
-
return 0;
|
|
11
|
-
});
|
|
12
|
-
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("mounts without error", async () => {
|
|
16
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
17
|
-
props: { backgroundImage: "/img/bg.jpg" },
|
|
18
|
-
});
|
|
19
|
-
expect(wrapper.vm).toBeTruthy();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("renders the bg and content divs", async () => {
|
|
23
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
24
|
-
props: { backgroundImage: "/img/bg.jpg" },
|
|
25
|
-
});
|
|
26
|
-
expect(wrapper.find(".scroll-parallax-section__bg").exists()).toBe(true);
|
|
27
|
-
expect(wrapper.find(".scroll-parallax-section__content").exists()).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("renders slot content inside __content", async () => {
|
|
31
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
32
|
-
props: { backgroundImage: "/img/bg.jpg" },
|
|
33
|
-
slots: { default: "<p>Hello</p>" },
|
|
34
|
-
});
|
|
35
|
-
expect(wrapper.find(".scroll-parallax-section__content p").text()).toBe("Hello");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("renders as the given tag", async () => {
|
|
39
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
40
|
-
props: { backgroundImage: "/img/bg.jpg", tag: "section" },
|
|
41
|
-
});
|
|
42
|
-
expect(wrapper.element.tagName.toLowerCase()).toBe("section");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("defaults to div tag", async () => {
|
|
46
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
47
|
-
props: { backgroundImage: "/img/bg.jpg" },
|
|
48
|
-
});
|
|
49
|
-
expect(wrapper.element.tagName.toLowerCase()).toBe("div");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("applies styleClassPassthrough classes", async () => {
|
|
53
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
54
|
-
props: { backgroundImage: "/img/bg.jpg", styleClassPassthrough: "my-custom-class" },
|
|
55
|
-
});
|
|
56
|
-
expect(wrapper.classes()).toContain("my-custom-class");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("adds and removes scroll and resize listeners on mount/unmount", async () => {
|
|
60
|
-
const addSpy = vi.spyOn(window, "addEventListener");
|
|
61
|
-
const removeSpy = vi.spyOn(window, "removeEventListener");
|
|
62
|
-
|
|
63
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
64
|
-
props: { backgroundImage: "/img/bg.jpg" },
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(addSpy).toHaveBeenCalledWith("scroll", expect.any(Function), { passive: true });
|
|
68
|
-
expect(addSpy).toHaveBeenCalledWith("resize", expect.any(Function), { passive: true });
|
|
69
|
-
|
|
70
|
-
wrapper.unmount();
|
|
71
|
-
|
|
72
|
-
expect(removeSpy).toHaveBeenCalledWith("scroll", expect.any(Function));
|
|
73
|
-
expect(removeSpy).toHaveBeenCalledWith("resize", expect.any(Function));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("updates bg transform on scroll", async () => {
|
|
77
|
-
const wrapper = await mountSuspended(ScrollParallaxSection, {
|
|
78
|
-
props: { backgroundImage: "/img/bg.jpg", parallaxStrength: 0.3 },
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const bg = wrapper.find(".scroll-parallax-section__bg").element as HTMLElement;
|
|
82
|
-
|
|
83
|
-
Object.defineProperty(wrapper.element, "offsetTop", { value: 400, configurable: true });
|
|
84
|
-
Object.defineProperty(wrapper.element, "offsetHeight", { value: 600, configurable: true });
|
|
85
|
-
|
|
86
|
-
window.dispatchEvent(new Event("scroll"));
|
|
87
|
-
await nextTick();
|
|
88
|
-
|
|
89
|
-
expect(bg.style.transform).toMatch(/translate3d\(/);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div :class="[elementClasses]">
|
|
3
|
-
<slot v-for="item in gridData" :key="item.id" :name="item.id"></slot>
|
|
4
|
-
</div>
|
|
5
|
-
</template>
|
|
6
|
-
|
|
7
|
-
<script setup lang="ts">
|
|
8
|
-
const props = defineProps({
|
|
9
|
-
gridData: {
|
|
10
|
-
type: Object,
|
|
11
|
-
default: () => ({}),
|
|
12
|
-
},
|
|
13
|
-
styleClassPassthrough: {
|
|
14
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
15
|
-
default: () => [],
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
20
|
-
|
|
21
|
-
const gridData = toRef(() => props.gridData);
|
|
22
|
-
</script>
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="capture-qr-stream" :class="[elementClasses]">
|
|
3
|
-
<h2>Capture QR Code</h2>
|
|
4
|
-
<div v-if="!state.error">
|
|
5
|
-
<QrcodeStream v-if="state.cameraOn" ref="qrcodeStreamRef" @error="onError" @detect="onDetect" />
|
|
6
|
-
<div v-else class="camera-stopped">
|
|
7
|
-
<p>Camera stopped</p>
|
|
8
|
-
</div>
|
|
9
|
-
<div class="pt-4">
|
|
10
|
-
<h5>Scanned QRCodes:</h5>
|
|
11
|
-
<ul v-if="result" class="list-disc pl-4">
|
|
12
|
-
<li v-for="(r, i) in result" :key="i">
|
|
13
|
-
<span class="text-wrap wrap-anywhere">
|
|
14
|
-
{{ r }}
|
|
15
|
-
</span>
|
|
16
|
-
</li>
|
|
17
|
-
</ul>
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
<div v-else>
|
|
21
|
-
<h3>
|
|
22
|
-
{{ state.errorMsg }}
|
|
23
|
-
</h3>
|
|
24
|
-
<button @click="resetCamera">reset</button>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
</template>
|
|
28
|
-
|
|
29
|
-
<script setup lang="ts">
|
|
30
|
-
import type { DetectedBarcode } from "nuxt-qrcode"
|
|
31
|
-
|
|
32
|
-
const props = defineProps({
|
|
33
|
-
styleClassPassthrough: {
|
|
34
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
35
|
-
default: () => [],
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const qrcodeStreamRef = ref()
|
|
40
|
-
const result = ref<string[]>()
|
|
41
|
-
const state = reactive({
|
|
42
|
-
errorMsg: "",
|
|
43
|
-
error: false,
|
|
44
|
-
cameraOn: true,
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
// Reset camera state when component mounts
|
|
48
|
-
onMounted(() => {
|
|
49
|
-
// Reset to default state on mount
|
|
50
|
-
state.cameraOn = true
|
|
51
|
-
state.error = false
|
|
52
|
-
state.errorMsg = ""
|
|
53
|
-
result.value = []
|
|
54
|
-
|
|
55
|
-
const handleVisibilityChange = () => {
|
|
56
|
-
if (document.hidden) {
|
|
57
|
-
state.cameraOn = false
|
|
58
|
-
stopAllMediaStreams()
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
document.addEventListener("visibilitychange", handleVisibilityChange)
|
|
63
|
-
|
|
64
|
-
// Cleanup listener on unmount
|
|
65
|
-
onBeforeUnmount(() => {
|
|
66
|
-
document.removeEventListener("visibilitychange", handleVisibilityChange)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
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
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function onError(err: Error) {
|
|
89
|
-
state.error = true
|
|
90
|
-
state.errorMsg = `[${err.name}]: ${err.message}`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function resetCamera() {
|
|
94
|
-
state.error = false
|
|
95
|
-
state.cameraOn = true
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Function to stop all media streams
|
|
99
|
-
function stopAllMediaStreams() {
|
|
100
|
-
// Stop streams via the QrcodeStream component ref
|
|
101
|
-
if (qrcodeStreamRef.value) {
|
|
102
|
-
try {
|
|
103
|
-
// Try to access the video element and stop its tracks
|
|
104
|
-
const videoElement = qrcodeStreamRef.value.$el?.querySelector("video")
|
|
105
|
-
if (videoElement && videoElement.srcObject) {
|
|
106
|
-
const stream = videoElement.srcObject as MediaStream
|
|
107
|
-
const tracks = stream.getTracks()
|
|
108
|
-
tracks.forEach((track) => {
|
|
109
|
-
track.stop()
|
|
110
|
-
})
|
|
111
|
-
videoElement.srcObject = null
|
|
112
|
-
}
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.warn("Error stopping camera stream:", error)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Global cleanup: Find all video elements and stop their streams
|
|
119
|
-
try {
|
|
120
|
-
const allVideoElements = document.querySelectorAll("video")
|
|
121
|
-
allVideoElements.forEach((video) => {
|
|
122
|
-
if (video.srcObject) {
|
|
123
|
-
const stream = video.srcObject as MediaStream
|
|
124
|
-
const tracks = stream.getTracks()
|
|
125
|
-
tracks.forEach((track) => {
|
|
126
|
-
track.stop()
|
|
127
|
-
})
|
|
128
|
-
video.srcObject = null
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.warn("Error in global video cleanup:", error)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Watch for camera state changes
|
|
137
|
-
watch(
|
|
138
|
-
() => state.cameraOn,
|
|
139
|
-
(newValue) => {
|
|
140
|
-
if (!newValue) {
|
|
141
|
-
// Wait a tick for the component to unmount, then clean up
|
|
142
|
-
nextTick(() => {
|
|
143
|
-
stopAllMediaStreams()
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
// Stop camera when component is unmounted (e.g., route change)
|
|
150
|
-
onBeforeUnmount(() => {
|
|
151
|
-
state.cameraOn = false
|
|
152
|
-
stopAllMediaStreams()
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// Also handle dynamic component switching (like in [componentName].vue)
|
|
156
|
-
onDeactivated(() => {
|
|
157
|
-
state.cameraOn = false
|
|
158
|
-
stopAllMediaStreams()
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
onActivated(() => {
|
|
162
|
-
// Reset state when component becomes active again
|
|
163
|
-
state.cameraOn = true
|
|
164
|
-
state.error = false
|
|
165
|
-
state.errorMsg = ""
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// Use Nuxt's navigation guard to stop camera before route changes
|
|
169
|
-
onBeforeRouteLeave(() => {
|
|
170
|
-
state.cameraOn = false
|
|
171
|
-
stopAllMediaStreams()
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|
|
175
|
-
</script>
|
|
176
|
-
|
|
177
|
-
<style lang="css">
|
|
178
|
-
@layer components {
|
|
179
|
-
.capture-qr-stream {
|
|
180
|
-
aspect-ratio: 1 / 1;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
</style>
|
|
@@ -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>
|