@codingfactory/inventory-locator-client 0.1.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.
Files changed (53) hide show
  1. package/package.json +47 -0
  2. package/src/api/client.ts +31 -0
  3. package/src/components/counting/CountEntryForm.vue +833 -0
  4. package/src/components/counting/CountReport.vue +385 -0
  5. package/src/components/counting/CountSessionDetail.vue +650 -0
  6. package/src/components/counting/CountSessionList.vue +683 -0
  7. package/src/components/dashboard/InventoryDashboard.vue +670 -0
  8. package/src/components/dashboard/UnlocatedProducts.vue +468 -0
  9. package/src/components/labels/LabelBatchPrint.vue +528 -0
  10. package/src/components/labels/LabelPreview.vue +293 -0
  11. package/src/components/locations/InventoryLocatorShell.vue +408 -0
  12. package/src/components/locations/LocationBreadcrumb.vue +144 -0
  13. package/src/components/locations/LocationCodeBadge.vue +46 -0
  14. package/src/components/locations/LocationDetail.vue +884 -0
  15. package/src/components/locations/LocationForm.vue +360 -0
  16. package/src/components/locations/LocationSearchInput.vue +428 -0
  17. package/src/components/locations/LocationTree.vue +156 -0
  18. package/src/components/locations/LocationTreeNode.vue +280 -0
  19. package/src/components/locations/LocationTypeIcon.vue +58 -0
  20. package/src/components/products/LocationProductAdd.vue +637 -0
  21. package/src/components/products/LocationProductList.vue +547 -0
  22. package/src/components/products/ProductLocationList.vue +215 -0
  23. package/src/components/products/QuickMoveModal.vue +592 -0
  24. package/src/components/scanning/ScanHistory.vue +146 -0
  25. package/src/components/scanning/ScanResult.vue +350 -0
  26. package/src/components/scanning/ScannerOverlay.vue +696 -0
  27. package/src/components/shared/InvBadge.vue +71 -0
  28. package/src/components/shared/InvButton.vue +206 -0
  29. package/src/components/shared/InvCard.vue +254 -0
  30. package/src/components/shared/InvEmptyState.vue +132 -0
  31. package/src/components/shared/InvInput.vue +125 -0
  32. package/src/components/shared/InvModal.vue +296 -0
  33. package/src/components/shared/InvSelect.vue +155 -0
  34. package/src/components/shared/InvTable.vue +288 -0
  35. package/src/composables/useCountSessions.ts +184 -0
  36. package/src/composables/useLabelPrinting.ts +71 -0
  37. package/src/composables/useLocationBreadcrumbs.ts +19 -0
  38. package/src/composables/useLocationProducts.ts +125 -0
  39. package/src/composables/useLocationSearch.ts +46 -0
  40. package/src/composables/useLocations.ts +159 -0
  41. package/src/composables/useMovements.ts +71 -0
  42. package/src/composables/useScanner.ts +83 -0
  43. package/src/env.d.ts +7 -0
  44. package/src/index.ts +46 -0
  45. package/src/plugin.ts +14 -0
  46. package/src/stores/countStore.ts +95 -0
  47. package/src/stores/locationStore.ts +113 -0
  48. package/src/stores/scannerStore.ts +51 -0
  49. package/src/types/index.ts +216 -0
  50. package/src/utils/codeFormatter.ts +29 -0
  51. package/src/utils/locationIcons.ts +64 -0
  52. package/tsconfig.json +21 -0
  53. package/vite.config.ts +37 -0
@@ -0,0 +1,696 @@
1
+ <template>
2
+ <div class="scanner-overlay">
3
+ <!-- Counting mode banner -->
4
+ <div
5
+ v-if="countStore.isCountingMode"
6
+ class="scanner-overlay__counting-banner"
7
+ role="status"
8
+ >
9
+ <svg
10
+ width="18"
11
+ height="18"
12
+ viewBox="0 0 18 18"
13
+ fill="none"
14
+ aria-hidden="true"
15
+ >
16
+ <rect x="2" y="2" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5" />
17
+ <path d="M6 6h2v2H6V6zm4 0h2v2h-2V6zm-4 4h2v2H6v-2zm4 0h2v2h-2v-2z" fill="currentColor" />
18
+ </svg>
19
+ <span class="scanner-overlay__counting-text">
20
+ Counting: Session #{{ countStore.activeSessionId }}
21
+ </span>
22
+ </div>
23
+
24
+ <!-- Header controls -->
25
+ <div class="scanner-overlay__header">
26
+ <button
27
+ type="button"
28
+ class="scanner-overlay__close-btn"
29
+ aria-label="Close scanner"
30
+ @click="handleClose"
31
+ >
32
+ <svg
33
+ width="24"
34
+ height="24"
35
+ viewBox="0 0 24 24"
36
+ fill="none"
37
+ aria-hidden="true"
38
+ >
39
+ <path
40
+ d="M18 6L6 18M6 6L18 18"
41
+ stroke="currentColor"
42
+ stroke-width="2"
43
+ stroke-linecap="round"
44
+ stroke-linejoin="round"
45
+ />
46
+ </svg>
47
+ </button>
48
+
49
+ <button
50
+ v-if="scanner.isScanning.value && scanner.isSupported.value"
51
+ type="button"
52
+ class="scanner-overlay__flash-btn"
53
+ :class="{ 'scanner-overlay__flash-btn--active': scanner.flashOn.value }"
54
+ :aria-label="scanner.flashOn.value ? 'Turn off flash' : 'Turn on flash'"
55
+ :aria-pressed="scanner.flashOn.value"
56
+ @click="scanner.toggleFlash()"
57
+ >
58
+ <svg
59
+ width="24"
60
+ height="24"
61
+ viewBox="0 0 24 24"
62
+ fill="none"
63
+ aria-hidden="true"
64
+ >
65
+ <path
66
+ d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"
67
+ stroke="currentColor"
68
+ stroke-width="2"
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ :fill="scanner.flashOn.value ? 'currentColor' : 'none'"
72
+ />
73
+ </svg>
74
+ </button>
75
+ </div>
76
+
77
+ <!-- Camera area -->
78
+ <div v-if="scanner.isSupported.value" class="scanner-overlay__camera-area">
79
+ <div
80
+ v-show="scanner.isScanning.value && !scanResult"
81
+ :id="scannerElementId"
82
+ class="scanner-overlay__camera-feed"
83
+ />
84
+
85
+ <!-- Scan box overlay guide -->
86
+ <div
87
+ v-if="scanner.isScanning.value && !scanResult"
88
+ class="scanner-overlay__scan-guide"
89
+ aria-hidden="true"
90
+ >
91
+ <div class="scanner-overlay__scan-box">
92
+ <span class="scanner-overlay__scan-corner scanner-overlay__scan-corner--tl" />
93
+ <span class="scanner-overlay__scan-corner scanner-overlay__scan-corner--tr" />
94
+ <span class="scanner-overlay__scan-corner scanner-overlay__scan-corner--bl" />
95
+ <span class="scanner-overlay__scan-corner scanner-overlay__scan-corner--br" />
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Camera not started message -->
100
+ <div
101
+ v-if="!scanner.isScanning.value && !scanner.error.value && !scanResult"
102
+ class="scanner-overlay__starting"
103
+ role="status"
104
+ >
105
+ <div class="scanner-overlay__spinner" aria-hidden="true" />
106
+ <p class="scanner-overlay__starting-text">Starting camera...</p>
107
+ </div>
108
+
109
+ <!-- Camera error -->
110
+ <div
111
+ v-if="scanner.error.value && !scanResult"
112
+ class="scanner-overlay__error"
113
+ role="alert"
114
+ >
115
+ <svg
116
+ width="32"
117
+ height="32"
118
+ viewBox="0 0 24 24"
119
+ fill="none"
120
+ aria-hidden="true"
121
+ >
122
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" />
123
+ <path d="M15 9L9 15M9 9L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
124
+ </svg>
125
+ <p class="scanner-overlay__error-title">Camera access is required for scanning</p>
126
+ <p class="scanner-overlay__error-desc">
127
+ Enable camera access in your browser or device settings, or use manual entry below.
128
+ </p>
129
+ <InvButton
130
+ variant="secondary"
131
+ size="sm"
132
+ @click="retryCamera"
133
+ >
134
+ Try Again
135
+ </InvButton>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- No camera fallback (desktop) -->
140
+ <div
141
+ v-else
142
+ class="scanner-overlay__no-camera"
143
+ role="status"
144
+ >
145
+ <svg
146
+ width="48"
147
+ height="48"
148
+ viewBox="0 0 24 24"
149
+ fill="none"
150
+ aria-hidden="true"
151
+ >
152
+ <rect x="2" y="6" width="20" height="14" rx="2" stroke="currentColor" stroke-width="1.5" />
153
+ <circle cx="12" cy="13" r="4" stroke="currentColor" stroke-width="1.5" />
154
+ <path d="M2 8L22 8" stroke="currentColor" stroke-width="1.5" />
155
+ <path d="M3 3L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
156
+ </svg>
157
+ <p class="scanner-overlay__no-camera-text">
158
+ No camera detected. Use manual entry to look up locations.
159
+ </p>
160
+ </div>
161
+
162
+ <!-- Instruction text -->
163
+ <p
164
+ v-if="scanner.isSupported.value && scanner.isScanning.value && !scanResult"
165
+ class="scanner-overlay__instruction"
166
+ >
167
+ Point camera at QR code or barcode
168
+ </p>
169
+
170
+ <!-- Scan lookup status -->
171
+ <div aria-live="polite" class="sr-only">
172
+ <span v-if="lookupLoading">Looking up scanned code...</span>
173
+ <span v-if="lookupError">{{ lookupError }}</span>
174
+ <span v-if="scanResult">Location found: {{ scanResult.name }}</span>
175
+ </div>
176
+
177
+ <!-- Lookup error toast -->
178
+ <div
179
+ v-if="lookupError"
180
+ class="scanner-overlay__toast scanner-overlay__toast--error"
181
+ role="alert"
182
+ >
183
+ <span>{{ lookupError }}</span>
184
+ <button
185
+ type="button"
186
+ class="scanner-overlay__toast-dismiss"
187
+ aria-label="Dismiss error"
188
+ @click="dismissError"
189
+ >
190
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
191
+ <path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
192
+ </svg>
193
+ </button>
194
+ </div>
195
+
196
+ <!-- Manual entry -->
197
+ <div class="scanner-overlay__manual">
198
+ <label for="scanner-manual-input" class="scanner-overlay__manual-label">
199
+ Or enter code manually:
200
+ </label>
201
+ <div class="scanner-overlay__manual-row">
202
+ <input
203
+ id="scanner-manual-input"
204
+ v-model="manualCode"
205
+ type="text"
206
+ class="scanner-overlay__manual-input"
207
+ placeholder="e.g. WH1-A-01"
208
+ :disabled="lookupLoading"
209
+ @keydown.enter="handleManualSubmit"
210
+ />
211
+ <InvButton
212
+ variant="primary"
213
+ size="md"
214
+ :loading="lookupLoading"
215
+ :disabled="!manualCode.trim()"
216
+ @click="handleManualSubmit"
217
+ >
218
+ Go
219
+ </InvButton>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Recent scans -->
224
+ <ScanHistory
225
+ v-if="!scanResult"
226
+ :limit="5"
227
+ class="scanner-overlay__history"
228
+ />
229
+
230
+ <!-- Scan result bottom sheet -->
231
+ <ScanResult
232
+ v-if="scanResult"
233
+ :location="scanResult"
234
+ :show="!!scanResult"
235
+ @close="handleResultClose"
236
+ @navigate="handleResultNavigate"
237
+ @scan-again="handleScanAgain"
238
+ />
239
+ </div>
240
+ </template>
241
+
242
+ <script setup lang="ts">
243
+ import { ref, onMounted, onBeforeUnmount } from 'vue'
244
+ import { useRouter } from 'vue-router'
245
+ import { useScanner } from '../../composables/useScanner'
246
+ import { useLocations } from '../../composables/useLocations'
247
+ import { useScannerStore } from '../../stores/scannerStore'
248
+ import { useCountStore } from '../../stores/countStore'
249
+ import type { Location } from '../../types'
250
+ import InvButton from '../shared/InvButton.vue'
251
+ import ScanResult from './ScanResult.vue'
252
+ import ScanHistory from './ScanHistory.vue'
253
+
254
+ const router = useRouter()
255
+ const scanner = useScanner()
256
+ const locations = useLocations()
257
+ const scannerStore = useScannerStore()
258
+ const countStore = useCountStore()
259
+
260
+ const scannerElementId = 'inv-scanner-feed'
261
+ const manualCode = ref('')
262
+ const scanResult = ref<Location | null>(null)
263
+ const lookupLoading = ref(false)
264
+ const lookupError = ref<string | null>(null)
265
+
266
+ onMounted(async () => {
267
+ scannerStore.loadFromSession()
268
+ if (scanner.isSupported.value) {
269
+ await startCamera()
270
+ }
271
+ })
272
+
273
+ onBeforeUnmount(async () => {
274
+ await scanner.stopScanning()
275
+ })
276
+
277
+ async function startCamera() {
278
+ lookupError.value = null
279
+ await scanner.startScanning(scannerElementId, handleScanResult)
280
+ }
281
+
282
+ async function retryCamera() {
283
+ scanner.error.value = null
284
+ await startCamera()
285
+ }
286
+
287
+ async function handleScanResult(code: string) {
288
+ await scanner.stopScanning()
289
+ await lookupCode(code)
290
+ }
291
+
292
+ async function handleManualSubmit() {
293
+ const code = manualCode.value.trim()
294
+ if (!code) return
295
+ await lookupCode(code)
296
+ }
297
+
298
+ async function lookupCode(code: string) {
299
+ lookupLoading.value = true
300
+ lookupError.value = null
301
+
302
+ try {
303
+ const location = await locations.scanLookup(code)
304
+ if (location) {
305
+ scanResult.value = location
306
+ scannerStore.addToHistory({
307
+ code: location.full_code,
308
+ locationName: location.name,
309
+ locationId: location.id,
310
+ })
311
+ } else {
312
+ lookupError.value = `No location found for code "${code}"`
313
+ if (scanner.isSupported.value) {
314
+ await startCamera()
315
+ }
316
+ }
317
+ } catch {
318
+ lookupError.value = 'Could not look up location. Check connection.'
319
+ if (scanner.isSupported.value) {
320
+ await startCamera()
321
+ }
322
+ } finally {
323
+ lookupLoading.value = false
324
+ }
325
+ }
326
+
327
+ function dismissError() {
328
+ lookupError.value = null
329
+ }
330
+
331
+ function handleResultClose() {
332
+ scanResult.value = null
333
+ }
334
+
335
+ function handleResultNavigate(location: Location) {
336
+ scanResult.value = null
337
+ router.push({
338
+ name: 'inventory-location',
339
+ params: { code: location.full_code },
340
+ })
341
+ }
342
+
343
+ async function handleScanAgain() {
344
+ scanResult.value = null
345
+ manualCode.value = ''
346
+ if (scanner.isSupported.value) {
347
+ await startCamera()
348
+ }
349
+ }
350
+
351
+ function handleClose() {
352
+ router.back()
353
+ }
354
+ </script>
355
+
356
+ <style scoped>
357
+ .scanner-overlay {
358
+ position: fixed;
359
+ inset: 0;
360
+ z-index: 9000;
361
+ display: flex;
362
+ flex-direction: column;
363
+ background: var(--admin-bg, #0f172a);
364
+ color: #fff;
365
+ overflow: hidden;
366
+ }
367
+
368
+ .scanner-overlay__counting-banner {
369
+ display: flex;
370
+ align-items: center;
371
+ gap: var(--space-2, 0.5rem);
372
+ padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
373
+ background: color-mix(in srgb, var(--color-primary, #2563eb) 90%, black);
374
+ color: #fff;
375
+ font-size: var(--text-sm, 0.875rem);
376
+ font-weight: 500;
377
+ flex-shrink: 0;
378
+ }
379
+
380
+ .scanner-overlay__counting-text {
381
+ overflow: hidden;
382
+ text-overflow: ellipsis;
383
+ white-space: nowrap;
384
+ }
385
+
386
+ .scanner-overlay__header {
387
+ display: flex;
388
+ align-items: center;
389
+ justify-content: space-between;
390
+ padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
391
+ flex-shrink: 0;
392
+ z-index: 10;
393
+ position: relative;
394
+ }
395
+
396
+ .scanner-overlay__close-btn,
397
+ .scanner-overlay__flash-btn {
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ width: 44px;
402
+ height: 44px;
403
+ padding: 0;
404
+ border: none;
405
+ border-radius: 50%;
406
+ background: rgba(0, 0, 0, 0.4);
407
+ color: #fff;
408
+ cursor: pointer;
409
+ transition: background-color 0.15s ease;
410
+ }
411
+
412
+ .scanner-overlay__close-btn:hover,
413
+ .scanner-overlay__flash-btn:hover {
414
+ background: rgba(0, 0, 0, 0.6);
415
+ }
416
+
417
+ .scanner-overlay__close-btn:focus-visible,
418
+ .scanner-overlay__flash-btn:focus-visible {
419
+ outline: 2px solid var(--admin-focus-ring, #2563eb);
420
+ outline-offset: 2px;
421
+ }
422
+
423
+ .scanner-overlay__flash-btn--active {
424
+ background: rgba(255, 255, 255, 0.3);
425
+ }
426
+
427
+ .scanner-overlay__camera-area {
428
+ flex: 1;
429
+ position: relative;
430
+ min-height: 0;
431
+ display: flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ overflow: hidden;
435
+ }
436
+
437
+ .scanner-overlay__camera-feed {
438
+ width: 100%;
439
+ height: 100%;
440
+ }
441
+
442
+ .scanner-overlay__camera-feed :deep(video) {
443
+ width: 100% !important;
444
+ height: 100% !important;
445
+ object-fit: cover;
446
+ }
447
+
448
+ .scanner-overlay__scan-guide {
449
+ position: absolute;
450
+ inset: 0;
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ pointer-events: none;
455
+ }
456
+
457
+ .scanner-overlay__scan-box {
458
+ width: 250px;
459
+ height: 250px;
460
+ position: relative;
461
+ }
462
+
463
+ .scanner-overlay__scan-corner {
464
+ position: absolute;
465
+ width: 28px;
466
+ height: 28px;
467
+ border-color: #fff;
468
+ border-style: solid;
469
+ border-width: 0;
470
+ }
471
+
472
+ .scanner-overlay__scan-corner--tl {
473
+ top: 0;
474
+ left: 0;
475
+ border-top-width: 3px;
476
+ border-left-width: 3px;
477
+ border-top-left-radius: 8px;
478
+ }
479
+
480
+ .scanner-overlay__scan-corner--tr {
481
+ top: 0;
482
+ right: 0;
483
+ border-top-width: 3px;
484
+ border-right-width: 3px;
485
+ border-top-right-radius: 8px;
486
+ }
487
+
488
+ .scanner-overlay__scan-corner--bl {
489
+ bottom: 0;
490
+ left: 0;
491
+ border-bottom-width: 3px;
492
+ border-left-width: 3px;
493
+ border-bottom-left-radius: 8px;
494
+ }
495
+
496
+ .scanner-overlay__scan-corner--br {
497
+ bottom: 0;
498
+ right: 0;
499
+ border-bottom-width: 3px;
500
+ border-right-width: 3px;
501
+ border-bottom-right-radius: 8px;
502
+ }
503
+
504
+ .scanner-overlay__starting {
505
+ display: flex;
506
+ flex-direction: column;
507
+ align-items: center;
508
+ gap: var(--space-3, 0.75rem);
509
+ }
510
+
511
+ .scanner-overlay__spinner {
512
+ width: 36px;
513
+ height: 36px;
514
+ border: 3px solid rgba(255, 255, 255, 0.2);
515
+ border-top-color: #fff;
516
+ border-radius: 50%;
517
+ animation: scanner-spin 0.8s linear infinite;
518
+ }
519
+
520
+ @keyframes scanner-spin {
521
+ from { transform: rotate(0deg); }
522
+ to { transform: rotate(360deg); }
523
+ }
524
+
525
+ .scanner-overlay__starting-text {
526
+ margin: 0;
527
+ font-size: var(--text-sm, 0.875rem);
528
+ color: rgba(255, 255, 255, 0.7);
529
+ }
530
+
531
+ .scanner-overlay__error {
532
+ display: flex;
533
+ flex-direction: column;
534
+ align-items: center;
535
+ gap: var(--space-3, 0.75rem);
536
+ padding: var(--space-6, 1.5rem);
537
+ text-align: center;
538
+ color: rgba(255, 255, 255, 0.8);
539
+ }
540
+
541
+ .scanner-overlay__error-title {
542
+ margin: 0;
543
+ font-size: var(--text-base, 1rem);
544
+ font-weight: 600;
545
+ color: #fff;
546
+ }
547
+
548
+ .scanner-overlay__error-desc {
549
+ margin: 0;
550
+ font-size: var(--text-sm, 0.875rem);
551
+ max-width: 320px;
552
+ line-height: 1.5;
553
+ }
554
+
555
+ .scanner-overlay__no-camera {
556
+ flex: 1;
557
+ display: flex;
558
+ flex-direction: column;
559
+ align-items: center;
560
+ justify-content: center;
561
+ gap: var(--space-4, 1rem);
562
+ padding: var(--space-6, 1.5rem);
563
+ color: rgba(255, 255, 255, 0.6);
564
+ text-align: center;
565
+ }
566
+
567
+ .scanner-overlay__no-camera-text {
568
+ margin: 0;
569
+ font-size: var(--text-sm, 0.875rem);
570
+ max-width: 320px;
571
+ line-height: 1.5;
572
+ }
573
+
574
+ .scanner-overlay__instruction {
575
+ margin: 0;
576
+ padding: var(--space-3, 0.75rem);
577
+ text-align: center;
578
+ font-size: var(--text-sm, 0.875rem);
579
+ color: rgba(255, 255, 255, 0.7);
580
+ flex-shrink: 0;
581
+ }
582
+
583
+ .scanner-overlay__toast {
584
+ position: absolute;
585
+ bottom: 240px;
586
+ left: var(--space-4, 1rem);
587
+ right: var(--space-4, 1rem);
588
+ display: flex;
589
+ align-items: center;
590
+ justify-content: space-between;
591
+ gap: var(--space-2, 0.5rem);
592
+ padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
593
+ border-radius: 8px;
594
+ font-size: var(--text-sm, 0.875rem);
595
+ z-index: 20;
596
+ animation: scanner-toast-in 0.2s ease;
597
+ }
598
+
599
+ .scanner-overlay__toast--error {
600
+ background: var(--color-error, #dc2626);
601
+ color: #fff;
602
+ }
603
+
604
+ @keyframes scanner-toast-in {
605
+ from {
606
+ opacity: 0;
607
+ transform: translateY(8px);
608
+ }
609
+ to {
610
+ opacity: 1;
611
+ transform: translateY(0);
612
+ }
613
+ }
614
+
615
+ .scanner-overlay__toast-dismiss {
616
+ display: flex;
617
+ align-items: center;
618
+ justify-content: center;
619
+ width: 28px;
620
+ height: 28px;
621
+ padding: 0;
622
+ border: none;
623
+ border-radius: 50%;
624
+ background: rgba(255, 255, 255, 0.2);
625
+ color: #fff;
626
+ cursor: pointer;
627
+ flex-shrink: 0;
628
+ }
629
+
630
+ .scanner-overlay__toast-dismiss:hover {
631
+ background: rgba(255, 255, 255, 0.3);
632
+ }
633
+
634
+ .scanner-overlay__toast-dismiss:focus-visible {
635
+ outline: 2px solid #fff;
636
+ outline-offset: 2px;
637
+ }
638
+
639
+ .scanner-overlay__manual {
640
+ padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
641
+ flex-shrink: 0;
642
+ }
643
+
644
+ .scanner-overlay__manual-label {
645
+ display: block;
646
+ font-size: var(--text-sm, 0.875rem);
647
+ color: rgba(255, 255, 255, 0.7);
648
+ margin-bottom: var(--space-2, 0.5rem);
649
+ }
650
+
651
+ .scanner-overlay__manual-row {
652
+ display: flex;
653
+ gap: var(--space-2, 0.5rem);
654
+ }
655
+
656
+ .scanner-overlay__manual-input {
657
+ flex: 1;
658
+ height: 44px;
659
+ padding: 0 var(--space-3, 0.75rem);
660
+ border: 1px solid rgba(255, 255, 255, 0.2);
661
+ border-radius: 6px;
662
+ background: rgba(255, 255, 255, 0.1);
663
+ color: #fff;
664
+ font-size: var(--text-sm, 0.875rem);
665
+ outline: none;
666
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
667
+ }
668
+
669
+ .scanner-overlay__manual-input::placeholder {
670
+ color: rgba(255, 255, 255, 0.4);
671
+ }
672
+
673
+ .scanner-overlay__manual-input:focus {
674
+ border-color: var(--color-primary, #2563eb);
675
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
676
+ }
677
+
678
+ .scanner-overlay__history {
679
+ flex-shrink: 0;
680
+ padding: 0 var(--space-4, 1rem) var(--space-4, 1rem);
681
+ max-height: 160px;
682
+ overflow-y: auto;
683
+ }
684
+
685
+ .sr-only {
686
+ position: absolute;
687
+ width: 1px;
688
+ height: 1px;
689
+ padding: 0;
690
+ margin: -1px;
691
+ overflow: hidden;
692
+ clip: rect(0, 0, 0, 0);
693
+ white-space: nowrap;
694
+ border-width: 0;
695
+ }
696
+ </style>