@delightstack/components 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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
// ── QR Code Generator ─────────────────────────────────────────────
|
|
3
|
+
// Minimal self-contained implementation supporting versions 1-40,
|
|
4
|
+
// byte-mode encoding, and error correction levels L/M/Q/H.
|
|
5
|
+
// Based on the public-domain QR specification (ISO/IEC 18004).
|
|
6
|
+
|
|
7
|
+
type ECLevel = 'L' | 'M' | 'Q' | 'H';
|
|
8
|
+
|
|
9
|
+
const EC_LEVEL_BITS: Record<ECLevel, number> = { L: 1, M: 0, Q: 3, H: 2 };
|
|
10
|
+
|
|
11
|
+
// Error correction codewords per block and block structure for each version/level.
|
|
12
|
+
// Format: [ec_codewords_per_block, num_blocks_group1, data_codewords_per_block_g1, num_blocks_group2, data_codewords_per_block_g2]
|
|
13
|
+
const EC_TABLE: Record<ECLevel, number[][]> = {
|
|
14
|
+
L: [
|
|
15
|
+
[7, 1, 19, 0, 0],
|
|
16
|
+
[10, 1, 34, 0, 0],
|
|
17
|
+
[15, 1, 55, 0, 0],
|
|
18
|
+
[20, 1, 80, 0, 0],
|
|
19
|
+
[26, 1, 108, 0, 0],
|
|
20
|
+
[18, 2, 68, 0, 0],
|
|
21
|
+
[20, 2, 78, 0, 0],
|
|
22
|
+
[24, 2, 97, 0, 0],
|
|
23
|
+
[30, 2, 116, 0, 0],
|
|
24
|
+
[18, 2, 68, 2, 69],
|
|
25
|
+
[20, 4, 81, 0, 0],
|
|
26
|
+
[24, 2, 92, 2, 93],
|
|
27
|
+
[26, 4, 107, 0, 0],
|
|
28
|
+
[30, 3, 115, 1, 116],
|
|
29
|
+
[22, 5, 87, 1, 88],
|
|
30
|
+
[24, 5, 98, 1, 99],
|
|
31
|
+
[28, 1, 107, 5, 108],
|
|
32
|
+
[30, 5, 120, 1, 121],
|
|
33
|
+
[28, 3, 113, 4, 114],
|
|
34
|
+
[28, 3, 107, 5, 108],
|
|
35
|
+
[28, 4, 116, 4, 117],
|
|
36
|
+
[28, 2, 111, 7, 112],
|
|
37
|
+
[30, 4, 121, 5, 122],
|
|
38
|
+
[30, 6, 117, 4, 118],
|
|
39
|
+
[26, 8, 106, 4, 107],
|
|
40
|
+
[28, 10, 114, 2, 115],
|
|
41
|
+
[30, 8, 122, 4, 123],
|
|
42
|
+
[30, 3, 117, 10, 118],
|
|
43
|
+
[30, 7, 116, 7, 117],
|
|
44
|
+
[30, 5, 115, 10, 116],
|
|
45
|
+
[30, 13, 115, 3, 116],
|
|
46
|
+
[30, 17, 115, 0, 0],
|
|
47
|
+
[30, 17, 115, 1, 116],
|
|
48
|
+
[30, 13, 115, 6, 116],
|
|
49
|
+
[30, 12, 121, 7, 122],
|
|
50
|
+
[30, 6, 121, 14, 122],
|
|
51
|
+
[30, 17, 122, 4, 123],
|
|
52
|
+
[30, 4, 122, 18, 123],
|
|
53
|
+
[30, 20, 117, 4, 118],
|
|
54
|
+
[30, 19, 118, 6, 119],
|
|
55
|
+
],
|
|
56
|
+
M: [
|
|
57
|
+
[10, 1, 16, 0, 0],
|
|
58
|
+
[16, 1, 28, 0, 0],
|
|
59
|
+
[26, 1, 44, 0, 0],
|
|
60
|
+
[18, 2, 32, 0, 0],
|
|
61
|
+
[24, 2, 43, 0, 0],
|
|
62
|
+
[16, 4, 27, 0, 0],
|
|
63
|
+
[18, 4, 31, 0, 0],
|
|
64
|
+
[22, 2, 38, 2, 39],
|
|
65
|
+
[22, 3, 36, 2, 37],
|
|
66
|
+
[26, 4, 43, 1, 44],
|
|
67
|
+
[30, 1, 50, 4, 51],
|
|
68
|
+
[22, 6, 36, 2, 37],
|
|
69
|
+
[22, 8, 37, 1, 38],
|
|
70
|
+
[24, 4, 40, 5, 41],
|
|
71
|
+
[24, 5, 41, 5, 42],
|
|
72
|
+
[28, 7, 45, 3, 46],
|
|
73
|
+
[28, 10, 46, 1, 47],
|
|
74
|
+
[26, 9, 43, 4, 44],
|
|
75
|
+
[26, 3, 44, 11, 45],
|
|
76
|
+
[26, 3, 41, 13, 42],
|
|
77
|
+
[26, 17, 42, 0, 0],
|
|
78
|
+
[28, 17, 46, 0, 0],
|
|
79
|
+
[28, 4, 47, 14, 48],
|
|
80
|
+
[28, 6, 45, 14, 46],
|
|
81
|
+
[28, 8, 47, 13, 48],
|
|
82
|
+
[28, 19, 46, 4, 47],
|
|
83
|
+
[28, 22, 45, 3, 46],
|
|
84
|
+
[28, 3, 45, 23, 46],
|
|
85
|
+
[28, 21, 45, 7, 46],
|
|
86
|
+
[28, 19, 47, 10, 48],
|
|
87
|
+
[28, 2, 46, 29, 47],
|
|
88
|
+
[28, 10, 46, 23, 47],
|
|
89
|
+
[28, 14, 46, 21, 47],
|
|
90
|
+
[28, 14, 46, 23, 47],
|
|
91
|
+
[28, 12, 47, 26, 48],
|
|
92
|
+
[28, 6, 47, 34, 48],
|
|
93
|
+
[28, 29, 46, 14, 47],
|
|
94
|
+
[28, 13, 46, 32, 47],
|
|
95
|
+
[28, 40, 47, 7, 48],
|
|
96
|
+
[28, 18, 47, 31, 48],
|
|
97
|
+
],
|
|
98
|
+
Q: [
|
|
99
|
+
[13, 1, 13, 0, 0],
|
|
100
|
+
[22, 1, 22, 0, 0],
|
|
101
|
+
[18, 2, 17, 0, 0],
|
|
102
|
+
[26, 2, 24, 0, 0],
|
|
103
|
+
[18, 2, 15, 2, 16],
|
|
104
|
+
[24, 4, 19, 0, 0],
|
|
105
|
+
[18, 2, 14, 4, 15],
|
|
106
|
+
[22, 4, 18, 2, 19],
|
|
107
|
+
[20, 4, 16, 4, 17],
|
|
108
|
+
[24, 6, 19, 2, 20],
|
|
109
|
+
[28, 4, 22, 4, 23],
|
|
110
|
+
[26, 4, 20, 6, 21],
|
|
111
|
+
[24, 8, 20, 4, 21],
|
|
112
|
+
[20, 11, 16, 5, 17],
|
|
113
|
+
[30, 5, 24, 7, 25],
|
|
114
|
+
[24, 15, 19, 2, 20],
|
|
115
|
+
[28, 1, 22, 15, 23],
|
|
116
|
+
[28, 17, 22, 1, 23],
|
|
117
|
+
[26, 17, 21, 4, 22],
|
|
118
|
+
[30, 15, 24, 5, 25],
|
|
119
|
+
[28, 17, 22, 6, 23],
|
|
120
|
+
[30, 7, 24, 16, 25],
|
|
121
|
+
[30, 11, 24, 14, 25],
|
|
122
|
+
[30, 11, 24, 16, 25],
|
|
123
|
+
[30, 7, 24, 22, 25],
|
|
124
|
+
[28, 28, 22, 6, 23],
|
|
125
|
+
[30, 8, 23, 26, 24],
|
|
126
|
+
[30, 4, 24, 31, 25],
|
|
127
|
+
[30, 1, 23, 37, 24],
|
|
128
|
+
[30, 15, 24, 25, 25],
|
|
129
|
+
[30, 42, 24, 1, 25],
|
|
130
|
+
[30, 10, 24, 35, 25],
|
|
131
|
+
[30, 29, 24, 19, 25],
|
|
132
|
+
[30, 44, 24, 7, 25],
|
|
133
|
+
[30, 39, 24, 14, 25],
|
|
134
|
+
[30, 46, 24, 10, 25],
|
|
135
|
+
[30, 49, 24, 10, 25],
|
|
136
|
+
[30, 48, 24, 14, 25],
|
|
137
|
+
[30, 43, 24, 22, 25],
|
|
138
|
+
[30, 34, 24, 34, 25],
|
|
139
|
+
],
|
|
140
|
+
H: [
|
|
141
|
+
[17, 1, 9, 0, 0],
|
|
142
|
+
[28, 1, 16, 0, 0],
|
|
143
|
+
[22, 2, 13, 0, 0],
|
|
144
|
+
[16, 4, 9, 0, 0],
|
|
145
|
+
[22, 2, 11, 2, 12],
|
|
146
|
+
[28, 4, 15, 0, 0],
|
|
147
|
+
[26, 4, 13, 1, 14],
|
|
148
|
+
[26, 4, 14, 2, 15],
|
|
149
|
+
[24, 4, 12, 4, 13],
|
|
150
|
+
[28, 6, 15, 2, 16],
|
|
151
|
+
[24, 3, 12, 8, 13],
|
|
152
|
+
[28, 7, 14, 4, 15],
|
|
153
|
+
[22, 12, 11, 4, 12],
|
|
154
|
+
[24, 11, 12, 5, 13],
|
|
155
|
+
[24, 11, 12, 7, 13],
|
|
156
|
+
[30, 3, 15, 13, 16],
|
|
157
|
+
[28, 2, 14, 17, 15],
|
|
158
|
+
[28, 2, 14, 19, 15],
|
|
159
|
+
[26, 9, 13, 16, 14],
|
|
160
|
+
[28, 15, 15, 10, 16],
|
|
161
|
+
[30, 19, 16, 6, 17],
|
|
162
|
+
[24, 34, 13, 0, 0],
|
|
163
|
+
[30, 16, 15, 14, 16],
|
|
164
|
+
[30, 30, 16, 2, 17],
|
|
165
|
+
[30, 22, 15, 13, 16],
|
|
166
|
+
[30, 33, 16, 4, 17],
|
|
167
|
+
[30, 12, 15, 28, 16],
|
|
168
|
+
[30, 11, 15, 31, 16],
|
|
169
|
+
[30, 19, 15, 26, 16],
|
|
170
|
+
[30, 23, 15, 25, 16],
|
|
171
|
+
[30, 23, 15, 28, 16],
|
|
172
|
+
[30, 19, 15, 35, 16],
|
|
173
|
+
[30, 11, 15, 46, 16],
|
|
174
|
+
[30, 59, 16, 1, 17],
|
|
175
|
+
[30, 22, 15, 41, 16],
|
|
176
|
+
[30, 2, 15, 64, 16],
|
|
177
|
+
[30, 24, 15, 46, 16],
|
|
178
|
+
[30, 42, 15, 32, 16],
|
|
179
|
+
[30, 10, 15, 67, 16],
|
|
180
|
+
[30, 20, 15, 61, 16],
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Total data codewords per version/level
|
|
185
|
+
function getDataCapacity(version: number, level: ECLevel): number {
|
|
186
|
+
const row = EC_TABLE[level][version - 1];
|
|
187
|
+
return row[1] * row[2] + row[3] * row[4];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Alignment pattern positions per version
|
|
191
|
+
const ALIGNMENT_POSITIONS: number[][] = [
|
|
192
|
+
[],
|
|
193
|
+
[], // v0 placeholder, v1
|
|
194
|
+
[6, 18],
|
|
195
|
+
[6, 22],
|
|
196
|
+
[6, 26],
|
|
197
|
+
[6, 30],
|
|
198
|
+
[6, 34],
|
|
199
|
+
[6, 22, 38],
|
|
200
|
+
[6, 24, 42],
|
|
201
|
+
[6, 26, 46],
|
|
202
|
+
[6, 28, 50],
|
|
203
|
+
[6, 30, 54],
|
|
204
|
+
[6, 32, 58],
|
|
205
|
+
[6, 34, 62],
|
|
206
|
+
[6, 26, 46, 66],
|
|
207
|
+
[6, 26, 48, 70],
|
|
208
|
+
[6, 26, 50, 74],
|
|
209
|
+
[6, 30, 54, 78],
|
|
210
|
+
[6, 30, 56, 82],
|
|
211
|
+
[6, 30, 58, 86],
|
|
212
|
+
[6, 34, 62, 90],
|
|
213
|
+
[6, 28, 50, 72, 94],
|
|
214
|
+
[6, 26, 50, 74, 98],
|
|
215
|
+
[6, 30, 54, 78, 102],
|
|
216
|
+
[6, 28, 54, 80, 106],
|
|
217
|
+
[6, 32, 58, 84, 110],
|
|
218
|
+
[6, 30, 58, 86, 114],
|
|
219
|
+
[6, 34, 62, 90, 118],
|
|
220
|
+
[6, 26, 50, 74, 98, 122],
|
|
221
|
+
[6, 30, 54, 78, 102, 126],
|
|
222
|
+
[6, 26, 52, 78, 104, 130],
|
|
223
|
+
[6, 30, 56, 82, 108, 134],
|
|
224
|
+
[6, 34, 60, 86, 112, 138],
|
|
225
|
+
[6, 30, 58, 86, 114, 142],
|
|
226
|
+
[6, 34, 62, 90, 118, 146],
|
|
227
|
+
[6, 30, 54, 78, 102, 126, 150],
|
|
228
|
+
[6, 24, 50, 76, 102, 128, 154],
|
|
229
|
+
[6, 28, 54, 80, 106, 132, 158],
|
|
230
|
+
[6, 32, 58, 84, 110, 136, 162],
|
|
231
|
+
[6, 26, 54, 82, 110, 138, 166],
|
|
232
|
+
[6, 30, 58, 86, 114, 142, 170],
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// GF(256) arithmetic for Reed-Solomon
|
|
236
|
+
const GF_EXP = new Uint8Array(512);
|
|
237
|
+
const GF_LOG = new Uint8Array(256);
|
|
238
|
+
{
|
|
239
|
+
let x = 1;
|
|
240
|
+
for (let i = 0; i < 255; i++) {
|
|
241
|
+
GF_EXP[i] = x;
|
|
242
|
+
GF_LOG[x] = i;
|
|
243
|
+
x = x << 1;
|
|
244
|
+
if (x >= 256) x ^= 0x11d;
|
|
245
|
+
}
|
|
246
|
+
for (let i = 255; i < 512; i++) {
|
|
247
|
+
GF_EXP[i] = GF_EXP[i - 255];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function gfMul(a: number, b: number): number {
|
|
252
|
+
if (a === 0 || b === 0) return 0;
|
|
253
|
+
return GF_EXP[GF_LOG[a] + GF_LOG[b]];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function rsGeneratorPoly(degree: number): Uint8Array {
|
|
257
|
+
let gen = new Uint8Array([1]);
|
|
258
|
+
for (let i = 0; i < degree; i++) {
|
|
259
|
+
const next = new Uint8Array(gen.length + 1);
|
|
260
|
+
for (let j = 0; j < gen.length; j++) {
|
|
261
|
+
next[j] ^= gen[j];
|
|
262
|
+
next[j + 1] ^= gfMul(gen[j], GF_EXP[i]);
|
|
263
|
+
}
|
|
264
|
+
gen = next;
|
|
265
|
+
}
|
|
266
|
+
return gen;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function rsEncode(data: Uint8Array, ec_count: number): Uint8Array {
|
|
270
|
+
const gen = rsGeneratorPoly(ec_count);
|
|
271
|
+
const remainder = new Uint8Array(ec_count);
|
|
272
|
+
for (let i = 0; i < data.length; i++) {
|
|
273
|
+
const factor = data[i] ^ remainder[0];
|
|
274
|
+
// Shift remainder left
|
|
275
|
+
for (let j = 0; j < ec_count - 1; j++) {
|
|
276
|
+
remainder[j] = remainder[j + 1];
|
|
277
|
+
}
|
|
278
|
+
remainder[ec_count - 1] = 0;
|
|
279
|
+
for (let j = 0; j < gen.length - 1; j++) {
|
|
280
|
+
remainder[j] ^= gfMul(gen[j + 1], factor);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return remainder;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Encode data in byte mode, return bits
|
|
287
|
+
function encodeData(text: string, version: number, level: ECLevel): Uint8Array {
|
|
288
|
+
const capacity = getDataCapacity(version, level);
|
|
289
|
+
const encoder = new TextEncoder();
|
|
290
|
+
const bytes = encoder.encode(text);
|
|
291
|
+
|
|
292
|
+
// Build bit stream: mode(4) + count(8 or 16) + data + terminator + padding
|
|
293
|
+
const count_bits = version <= 9 ? 8 : 16;
|
|
294
|
+
const bits: number[] = [];
|
|
295
|
+
|
|
296
|
+
function pushBits(value: number, length: number) {
|
|
297
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
298
|
+
bits.push((value >> i) & 1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Mode indicator: 0100 = byte mode
|
|
303
|
+
pushBits(0b0100, 4);
|
|
304
|
+
// Character count
|
|
305
|
+
pushBits(bytes.length, count_bits);
|
|
306
|
+
// Data bytes
|
|
307
|
+
for (const b of bytes) {
|
|
308
|
+
pushBits(b, 8);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Terminator (up to 4 zeros)
|
|
312
|
+
const total_bits = capacity * 8;
|
|
313
|
+
const term = Math.min(4, total_bits - bits.length);
|
|
314
|
+
for (let i = 0; i < term; i++) bits.push(0);
|
|
315
|
+
|
|
316
|
+
// Pad to byte boundary
|
|
317
|
+
while (bits.length % 8 !== 0) bits.push(0);
|
|
318
|
+
|
|
319
|
+
// Pad codewords
|
|
320
|
+
const pad_bytes = [0xec, 0x11];
|
|
321
|
+
let pad_idx = 0;
|
|
322
|
+
while (bits.length < total_bits) {
|
|
323
|
+
pushBits(pad_bytes[pad_idx % 2], 8);
|
|
324
|
+
pad_idx++;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Convert to codewords
|
|
328
|
+
const codewords = new Uint8Array(capacity);
|
|
329
|
+
for (let i = 0; i < capacity; i++) {
|
|
330
|
+
let byte = 0;
|
|
331
|
+
for (let b = 0; b < 8; b++) {
|
|
332
|
+
byte = (byte << 1) | (bits[i * 8 + b] || 0);
|
|
333
|
+
}
|
|
334
|
+
codewords[i] = byte;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return codewords;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function interleaveBlocks(
|
|
341
|
+
codewords: Uint8Array,
|
|
342
|
+
version: number,
|
|
343
|
+
level: ECLevel,
|
|
344
|
+
): number[] {
|
|
345
|
+
const row = EC_TABLE[level][version - 1];
|
|
346
|
+
const ec_per_block = row[0];
|
|
347
|
+
const g1_blocks = row[1];
|
|
348
|
+
const g1_data = row[2];
|
|
349
|
+
const g2_blocks = row[3];
|
|
350
|
+
const g2_data = row[4];
|
|
351
|
+
|
|
352
|
+
type Block = { data: Uint8Array; ec: Uint8Array };
|
|
353
|
+
const blocks: Block[] = [];
|
|
354
|
+
let offset = 0;
|
|
355
|
+
|
|
356
|
+
for (let i = 0; i < g1_blocks; i++) {
|
|
357
|
+
const data = codewords.slice(offset, offset + g1_data);
|
|
358
|
+
offset += g1_data;
|
|
359
|
+
blocks.push({ data, ec: rsEncode(data, ec_per_block) });
|
|
360
|
+
}
|
|
361
|
+
for (let i = 0; i < g2_blocks; i++) {
|
|
362
|
+
const data = codewords.slice(offset, offset + g2_data);
|
|
363
|
+
offset += g2_data;
|
|
364
|
+
blocks.push({ data, ec: rsEncode(data, ec_per_block) });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Interleave data codewords
|
|
368
|
+
const result: number[] = [];
|
|
369
|
+
const max_data = Math.max(g1_data, g2_data);
|
|
370
|
+
for (let i = 0; i < max_data; i++) {
|
|
371
|
+
for (const block of blocks) {
|
|
372
|
+
if (i < block.data.length) {
|
|
373
|
+
result.push(block.data[i]);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Interleave EC codewords
|
|
378
|
+
for (let i = 0; i < ec_per_block; i++) {
|
|
379
|
+
for (const block of blocks) {
|
|
380
|
+
result.push(block.ec[i]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function chooseVersion(text: string, level: ECLevel): number {
|
|
388
|
+
const encoder = new TextEncoder();
|
|
389
|
+
const byte_length = encoder.encode(text).length;
|
|
390
|
+
for (let v = 1; v <= 40; v++) {
|
|
391
|
+
const count_bits = v <= 9 ? 8 : 16;
|
|
392
|
+
const data_bits = 4 + count_bits + byte_length * 8;
|
|
393
|
+
const capacity = getDataCapacity(v, level);
|
|
394
|
+
if (data_bits <= capacity * 8) return v;
|
|
395
|
+
}
|
|
396
|
+
return 40; // best effort
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function createMatrix(version: number): {
|
|
400
|
+
modules: boolean[][];
|
|
401
|
+
reserved: boolean[][];
|
|
402
|
+
} {
|
|
403
|
+
const size = version * 4 + 17;
|
|
404
|
+
const modules = Array.from({ length: size }, () => Array(size).fill(false));
|
|
405
|
+
const reserved = Array.from({ length: size }, () => Array(size).fill(false));
|
|
406
|
+
return { modules, reserved };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function placeFinderPattern(
|
|
410
|
+
modules: boolean[][],
|
|
411
|
+
reserved: boolean[][],
|
|
412
|
+
row: number,
|
|
413
|
+
col: number,
|
|
414
|
+
) {
|
|
415
|
+
for (let r = -1; r <= 7; r++) {
|
|
416
|
+
for (let c = -1; c <= 7; c++) {
|
|
417
|
+
const rr = row + r;
|
|
418
|
+
const cc = col + c;
|
|
419
|
+
if (rr < 0 || rr >= modules.length || cc < 0 || cc >= modules.length) continue;
|
|
420
|
+
const is_border = r === -1 || r === 7 || c === -1 || c === 7;
|
|
421
|
+
const is_outer = r === 0 || r === 6 || c === 0 || c === 6;
|
|
422
|
+
const is_inner = r >= 2 && r <= 4 && c >= 2 && c <= 4;
|
|
423
|
+
modules[rr][cc] = is_outer || is_inner;
|
|
424
|
+
if (!is_border) {
|
|
425
|
+
reserved[rr][cc] = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function placeAlignmentPattern(
|
|
432
|
+
modules: boolean[][],
|
|
433
|
+
reserved: boolean[][],
|
|
434
|
+
row: number,
|
|
435
|
+
col: number,
|
|
436
|
+
) {
|
|
437
|
+
for (let r = -2; r <= 2; r++) {
|
|
438
|
+
for (let c = -2; c <= 2; c++) {
|
|
439
|
+
const rr = row + r;
|
|
440
|
+
const cc = col + c;
|
|
441
|
+
if (reserved[rr][cc]) return; // Overlaps finder, skip entirely
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (let r = -2; r <= 2; r++) {
|
|
445
|
+
for (let c = -2; c <= 2; c++) {
|
|
446
|
+
const rr = row + r;
|
|
447
|
+
const cc = col + c;
|
|
448
|
+
const is_outer = Math.abs(r) === 2 || Math.abs(c) === 2;
|
|
449
|
+
const is_center = r === 0 && c === 0;
|
|
450
|
+
modules[rr][cc] = is_outer || is_center;
|
|
451
|
+
reserved[rr][cc] = true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function placeTimingPatterns(modules: boolean[][], reserved: boolean[][]) {
|
|
457
|
+
const size = modules.length;
|
|
458
|
+
for (let i = 8; i < size - 8; i++) {
|
|
459
|
+
// Horizontal
|
|
460
|
+
if (!reserved[6][i]) {
|
|
461
|
+
modules[6][i] = i % 2 === 0;
|
|
462
|
+
reserved[6][i] = true;
|
|
463
|
+
}
|
|
464
|
+
// Vertical
|
|
465
|
+
if (!reserved[i][6]) {
|
|
466
|
+
modules[i][6] = i % 2 === 0;
|
|
467
|
+
reserved[i][6] = true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function reserveFormatArea(reserved: boolean[][], version: number) {
|
|
473
|
+
const size = reserved.length;
|
|
474
|
+
// Around top-left finder
|
|
475
|
+
for (let i = 0; i < 9; i++) {
|
|
476
|
+
reserved[8][i] = true;
|
|
477
|
+
reserved[i][8] = true;
|
|
478
|
+
}
|
|
479
|
+
// Around top-right finder
|
|
480
|
+
for (let i = 0; i < 8; i++) {
|
|
481
|
+
reserved[8][size - 1 - i] = true;
|
|
482
|
+
}
|
|
483
|
+
// Around bottom-left finder
|
|
484
|
+
for (let i = 0; i < 7; i++) {
|
|
485
|
+
reserved[size - 1 - i][8] = true;
|
|
486
|
+
}
|
|
487
|
+
// Dark module
|
|
488
|
+
reserved[size - 8][8] = true;
|
|
489
|
+
|
|
490
|
+
// Version info areas (versions >= 7)
|
|
491
|
+
if (version >= 7) {
|
|
492
|
+
for (let i = 0; i < 6; i++) {
|
|
493
|
+
for (let j = 0; j < 3; j++) {
|
|
494
|
+
reserved[i][size - 11 + j] = true;
|
|
495
|
+
reserved[size - 11 + j][i] = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function placeDataBits(
|
|
502
|
+
modules: boolean[][],
|
|
503
|
+
reserved: boolean[][],
|
|
504
|
+
data_bits: number[],
|
|
505
|
+
) {
|
|
506
|
+
const size = modules.length;
|
|
507
|
+
let bit_idx = 0;
|
|
508
|
+
// Data is placed in 2-column strips from right to left
|
|
509
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
510
|
+
// Skip column 6 (timing pattern)
|
|
511
|
+
let col = right;
|
|
512
|
+
if (col <= 6) col--;
|
|
513
|
+
|
|
514
|
+
// Alternate upward and downward
|
|
515
|
+
const is_upward = ((size - 1 - right) / 2) % 2 === 0;
|
|
516
|
+
const rows = is_upward
|
|
517
|
+
? Array.from({ length: size }, (_, i) => size - 1 - i)
|
|
518
|
+
: Array.from({ length: size }, (_, i) => i);
|
|
519
|
+
|
|
520
|
+
for (const row of rows) {
|
|
521
|
+
for (let dc = 0; dc <= 1; dc++) {
|
|
522
|
+
const c = col - dc;
|
|
523
|
+
if (c < 0) continue;
|
|
524
|
+
if (reserved[row][c]) continue;
|
|
525
|
+
if (bit_idx < data_bits.length) {
|
|
526
|
+
modules[row][c] = data_bits[bit_idx] === 1;
|
|
527
|
+
bit_idx++;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Mask patterns
|
|
535
|
+
const MASK_FUNCTIONS: ((r: number, c: number) => boolean)[] = [
|
|
536
|
+
(r, c) => (r + c) % 2 === 0,
|
|
537
|
+
(r, _c) => r % 2 === 0,
|
|
538
|
+
(_r, c) => c % 3 === 0,
|
|
539
|
+
(r, c) => (r + c) % 3 === 0,
|
|
540
|
+
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
|
|
541
|
+
(r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
|
|
542
|
+
(r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
543
|
+
(r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
function applyMask(
|
|
547
|
+
modules: boolean[][],
|
|
548
|
+
reserved: boolean[][],
|
|
549
|
+
mask_idx: number,
|
|
550
|
+
): boolean[][] {
|
|
551
|
+
const size = modules.length;
|
|
552
|
+
const result = modules.map((row) => [...row]);
|
|
553
|
+
const fn = MASK_FUNCTIONS[mask_idx];
|
|
554
|
+
for (let r = 0; r < size; r++) {
|
|
555
|
+
for (let c = 0; c < size; c++) {
|
|
556
|
+
if (!reserved[r][c]) {
|
|
557
|
+
if (fn(r, c)) {
|
|
558
|
+
result[r][c] = !result[r][c];
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function writeFormatBits(
|
|
567
|
+
modules: boolean[][],
|
|
568
|
+
version: number,
|
|
569
|
+
level: ECLevel,
|
|
570
|
+
mask_idx: number,
|
|
571
|
+
) {
|
|
572
|
+
const size = modules.length;
|
|
573
|
+
const ec_bits = EC_LEVEL_BITS[level];
|
|
574
|
+
let data = (ec_bits << 3) | mask_idx;
|
|
575
|
+
|
|
576
|
+
// Calculate BCH(15,5) error correction
|
|
577
|
+
let rem = data;
|
|
578
|
+
for (let i = 0; i < 10; i++) {
|
|
579
|
+
rem = (rem << 1) ^ ((rem >> 9) * 0x537);
|
|
580
|
+
}
|
|
581
|
+
const format_bits = ((data << 10) | rem) ^ 0x5412;
|
|
582
|
+
|
|
583
|
+
// Place format bits
|
|
584
|
+
for (let i = 0; i < 15; i++) {
|
|
585
|
+
const bit = ((format_bits >> (14 - i)) & 1) === 1;
|
|
586
|
+
|
|
587
|
+
// Top-left
|
|
588
|
+
if (i < 6) {
|
|
589
|
+
modules[8][i] = bit;
|
|
590
|
+
} else if (i === 6) {
|
|
591
|
+
modules[8][7] = bit;
|
|
592
|
+
} else if (i === 7) {
|
|
593
|
+
modules[8][8] = bit;
|
|
594
|
+
} else if (i === 8) {
|
|
595
|
+
modules[7][8] = bit;
|
|
596
|
+
} else {
|
|
597
|
+
modules[14 - i][8] = bit;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Other two strips
|
|
601
|
+
if (i < 8) {
|
|
602
|
+
modules[size - 1 - i][8] = bit;
|
|
603
|
+
} else {
|
|
604
|
+
modules[8][size - 15 + i] = bit;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Dark module
|
|
609
|
+
modules[size - 8][8] = true;
|
|
610
|
+
|
|
611
|
+
// Version info (version >= 7)
|
|
612
|
+
if (version >= 7) {
|
|
613
|
+
let ver_rem = version;
|
|
614
|
+
for (let i = 0; i < 12; i++) {
|
|
615
|
+
ver_rem = (ver_rem << 1) ^ ((ver_rem >> 11) * 0x1f25);
|
|
616
|
+
}
|
|
617
|
+
const ver_bits = (version << 12) | ver_rem;
|
|
618
|
+
for (let i = 0; i < 18; i++) {
|
|
619
|
+
const bit = ((ver_bits >> i) & 1) === 1;
|
|
620
|
+
const row = Math.floor(i / 3);
|
|
621
|
+
const col = i % 3;
|
|
622
|
+
modules[row][size - 11 + col] = bit;
|
|
623
|
+
modules[size - 11 + col][row] = bit;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Penalty scoring for mask selection
|
|
629
|
+
function scoreMask(modules: boolean[][]): number {
|
|
630
|
+
const size = modules.length;
|
|
631
|
+
let penalty = 0;
|
|
632
|
+
|
|
633
|
+
// Rule 1: consecutive same-color modules in row/col
|
|
634
|
+
for (let r = 0; r < size; r++) {
|
|
635
|
+
let run = 1;
|
|
636
|
+
for (let c = 1; c < size; c++) {
|
|
637
|
+
if (modules[r][c] === modules[r][c - 1]) {
|
|
638
|
+
run++;
|
|
639
|
+
} else {
|
|
640
|
+
if (run >= 5) penalty += run - 2;
|
|
641
|
+
run = 1;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (run >= 5) penalty += run - 2;
|
|
645
|
+
}
|
|
646
|
+
for (let c = 0; c < size; c++) {
|
|
647
|
+
let run = 1;
|
|
648
|
+
for (let r = 1; r < size; r++) {
|
|
649
|
+
if (modules[r][c] === modules[r - 1][c]) {
|
|
650
|
+
run++;
|
|
651
|
+
} else {
|
|
652
|
+
if (run >= 5) penalty += run - 2;
|
|
653
|
+
run = 1;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (run >= 5) penalty += run - 2;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Rule 2: 2x2 blocks of same color
|
|
660
|
+
for (let r = 0; r < size - 1; r++) {
|
|
661
|
+
for (let c = 0; c < size - 1; c++) {
|
|
662
|
+
const v = modules[r][c];
|
|
663
|
+
if (
|
|
664
|
+
v === modules[r][c + 1] &&
|
|
665
|
+
v === modules[r + 1][c] &&
|
|
666
|
+
v === modules[r + 1][c + 1]
|
|
667
|
+
) {
|
|
668
|
+
penalty += 3;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Rule 3: finder-like patterns
|
|
674
|
+
const pattern_a = [
|
|
675
|
+
true,
|
|
676
|
+
false,
|
|
677
|
+
true,
|
|
678
|
+
true,
|
|
679
|
+
true,
|
|
680
|
+
false,
|
|
681
|
+
true,
|
|
682
|
+
false,
|
|
683
|
+
false,
|
|
684
|
+
false,
|
|
685
|
+
false,
|
|
686
|
+
];
|
|
687
|
+
const pattern_b = [...pattern_a].reverse();
|
|
688
|
+
for (let r = 0; r < size; r++) {
|
|
689
|
+
for (let c = 0; c <= size - 11; c++) {
|
|
690
|
+
let match_a = true;
|
|
691
|
+
let match_b = true;
|
|
692
|
+
for (let i = 0; i < 11; i++) {
|
|
693
|
+
if (modules[r][c + i] !== pattern_a[i]) match_a = false;
|
|
694
|
+
if (modules[r][c + i] !== pattern_b[i]) match_b = false;
|
|
695
|
+
}
|
|
696
|
+
if (match_a || match_b) penalty += 40;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
for (let c = 0; c < size; c++) {
|
|
700
|
+
for (let r = 0; r <= size - 11; r++) {
|
|
701
|
+
let match_a = true;
|
|
702
|
+
let match_b = true;
|
|
703
|
+
for (let i = 0; i < 11; i++) {
|
|
704
|
+
if (modules[r + i][c] !== pattern_a[i]) match_a = false;
|
|
705
|
+
if (modules[r + i][c] !== pattern_b[i]) match_b = false;
|
|
706
|
+
}
|
|
707
|
+
if (match_a || match_b) penalty += 40;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Rule 4: proportion of dark modules
|
|
712
|
+
let dark = 0;
|
|
713
|
+
for (let r = 0; r < size; r++) {
|
|
714
|
+
for (let c = 0; c < size; c++) {
|
|
715
|
+
if (modules[r][c]) dark++;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const total = size * size;
|
|
719
|
+
const pct = (dark / total) * 100;
|
|
720
|
+
const prev5 = Math.floor(pct / 5) * 5;
|
|
721
|
+
const next5 = prev5 + 5;
|
|
722
|
+
penalty += Math.min(Math.abs(prev5 - 50) / 5, Math.abs(next5 - 50) / 5) * 10;
|
|
723
|
+
|
|
724
|
+
return penalty;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Generate a QR code matrix from text data.
|
|
729
|
+
* Returns a 2D boolean array where `true` = dark module.
|
|
730
|
+
*/
|
|
731
|
+
function generateQRMatrix(text: string, level: ECLevel): boolean[][] {
|
|
732
|
+
const version = chooseVersion(text, level);
|
|
733
|
+
const size = version * 4 + 17;
|
|
734
|
+
|
|
735
|
+
// Encode data
|
|
736
|
+
const codewords = encodeData(text, version, level);
|
|
737
|
+
const interleaved = interleaveBlocks(codewords, version, level);
|
|
738
|
+
|
|
739
|
+
// Convert to bits
|
|
740
|
+
const data_bits: number[] = [];
|
|
741
|
+
for (const byte of interleaved) {
|
|
742
|
+
for (let i = 7; i >= 0; i--) {
|
|
743
|
+
data_bits.push((byte >> i) & 1);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Build matrix
|
|
748
|
+
const { modules, reserved } = createMatrix(version);
|
|
749
|
+
|
|
750
|
+
// Finder patterns
|
|
751
|
+
placeFinderPattern(modules, reserved, 0, 0);
|
|
752
|
+
placeFinderPattern(modules, reserved, 0, size - 7);
|
|
753
|
+
placeFinderPattern(modules, reserved, size - 7, 0);
|
|
754
|
+
|
|
755
|
+
// Alignment patterns
|
|
756
|
+
if (version >= 2) {
|
|
757
|
+
const positions = ALIGNMENT_POSITIONS[version];
|
|
758
|
+
for (const r of positions) {
|
|
759
|
+
for (const c of positions) {
|
|
760
|
+
placeAlignmentPattern(modules, reserved, r, c);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Timing patterns
|
|
766
|
+
placeTimingPatterns(modules, reserved);
|
|
767
|
+
|
|
768
|
+
// Reserve format/version areas
|
|
769
|
+
reserveFormatArea(reserved, version);
|
|
770
|
+
|
|
771
|
+
// Place data
|
|
772
|
+
placeDataBits(modules, reserved, data_bits);
|
|
773
|
+
|
|
774
|
+
// Try all masks, pick best
|
|
775
|
+
let best_mask = 0;
|
|
776
|
+
let best_score = Infinity;
|
|
777
|
+
let best_modules: boolean[][] = modules;
|
|
778
|
+
|
|
779
|
+
for (let m = 0; m < 8; m++) {
|
|
780
|
+
const masked = applyMask(modules, reserved, m);
|
|
781
|
+
// Write format bits to a copy
|
|
782
|
+
const copy = masked.map((row) => [...row]);
|
|
783
|
+
writeFormatBits(copy, version, level, m);
|
|
784
|
+
const score = scoreMask(copy);
|
|
785
|
+
if (score < best_score) {
|
|
786
|
+
best_score = score;
|
|
787
|
+
best_mask = m;
|
|
788
|
+
best_modules = copy;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// If no best was found (shouldn't happen), use mask 0
|
|
793
|
+
if (best_modules === modules) {
|
|
794
|
+
const masked = applyMask(modules, reserved, best_mask);
|
|
795
|
+
writeFormatBits(masked, version, level, best_mask);
|
|
796
|
+
best_modules = masked;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return best_modules;
|
|
800
|
+
}
|
|
801
|
+
</script>
|
|
802
|
+
|
|
803
|
+
<script lang="ts">
|
|
804
|
+
const propId = $props.id();
|
|
805
|
+
let {
|
|
806
|
+
/** The data to encode (URL, text, etc.) */
|
|
807
|
+
value,
|
|
808
|
+
|
|
809
|
+
/** The pixel size of the rendered QR code */
|
|
810
|
+
size = 200,
|
|
811
|
+
|
|
812
|
+
/** Error correction level */
|
|
813
|
+
level = 'M' as ECLevel,
|
|
814
|
+
|
|
815
|
+
/** Foreground (dark module) color */
|
|
816
|
+
foreground = '#000000',
|
|
817
|
+
|
|
818
|
+
/** Background (light module) color */
|
|
819
|
+
background = '#ffffff',
|
|
820
|
+
|
|
821
|
+
/** Quiet zone margin in modules around the QR code */
|
|
822
|
+
margin = 4,
|
|
823
|
+
|
|
824
|
+
/** Optional logo image URL to overlay in the center */
|
|
825
|
+
logo = undefined as string | undefined,
|
|
826
|
+
|
|
827
|
+
/** Logo size as a fraction of the QR code size (0 to 1) */
|
|
828
|
+
logo_size = 0.25,
|
|
829
|
+
|
|
830
|
+
/** Use rounded module shapes */
|
|
831
|
+
rounded = false,
|
|
832
|
+
|
|
833
|
+
/** Show a download button */
|
|
834
|
+
downloadable = false,
|
|
835
|
+
|
|
836
|
+
/** Filename for the downloaded PNG (without extension) */
|
|
837
|
+
download_filename = 'qr-code',
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Show a skeleton loading state while `value` is not yet available.
|
|
841
|
+
* It dismisses itself as soon as the QR code can be rendered.
|
|
842
|
+
*/
|
|
843
|
+
skeleton = false,
|
|
844
|
+
|
|
845
|
+
/** Element ID */
|
|
846
|
+
id = propId,
|
|
847
|
+
|
|
848
|
+
/** Additional CSS classes */
|
|
849
|
+
class: class_name = '',
|
|
850
|
+
}: {
|
|
851
|
+
value?: string;
|
|
852
|
+
size?: number;
|
|
853
|
+
level?: ECLevel;
|
|
854
|
+
foreground?: string;
|
|
855
|
+
background?: string;
|
|
856
|
+
margin?: number;
|
|
857
|
+
logo?: string;
|
|
858
|
+
logo_size?: number;
|
|
859
|
+
rounded?: boolean;
|
|
860
|
+
downloadable?: boolean;
|
|
861
|
+
download_filename?: string;
|
|
862
|
+
skeleton?: boolean;
|
|
863
|
+
id?: string;
|
|
864
|
+
class?: string;
|
|
865
|
+
} = $props();
|
|
866
|
+
|
|
867
|
+
// Auto-upgrade EC level to H when logo is present (logo obscures center modules)
|
|
868
|
+
const effective_level = $derived<ECLevel>(logo ? 'H' : level);
|
|
869
|
+
|
|
870
|
+
const matrix = $derived(value ? generateQRMatrix(value, effective_level) : null);
|
|
871
|
+
const module_count = $derived(matrix ? matrix.length : 0);
|
|
872
|
+
const total_modules = $derived(module_count + margin * 2);
|
|
873
|
+
const viewbox = $derived(`0 0 ${total_modules} ${total_modules}`);
|
|
874
|
+
const radius = $derived(rounded ? 0.5 : 0);
|
|
875
|
+
|
|
876
|
+
// Logo dimensions (in viewBox units)
|
|
877
|
+
const logo_modules = $derived(Math.floor(module_count * logo_size));
|
|
878
|
+
const logo_offset = $derived(margin + Math.floor((module_count - logo_modules) / 2));
|
|
879
|
+
|
|
880
|
+
let is_downloading = $state(false);
|
|
881
|
+
let logo_loaded = $state(!logo);
|
|
882
|
+
let logo_error = $state(false);
|
|
883
|
+
|
|
884
|
+
$effect(() => {
|
|
885
|
+
if (logo) {
|
|
886
|
+
logo_loaded = false;
|
|
887
|
+
logo_error = false;
|
|
888
|
+
} else {
|
|
889
|
+
logo_loaded = true;
|
|
890
|
+
logo_error = false;
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
function handleLogoLoad() {
|
|
895
|
+
logo_loaded = true;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function handleLogoError() {
|
|
899
|
+
logo_error = true;
|
|
900
|
+
logo_loaded = true;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Trigger a PNG download of the QR code. Consumers can call this to wire
|
|
904
|
+
* up their own download button. Returns a promise that resolves when the
|
|
905
|
+
* download has been initiated. */
|
|
906
|
+
export async function triggerDownload(filename?: string): Promise<void> {
|
|
907
|
+
await handleDownload(filename);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Load the logo for canvas export. Uses an anonymous CORS request so a
|
|
911
|
+
* successfully-loaded image never taints the canvas; resolves null on any
|
|
912
|
+
* failure so export can proceed without the logo. */
|
|
913
|
+
function loadLogoForExport(src: string): Promise<HTMLImageElement | null> {
|
|
914
|
+
return new Promise((resolve) => {
|
|
915
|
+
const img = new Image();
|
|
916
|
+
img.crossOrigin = 'anonymous';
|
|
917
|
+
img.onload = () => resolve(img);
|
|
918
|
+
img.onerror = () => resolve(null);
|
|
919
|
+
img.src = src;
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async function handleDownload(filenameOverride?: string) {
|
|
924
|
+
const current = matrix;
|
|
925
|
+
if (is_downloading || !current) return;
|
|
926
|
+
is_downloading = true;
|
|
927
|
+
const filename = filenameOverride ?? download_filename;
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
// Rasterise the QR directly from the matrix instead of serialising the
|
|
931
|
+
// <svg> and loading it through an <img>. The latter taints the canvas
|
|
932
|
+
// in several browsers, which makes canvas.toBlob() silently never fire
|
|
933
|
+
// its callback — the download promise never settles and the button
|
|
934
|
+
// spins forever. Drawing rects keeps the canvas clean.
|
|
935
|
+
const px = Math.max(4, Math.round((size * 2) / total_modules));
|
|
936
|
+
const dim = total_modules * px;
|
|
937
|
+
const canvas = document.createElement('canvas');
|
|
938
|
+
canvas.width = dim;
|
|
939
|
+
canvas.height = dim;
|
|
940
|
+
const ctx = canvas.getContext('2d');
|
|
941
|
+
if (!ctx) return;
|
|
942
|
+
|
|
943
|
+
// Background / quiet zone
|
|
944
|
+
ctx.fillStyle = background;
|
|
945
|
+
ctx.fillRect(0, 0, dim, dim);
|
|
946
|
+
|
|
947
|
+
// Modules
|
|
948
|
+
ctx.fillStyle = foreground;
|
|
949
|
+
const supports_round = typeof ctx.roundRect === 'function';
|
|
950
|
+
const r_px = rounded ? Math.min(px / 2, radius * px) : 0;
|
|
951
|
+
for (let r = 0; r < current.length; r++) {
|
|
952
|
+
const row = current[r];
|
|
953
|
+
for (let c = 0; c < row.length; c++) {
|
|
954
|
+
if (!row[c]) continue;
|
|
955
|
+
const x = (c + margin) * px;
|
|
956
|
+
const y = (r + margin) * px;
|
|
957
|
+
if (rounded && supports_round) {
|
|
958
|
+
ctx.beginPath();
|
|
959
|
+
ctx.roundRect(x, y, px, px, r_px);
|
|
960
|
+
ctx.fill();
|
|
961
|
+
} else {
|
|
962
|
+
ctx.fillRect(x, y, px, px);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Optional centre logo (best effort — skipped if it can't be loaded
|
|
968
|
+
// CORS-clean, so the canvas is never tainted)
|
|
969
|
+
if (logo && !logo_error) {
|
|
970
|
+
const logo_img = await loadLogoForExport(logo);
|
|
971
|
+
if (logo_img) {
|
|
972
|
+
ctx.fillStyle = background;
|
|
973
|
+
ctx.fillRect(
|
|
974
|
+
(logo_offset - 1) * px,
|
|
975
|
+
(logo_offset - 1) * px,
|
|
976
|
+
(logo_modules + 2) * px,
|
|
977
|
+
(logo_modules + 2) * px,
|
|
978
|
+
);
|
|
979
|
+
ctx.drawImage(
|
|
980
|
+
logo_img,
|
|
981
|
+
logo_offset * px,
|
|
982
|
+
logo_offset * px,
|
|
983
|
+
logo_modules * px,
|
|
984
|
+
logo_modules * px,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const blob = await new Promise<Blob | null>((resolve) =>
|
|
990
|
+
canvas.toBlob((b) => resolve(b), 'image/png'),
|
|
991
|
+
);
|
|
992
|
+
if (!blob) return;
|
|
993
|
+
|
|
994
|
+
const download_url = URL.createObjectURL(blob);
|
|
995
|
+
const a = document.createElement('a');
|
|
996
|
+
a.href = download_url;
|
|
997
|
+
a.download = `${filename}.png`;
|
|
998
|
+
document.body.appendChild(a);
|
|
999
|
+
a.click();
|
|
1000
|
+
document.body.removeChild(a);
|
|
1001
|
+
URL.revokeObjectURL(download_url);
|
|
1002
|
+
} finally {
|
|
1003
|
+
is_downloading = false;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
</script>
|
|
1007
|
+
|
|
1008
|
+
{#if skeleton && !matrix}
|
|
1009
|
+
<div
|
|
1010
|
+
class={['qr', 'skeleton', class_name].filter(Boolean).join(' ')}
|
|
1011
|
+
{id}
|
|
1012
|
+
style:--qr-size="{size}px"
|
|
1013
|
+
role="img"
|
|
1014
|
+
aria-label="Loading QR code">
|
|
1015
|
+
<div class="skeleton-inner">
|
|
1016
|
+
<!-- Faint finder-pattern squares so the placeholder reads as
|
|
1017
|
+
"a QR code will appear here", not just a gray box. -->
|
|
1018
|
+
<div class="skeleton-finder tl"></div>
|
|
1019
|
+
<div class="skeleton-finder tr"></div>
|
|
1020
|
+
<div class="skeleton-finder bl"></div>
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
{:else if matrix}
|
|
1024
|
+
<div
|
|
1025
|
+
class={['qr', class_name].filter(Boolean).join(' ')}
|
|
1026
|
+
{id}
|
|
1027
|
+
style:--qr-size="{size}px"
|
|
1028
|
+
role="img"
|
|
1029
|
+
aria-label="QR code for {value}">
|
|
1030
|
+
<svg
|
|
1031
|
+
id="{id}-svg"
|
|
1032
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1033
|
+
viewBox={viewbox}
|
|
1034
|
+
width={size}
|
|
1035
|
+
height={size}
|
|
1036
|
+
shape-rendering={rounded ? 'auto' : 'crispEdges'}>
|
|
1037
|
+
<!-- Background -->
|
|
1038
|
+
<rect width={total_modules} height={total_modules} fill={background} />
|
|
1039
|
+
|
|
1040
|
+
<!-- QR modules -->
|
|
1041
|
+
{#each matrix as row, r}
|
|
1042
|
+
{#each row as cell, c}
|
|
1043
|
+
{#if cell}
|
|
1044
|
+
{#if rounded}
|
|
1045
|
+
<rect
|
|
1046
|
+
x={c + margin}
|
|
1047
|
+
y={r + margin}
|
|
1048
|
+
width={1}
|
|
1049
|
+
height={1}
|
|
1050
|
+
rx={radius}
|
|
1051
|
+
ry={radius}
|
|
1052
|
+
fill={foreground} />
|
|
1053
|
+
{:else}
|
|
1054
|
+
<rect
|
|
1055
|
+
x={c + margin}
|
|
1056
|
+
y={r + margin}
|
|
1057
|
+
width={1}
|
|
1058
|
+
height={1}
|
|
1059
|
+
fill={foreground} />
|
|
1060
|
+
{/if}
|
|
1061
|
+
{/if}
|
|
1062
|
+
{/each}
|
|
1063
|
+
{/each}
|
|
1064
|
+
|
|
1065
|
+
<!-- Logo overlay -->
|
|
1066
|
+
{#if logo && !logo_error}
|
|
1067
|
+
<!-- White background behind logo -->
|
|
1068
|
+
<rect
|
|
1069
|
+
x={logo_offset - 1}
|
|
1070
|
+
y={logo_offset - 1}
|
|
1071
|
+
width={logo_modules + 2}
|
|
1072
|
+
height={logo_modules + 2}
|
|
1073
|
+
rx={rounded ? 1 : 0}
|
|
1074
|
+
ry={rounded ? 1 : 0}
|
|
1075
|
+
fill={background} />
|
|
1076
|
+
<image
|
|
1077
|
+
href={logo}
|
|
1078
|
+
x={logo_offset}
|
|
1079
|
+
y={logo_offset}
|
|
1080
|
+
width={logo_modules}
|
|
1081
|
+
height={logo_modules}
|
|
1082
|
+
preserveAspectRatio="xMidYMid meet"
|
|
1083
|
+
onload={handleLogoLoad}
|
|
1084
|
+
onerror={handleLogoError} />
|
|
1085
|
+
{/if}
|
|
1086
|
+
</svg>
|
|
1087
|
+
</div>
|
|
1088
|
+
{/if}
|
|
1089
|
+
|
|
1090
|
+
<style>
|
|
1091
|
+
.qr {
|
|
1092
|
+
position: relative;
|
|
1093
|
+
display: inline-flex;
|
|
1094
|
+
flex-direction: column;
|
|
1095
|
+
align-items: center;
|
|
1096
|
+
gap: 8px;
|
|
1097
|
+
width: var(--qr-size);
|
|
1098
|
+
|
|
1099
|
+
svg {
|
|
1100
|
+
display: block;
|
|
1101
|
+
width: var(--qr-size);
|
|
1102
|
+
height: var(--qr-size);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/* ── Skeleton ─────────────────────────────────────────────────── */
|
|
1106
|
+
|
|
1107
|
+
&.skeleton {
|
|
1108
|
+
pointer-events: none;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.skeleton-inner {
|
|
1113
|
+
width: var(--qr-size);
|
|
1114
|
+
height: var(--qr-size);
|
|
1115
|
+
border-radius: var(--radius-lg, 8px);
|
|
1116
|
+
@supports (corner-shape: squircle) {
|
|
1117
|
+
corner-shape: squircle;
|
|
1118
|
+
border-radius: calc(var(--radius-lg, 8px) * var(--squircle-ratio, 2));
|
|
1119
|
+
}
|
|
1120
|
+
background: var(--skeleton-bg, rgb(from var(--color-text, #888) r g b / 0.1));
|
|
1121
|
+
position: relative;
|
|
1122
|
+
overflow: hidden;
|
|
1123
|
+
|
|
1124
|
+
/* The shimmer beam sweeps above the finder squares so it reads as a
|
|
1125
|
+
sheen passing over the whole (future) code. */
|
|
1126
|
+
&::after {
|
|
1127
|
+
content: '';
|
|
1128
|
+
position: absolute;
|
|
1129
|
+
inset: 0;
|
|
1130
|
+
z-index: 1;
|
|
1131
|
+
transform: translateX(-100%);
|
|
1132
|
+
background-image: linear-gradient(
|
|
1133
|
+
105deg,
|
|
1134
|
+
transparent 25%,
|
|
1135
|
+
var(--skeleton-sheen, rgb(from var(--color-text, #888) r g b / 0.12)) 50%,
|
|
1136
|
+
transparent 75%
|
|
1137
|
+
);
|
|
1138
|
+
animation: delight-skeleton-shimmer var(--skeleton-duration, 2.4s) ease-in-out
|
|
1139
|
+
infinite;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/* QR finder patterns: outer ring (border) + center dot (::after), at the
|
|
1144
|
+
three corners where a real code has them. Sized off --qr-size so they
|
|
1145
|
+
scale with the component. */
|
|
1146
|
+
.skeleton-finder {
|
|
1147
|
+
position: absolute;
|
|
1148
|
+
width: calc(var(--qr-size) * 0.18);
|
|
1149
|
+
height: calc(var(--qr-size) * 0.18);
|
|
1150
|
+
border: calc(var(--qr-size) * 0.026) solid
|
|
1151
|
+
rgb(from var(--color-text, #888) r g b / 0.1);
|
|
1152
|
+
border-radius: calc(var(--qr-size) * 0.02);
|
|
1153
|
+
|
|
1154
|
+
&::after {
|
|
1155
|
+
content: '';
|
|
1156
|
+
position: absolute;
|
|
1157
|
+
inset: calc(var(--qr-size) * 0.026);
|
|
1158
|
+
background: rgb(from var(--color-text, #888) r g b / 0.1);
|
|
1159
|
+
border-radius: calc(var(--qr-size) * 0.01);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
&.tl {
|
|
1163
|
+
top: calc(var(--qr-size) * 0.08);
|
|
1164
|
+
left: calc(var(--qr-size) * 0.08);
|
|
1165
|
+
}
|
|
1166
|
+
&.tr {
|
|
1167
|
+
top: calc(var(--qr-size) * 0.08);
|
|
1168
|
+
right: calc(var(--qr-size) * 0.08);
|
|
1169
|
+
}
|
|
1170
|
+
&.bl {
|
|
1171
|
+
bottom: calc(var(--qr-size) * 0.08);
|
|
1172
|
+
left: calc(var(--qr-size) * 0.08);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
@keyframes -global-delight-skeleton-shimmer {
|
|
1177
|
+
0% {
|
|
1178
|
+
transform: translateX(-100%);
|
|
1179
|
+
}
|
|
1180
|
+
55%,
|
|
1181
|
+
100% {
|
|
1182
|
+
transform: translateX(100%);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/* ── Reduced Motion ───────────────────────────────────────────── */
|
|
1187
|
+
|
|
1188
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1189
|
+
.skeleton-inner::after {
|
|
1190
|
+
animation: none;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
</style>
|