srcdev-nuxt-components 6.2.13 → 6.3.0
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/app/components/alert-mask/AlertMaskCore.vue +7 -16
- package/app/components/display-prompt/DisplayPromptCore.vue +4 -2
- package/app/components/display-toast/DisplayToast.vue +34 -104
- package/app/components/display-toast/molecules/DefaultToastContent.vue +165 -0
- package/app/components/display-tooltip/DisplayTooltip.vue +165 -0
- package/app/components/display-tooltip/DisplayTooltipDefined.vue +101 -0
- package/app/components/marquee-scroller/MarqueeScroller.vue +218 -53
- package/app/components/masonry-grid/MasonryGrid.vue +2 -2
- package/app/components/qr-code/CaptureQrCode.vue +181 -0
- package/app/components/qr-code/DecodeQrCode.vue +77 -0
- package/app/components/qr-code/DisplayQrCode.vue +51 -0
- package/app/composables/useTooltips.ts +174 -0
- package/app/types/components/alert-mask-core.d.ts +10 -0
- package/app/types/components/display-toast.d.ts +53 -0
- package/app/types/components/qr-code.d.ts +7 -0
- package/app/types/index.ts +5 -0
- package/nuxt.config.ts +2 -1
- package/package.json +7 -3
- package/types.d.ts +4 -0
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
<div
|
|
3
|
+
v-if="displayComponent"
|
|
4
|
+
class="marquee-scroller"
|
|
5
|
+
:class="{ reverse: reverse, paused: isPaused, 'reduced-motion': prefersReducedMotion }"
|
|
6
|
+
role="region"
|
|
7
|
+
:aria-label="ariaLabel || 'Scrolling content'"
|
|
8
|
+
:aria-live="isPaused ? 'polite' : 'off'"
|
|
9
|
+
tabindex="0"
|
|
10
|
+
@keydown="handleKeydown"
|
|
11
|
+
@focus="handleFocus"
|
|
12
|
+
@blur="handleBlur"
|
|
13
|
+
>
|
|
14
|
+
<!-- Screen reader instructions -->
|
|
15
|
+
<div class="sr-only">
|
|
16
|
+
{{ ariaDescription || "Use spacebar to pause/play animation, arrow keys when focused for manual control" }}
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Pause/Play button -->
|
|
20
|
+
<button
|
|
21
|
+
v-if="showControls"
|
|
22
|
+
class="control-btn"
|
|
23
|
+
@click="togglePause"
|
|
24
|
+
:aria-label="isPaused ? 'Play animation' : 'Pause animation'"
|
|
25
|
+
type="button"
|
|
26
|
+
>
|
|
27
|
+
<span aria-hidden="true">{{ isPaused ? "▶" : "⏸" }}</span>
|
|
28
|
+
</button>
|
|
29
|
+
|
|
30
|
+
<div class="marquee-track" :aria-hidden="!isPaused">
|
|
31
|
+
<div class="marquee-group">
|
|
32
|
+
<div v-for="item in marqueeData" :key="item.id" class="item">
|
|
33
|
+
<slot :name="item.id"></slot>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="marquee-group" aria-hidden="true">
|
|
37
|
+
<div v-for="item in marqueeData" :key="`duplicate-${item.id}`" class="item">
|
|
38
|
+
<slot :name="item.id"></slot>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
8
42
|
</div>
|
|
9
43
|
</template>
|
|
10
44
|
|
|
@@ -27,30 +61,95 @@ const props = defineProps({
|
|
|
27
61
|
default: () => ({
|
|
28
62
|
width: "50px",
|
|
29
63
|
height: "50px",
|
|
30
|
-
|
|
64
|
+
gap: "16px",
|
|
31
65
|
}),
|
|
32
66
|
required: true,
|
|
33
67
|
},
|
|
68
|
+
// Accessibility props
|
|
69
|
+
ariaLabel: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: null,
|
|
72
|
+
},
|
|
73
|
+
ariaDescription: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: null,
|
|
76
|
+
},
|
|
77
|
+
showControls: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: false,
|
|
80
|
+
},
|
|
81
|
+
respectReducedMotion: {
|
|
82
|
+
type: Boolean,
|
|
83
|
+
default: true,
|
|
84
|
+
},
|
|
34
85
|
})
|
|
35
86
|
|
|
36
87
|
const displayComponent = ref(false)
|
|
88
|
+
const isPaused = ref(false)
|
|
89
|
+
const isFocused = ref(false)
|
|
90
|
+
const prefersReducedMotion = ref(false)
|
|
37
91
|
|
|
38
92
|
const height = computed(() => props.itemConfig.height)
|
|
39
|
-
const quantity = computed(() => props.itemConfig.quantity)
|
|
40
93
|
const width = computed(() => props.itemConfig.width)
|
|
94
|
+
const gap = computed(() => props.itemConfig.gap || "16px")
|
|
41
95
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
96
|
+
// Check for reduced motion preference
|
|
97
|
+
const checkReducedMotion = () => {
|
|
98
|
+
if (typeof window !== "undefined" && props.respectReducedMotion) {
|
|
99
|
+
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
100
|
+
prefersReducedMotion.value = mediaQuery.matches
|
|
46
101
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
102
|
+
// Listen for changes
|
|
103
|
+
mediaQuery.addEventListener("change", (e) => {
|
|
104
|
+
prefersReducedMotion.value = e.matches
|
|
105
|
+
if (e.matches) {
|
|
106
|
+
isPaused.value = true
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const togglePause = () => {
|
|
113
|
+
isPaused.value = !isPaused.value
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
117
|
+
switch (event.key) {
|
|
118
|
+
case " ":
|
|
119
|
+
case "Spacebar":
|
|
120
|
+
event.preventDefault()
|
|
121
|
+
togglePause()
|
|
122
|
+
break
|
|
123
|
+
case "ArrowLeft":
|
|
124
|
+
case "ArrowRight":
|
|
125
|
+
// Could add manual stepping functionality here
|
|
126
|
+
event.preventDefault()
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const handleFocus = () => {
|
|
132
|
+
isFocused.value = true
|
|
133
|
+
if (props.respectReducedMotion) {
|
|
134
|
+
isPaused.value = true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const handleBlur = () => {
|
|
139
|
+
isFocused.value = false
|
|
140
|
+
if (!prefersReducedMotion.value) {
|
|
141
|
+
isPaused.value = false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
50
144
|
|
|
51
145
|
onMounted(() => {
|
|
52
|
-
console.log(`Mounted: quantity(${quantity.value}) | animationDelay(${animationDelay.value})`)
|
|
53
146
|
displayComponent.value = true
|
|
147
|
+
checkReducedMotion()
|
|
148
|
+
|
|
149
|
+
// Auto-pause if user prefers reduced motion
|
|
150
|
+
if (prefersReducedMotion.value) {
|
|
151
|
+
isPaused.value = true
|
|
152
|
+
}
|
|
54
153
|
})
|
|
55
154
|
</script>
|
|
56
155
|
|
|
@@ -60,63 +159,129 @@ onMounted(() => {
|
|
|
60
159
|
height: v-bind(height);
|
|
61
160
|
overflow: hidden;
|
|
62
161
|
mask-image: linear-gradient(to right, transparent, #000 10% 90%, transparent);
|
|
162
|
+
position: relative;
|
|
163
|
+
|
|
164
|
+
/* Focus styles */
|
|
165
|
+
&:focus {
|
|
166
|
+
outline: 2px solid var(--color-focus, #0066cc);
|
|
167
|
+
outline-offset: 2px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Paused state */
|
|
171
|
+
&.paused .marquee-track {
|
|
172
|
+
animation-play-state: paused;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Reduced motion - disable animation completely */
|
|
176
|
+
&.reduced-motion .marquee-track {
|
|
177
|
+
animation: none !important;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
&:hover .marquee-track {
|
|
181
|
+
animation-play-state: paused;
|
|
182
|
+
}
|
|
63
183
|
|
|
64
184
|
&:hover .item {
|
|
65
|
-
animation-play-state: paused !important;
|
|
66
185
|
filter: grayscale(1);
|
|
67
186
|
}
|
|
68
187
|
|
|
69
|
-
|
|
70
|
-
|
|
188
|
+
/* Screen reader only text */
|
|
189
|
+
.sr-only {
|
|
190
|
+
position: absolute;
|
|
191
|
+
width: 1px;
|
|
192
|
+
height: 1px;
|
|
193
|
+
padding: 0;
|
|
194
|
+
margin: -1px;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
clip: rect(0, 0, 0, 0);
|
|
197
|
+
white-space: nowrap;
|
|
198
|
+
border: 0;
|
|
71
199
|
}
|
|
72
200
|
|
|
73
|
-
|
|
201
|
+
/* Control button */
|
|
202
|
+
.control-btn {
|
|
203
|
+
position: absolute;
|
|
204
|
+
inset-block-start: 8px;
|
|
205
|
+
inset-inline-end: 8px;
|
|
206
|
+
z-index: 10;
|
|
207
|
+
background: rgba(0, 0, 0, 0.7);
|
|
208
|
+
color: white;
|
|
209
|
+
border: none;
|
|
210
|
+
border-radius: 4px;
|
|
211
|
+
padding: 8px;
|
|
212
|
+
cursor: pointer;
|
|
213
|
+
font-size: 14px;
|
|
214
|
+
transition: background-color 0.2s;
|
|
215
|
+
|
|
216
|
+
&:hover {
|
|
217
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&:focus {
|
|
221
|
+
outline: 2px solid var(--color-focus, #0066cc);
|
|
222
|
+
outline-offset: 2px;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.marquee-track {
|
|
74
227
|
display: flex;
|
|
75
|
-
width:
|
|
228
|
+
width: fit-content;
|
|
229
|
+
gap: v-bind(gap);
|
|
230
|
+
animation: marqueeMove v-bind(animationRuntime) linear infinite;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&.reverse .marquee-track {
|
|
234
|
+
animation-direction: reverse;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.marquee-group {
|
|
238
|
+
display: flex;
|
|
239
|
+
gap: v-bind(gap);
|
|
240
|
+
flex-shrink: 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.item {
|
|
244
|
+
width: v-bind(width);
|
|
76
245
|
height: v-bind(height);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
animation: autoRun v-bind(animationRuntime) linear infinite;
|
|
89
|
-
transition: filter 0.5s;
|
|
90
|
-
/* animation-delay: calc((50s / v-bind(quantity)) * (var(--position) - 1) - 50s) !important; */
|
|
91
|
-
animation-delay: calc(
|
|
92
|
-
(v-bind(animationDelay) / v-bind(quantity)) * (var(--position) - 1) - v-bind(animationDelay)
|
|
93
|
-
) !important;
|
|
94
|
-
|
|
95
|
-
border: 1px solid light-dark(var(--gray-12), var(--gray-0));
|
|
96
|
-
border-radius: 4px;
|
|
97
|
-
|
|
98
|
-
&:hover {
|
|
99
|
-
filter: grayscale(0);
|
|
100
|
-
}
|
|
246
|
+
display: grid;
|
|
247
|
+
place-items: center;
|
|
248
|
+
aspect-ratio: 1 / 1;
|
|
249
|
+
transition: filter 0.5s;
|
|
250
|
+
flex-shrink: 0;
|
|
251
|
+
|
|
252
|
+
border: 1px solid light-dark(var(--gray-12), var(--gray-0));
|
|
253
|
+
border-radius: 4px;
|
|
254
|
+
|
|
255
|
+
&:hover {
|
|
256
|
+
filter: grayscale(0);
|
|
101
257
|
}
|
|
102
258
|
}
|
|
103
259
|
}
|
|
104
260
|
|
|
105
|
-
@keyframes
|
|
261
|
+
@keyframes marqueeMove {
|
|
106
262
|
from {
|
|
107
|
-
|
|
263
|
+
transform: translateX(0);
|
|
108
264
|
}
|
|
109
265
|
to {
|
|
110
|
-
|
|
266
|
+
transform: translateX(-50%);
|
|
111
267
|
}
|
|
112
268
|
}
|
|
113
269
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
270
|
+
/* High contrast mode support */
|
|
271
|
+
@media (prefers-contrast: high) {
|
|
272
|
+
.marquee-scroller {
|
|
273
|
+
.control-btn {
|
|
274
|
+
background: ButtonFace;
|
|
275
|
+
color: ButtonText;
|
|
276
|
+
border: 1px solid ButtonText;
|
|
277
|
+
}
|
|
117
278
|
}
|
|
118
|
-
|
|
119
|
-
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Respect user's motion preferences */
|
|
282
|
+
@media (prefers-reduced-motion: reduce) {
|
|
283
|
+
.marquee-scroller .marquee-track {
|
|
284
|
+
animation: none !important;
|
|
120
285
|
}
|
|
121
286
|
}
|
|
122
287
|
</style>
|
|
@@ -53,8 +53,8 @@ watch(
|
|
|
53
53
|
--_border-color: light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
|
|
54
54
|
--_color: light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
|
|
55
55
|
|
|
56
|
-
columns: var(--_item-min-width);
|
|
57
|
-
gap:
|
|
56
|
+
columns: auto var(--_item-min-width);
|
|
57
|
+
column-gap: var(--_masonry-grid-gap);
|
|
58
58
|
|
|
59
59
|
.masonry-grid-item {
|
|
60
60
|
break-inside: avoid;
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
.capture-qr-stream {
|
|
179
|
+
aspect-ratio: 1 / 1;
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="decode-qr-code" :class="[elementClasses]">
|
|
3
|
+
<h2>Upload QR Code</h2>
|
|
4
|
+
<QrcodeCapture class="qr-code-capture" @detect="onDetect" />
|
|
5
|
+
|
|
6
|
+
<h2>Drop QR Code</h2>
|
|
7
|
+
<QrcodeDropZone class="qr-code-dropzone" @detect="onDetect" @dragover="onDropping" />
|
|
8
|
+
|
|
9
|
+
<div v-if="isDropping">
|
|
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">
|
|
16
|
+
<li v-for="(r, i) in result" :key="i">
|
|
17
|
+
<span class="text-wrap wrap-anywhere">
|
|
18
|
+
{{ r }}
|
|
19
|
+
</span>
|
|
20
|
+
</li>
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import type { DetectedBarcode } from "nuxt-qrcode"
|
|
28
|
+
|
|
29
|
+
const props = defineProps({
|
|
30
|
+
styleClassPassthrough: {
|
|
31
|
+
type: [String, Array] as PropType<string | string[]>,
|
|
32
|
+
default: () => [],
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const result = ref<string[]>()
|
|
37
|
+
const isDropping = ref(false)
|
|
38
|
+
|
|
39
|
+
function onDropping(dropping: boolean) {
|
|
40
|
+
isDropping.value = dropping
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
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
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<style lang="css">
|
|
65
|
+
.decode-qr-code {
|
|
66
|
+
aspect-ratio: 1 / 1;
|
|
67
|
+
|
|
68
|
+
.qr-code-capture {
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.qr-code-dropzone {
|
|
72
|
+
min-height: 3rem;
|
|
73
|
+
border-radius: 0.5rem;
|
|
74
|
+
border: 2px dashed gray;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
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/qr-code" // Adjust the import path as needed
|
|
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
|
+
.display-qr-code {
|
|
48
|
+
aspect-ratio: 1 / 1;
|
|
49
|
+
width: v-bind(size);
|
|
50
|
+
}
|
|
51
|
+
</style>
|