@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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. 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>