@zentauri-ui/zentauri-components 2.3.0 → 2.3.2

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 (243) hide show
  1. package/README.md +9 -5
  2. package/cli/props.json +2508 -1458
  3. package/cli/registry.json +20 -0
  4. package/dist/chunk-2RBBXVFA.js +19 -0
  5. package/dist/{chunk-ENKXB2BA.js.map → chunk-2RBBXVFA.js.map} +1 -1
  6. package/dist/{chunk-BFHJF4MV.mjs → chunk-425DAZTS.mjs} +4 -4
  7. package/dist/{chunk-BFHJF4MV.mjs.map → chunk-425DAZTS.mjs.map} +1 -1
  8. package/dist/chunk-4U6PVVST.mjs +15 -0
  9. package/dist/chunk-4U6PVVST.mjs.map +1 -0
  10. package/dist/{chunk-AARJLZXP.js → chunk-6GP74P4F.js} +6 -6
  11. package/dist/{chunk-AARJLZXP.js.map → chunk-6GP74P4F.js.map} +1 -1
  12. package/dist/{chunk-DIAA5VH4.mjs → chunk-CIZQQ32L.mjs} +3 -3
  13. package/dist/chunk-CIZQQ32L.mjs.map +1 -0
  14. package/dist/chunk-DUKHIN2W.js +44 -0
  15. package/dist/chunk-DUKHIN2W.js.map +1 -0
  16. package/dist/{chunk-PQ2XTY3M.js → chunk-G36ZV446.js} +13 -13
  17. package/dist/{chunk-PQ2XTY3M.js.map → chunk-G36ZV446.js.map} +1 -1
  18. package/dist/chunk-HJ7EFBED.js +86 -0
  19. package/dist/chunk-HJ7EFBED.js.map +1 -0
  20. package/dist/{chunk-H3BJOK22.js → chunk-HNAUUCR5.js} +3 -3
  21. package/dist/chunk-HNAUUCR5.js.map +1 -0
  22. package/dist/chunk-K2VCZK4I.mjs +75 -0
  23. package/dist/chunk-K2VCZK4I.mjs.map +1 -0
  24. package/dist/{chunk-DSX6RUYI.js → chunk-KVBFWRPF.js} +12 -12
  25. package/dist/{chunk-DSX6RUYI.js.map → chunk-KVBFWRPF.js.map} +1 -1
  26. package/dist/chunk-N4MLFU2Q.mjs +69 -0
  27. package/dist/chunk-N4MLFU2Q.mjs.map +1 -0
  28. package/dist/{chunk-ATE5SCTR.mjs → chunk-N6Q4ZLQR.mjs} +3 -3
  29. package/dist/{chunk-ATE5SCTR.mjs.map → chunk-N6Q4ZLQR.mjs.map} +1 -1
  30. package/dist/{chunk-YY7G4NV3.js → chunk-NZQA4M35.js} +49 -17
  31. package/dist/chunk-NZQA4M35.js.map +1 -0
  32. package/dist/chunk-OME7XOPN.js +78 -0
  33. package/dist/chunk-OME7XOPN.js.map +1 -0
  34. package/dist/chunk-PVDWJUMF.mjs +34 -0
  35. package/dist/chunk-PVDWJUMF.mjs.map +1 -0
  36. package/dist/chunk-PVVYOIU2.js +38 -0
  37. package/dist/chunk-PVVYOIU2.js.map +1 -0
  38. package/dist/chunk-PWEZB53R.js +90 -0
  39. package/dist/chunk-PWEZB53R.js.map +1 -0
  40. package/dist/chunk-PWYEC3KY.mjs +30 -0
  41. package/dist/chunk-PWYEC3KY.mjs.map +1 -0
  42. package/dist/{chunk-JKKF5DCF.mjs → chunk-R2JXARKB.mjs} +3 -3
  43. package/dist/{chunk-JKKF5DCF.mjs.map → chunk-R2JXARKB.mjs.map} +1 -1
  44. package/dist/chunk-SD7YXMNV.js +40 -0
  45. package/dist/chunk-SD7YXMNV.js.map +1 -0
  46. package/dist/{chunk-ZB6C6CJQ.mjs → chunk-TZTUL6C4.mjs} +40 -8
  47. package/dist/chunk-TZTUL6C4.mjs.map +1 -0
  48. package/dist/chunk-UBQY572I.mjs +81 -0
  49. package/dist/chunk-UBQY572I.mjs.map +1 -0
  50. package/dist/chunk-V5EE5ATH.mjs +36 -0
  51. package/dist/chunk-V5EE5ATH.mjs.map +1 -0
  52. package/dist/chunk-XUK5P37Y.js +19 -0
  53. package/dist/chunk-XUK5P37Y.js.map +1 -0
  54. package/dist/{chunk-WZY32L6K.mjs → chunk-YDD5HQGX.mjs} +3 -3
  55. package/dist/{chunk-WZY32L6K.mjs.map → chunk-YDD5HQGX.mjs.map} +1 -1
  56. package/dist/design-system/facade.js +12 -8
  57. package/dist/design-system/facade.js.map +1 -1
  58. package/dist/design-system/facade.mjs +11 -7
  59. package/dist/design-system/facade.mjs.map +1 -1
  60. package/dist/design-system/index.d.ts +4 -0
  61. package/dist/design-system/index.d.ts.map +1 -1
  62. package/dist/design-system/qr-code.d.ts +4 -0
  63. package/dist/design-system/qr-code.d.ts.map +1 -0
  64. package/dist/design-system/qr-scanner.d.ts +11 -0
  65. package/dist/design-system/qr-scanner.d.ts.map +1 -0
  66. package/dist/design-system/secret-reveal.d.ts +1 -1
  67. package/dist/design-system/secret-reveal.d.ts.map +1 -1
  68. package/dist/design-system/speech-recognition.d.ts +39 -0
  69. package/dist/design-system/speech-recognition.d.ts.map +1 -0
  70. package/dist/design-system/speech-synthesizer.d.ts +41 -0
  71. package/dist/design-system/speech-synthesizer.d.ts.map +1 -0
  72. package/dist/ui/buttons/animated.js +14 -10
  73. package/dist/ui/buttons/animated.js.map +1 -1
  74. package/dist/ui/buttons/animated.mjs +12 -8
  75. package/dist/ui/buttons/animated.mjs.map +1 -1
  76. package/dist/ui/buttons.js +15 -11
  77. package/dist/ui/buttons.mjs +13 -9
  78. package/dist/ui/data-table.js +25 -21
  79. package/dist/ui/data-table.js.map +1 -1
  80. package/dist/ui/data-table.mjs +15 -11
  81. package/dist/ui/data-table.mjs.map +1 -1
  82. package/dist/ui/dynamic-stepper.js +24 -20
  83. package/dist/ui/dynamic-stepper.js.map +1 -1
  84. package/dist/ui/dynamic-stepper.mjs +13 -9
  85. package/dist/ui/dynamic-stepper.mjs.map +1 -1
  86. package/dist/ui/pagination.js +16 -12
  87. package/dist/ui/pagination.mjs +13 -9
  88. package/dist/ui/qr-code/animated/animations.d.ts +8 -0
  89. package/dist/ui/qr-code/animated/animations.d.ts.map +1 -0
  90. package/dist/ui/qr-code/animated/index.d.ts +4 -0
  91. package/dist/ui/qr-code/animated/index.d.ts.map +1 -0
  92. package/dist/ui/qr-code/animated/qr-code-animated.d.ts +6 -0
  93. package/dist/ui/qr-code/animated/qr-code-animated.d.ts.map +1 -0
  94. package/dist/ui/qr-code/animated/types.d.ts +9 -0
  95. package/dist/ui/qr-code/animated/types.d.ts.map +1 -0
  96. package/dist/ui/qr-code/animated.js +156 -0
  97. package/dist/ui/qr-code/animated.js.map +1 -0
  98. package/dist/ui/qr-code/animated.mjs +149 -0
  99. package/dist/ui/qr-code/animated.mjs.map +1 -0
  100. package/dist/ui/qr-code/index.d.ts +5 -0
  101. package/dist/ui/qr-code/index.d.ts.map +1 -0
  102. package/dist/ui/qr-code/qr-code-base.d.ts +47 -0
  103. package/dist/ui/qr-code/qr-code-base.d.ts.map +1 -0
  104. package/dist/ui/qr-code/qr-code.d.ts +2 -0
  105. package/dist/ui/qr-code/qr-code.d.ts.map +1 -0
  106. package/dist/ui/qr-code/types.d.ts +14 -0
  107. package/dist/ui/qr-code/types.d.ts.map +1 -0
  108. package/dist/ui/qr-code/variants.d.ts +4 -0
  109. package/dist/ui/qr-code/variants.d.ts.map +1 -0
  110. package/dist/ui/qr-code.js +35 -0
  111. package/dist/ui/qr-code.js.map +1 -0
  112. package/dist/ui/qr-code.mjs +17 -0
  113. package/dist/ui/qr-code.mjs.map +1 -0
  114. package/dist/ui/qr-scanner/index.d.ts +4 -0
  115. package/dist/ui/qr-scanner/index.d.ts.map +1 -0
  116. package/dist/ui/qr-scanner/qr-scanner-base.d.ts +62 -0
  117. package/dist/ui/qr-scanner/qr-scanner-base.d.ts.map +1 -0
  118. package/dist/ui/qr-scanner/qr-scanner.d.ts +2 -0
  119. package/dist/ui/qr-scanner/qr-scanner.d.ts.map +1 -0
  120. package/dist/ui/qr-scanner/types.d.ts +28 -0
  121. package/dist/ui/qr-scanner/types.d.ts.map +1 -0
  122. package/dist/ui/qr-scanner/variants.d.ts +9 -0
  123. package/dist/ui/qr-scanner/variants.d.ts.map +1 -0
  124. package/dist/ui/qr-scanner.js +316 -0
  125. package/dist/ui/qr-scanner.js.map +1 -0
  126. package/dist/ui/qr-scanner.mjs +308 -0
  127. package/dist/ui/qr-scanner.mjs.map +1 -0
  128. package/dist/ui/secret-reveal/animated/secret-reveal-animated.d.ts.map +1 -1
  129. package/dist/ui/secret-reveal/animated.js +10 -7
  130. package/dist/ui/secret-reveal/animated.js.map +1 -1
  131. package/dist/ui/secret-reveal/animated.mjs +6 -3
  132. package/dist/ui/secret-reveal/animated.mjs.map +1 -1
  133. package/dist/ui/secret-reveal/secret-reveal-base.d.ts.map +1 -1
  134. package/dist/ui/secret-reveal.js +14 -11
  135. package/dist/ui/secret-reveal.js.map +1 -1
  136. package/dist/ui/secret-reveal.mjs +7 -4
  137. package/dist/ui/secret-reveal.mjs.map +1 -1
  138. package/dist/ui/speech-recognition/animated/animations.d.ts +8 -0
  139. package/dist/ui/speech-recognition/animated/animations.d.ts.map +1 -0
  140. package/dist/ui/speech-recognition/animated/index.d.ts +4 -0
  141. package/dist/ui/speech-recognition/animated/index.d.ts.map +1 -0
  142. package/dist/ui/speech-recognition/animated/speech-recognition-animated.d.ts +6 -0
  143. package/dist/ui/speech-recognition/animated/speech-recognition-animated.d.ts.map +1 -0
  144. package/dist/ui/speech-recognition/animated/types.d.ts +9 -0
  145. package/dist/ui/speech-recognition/animated/types.d.ts.map +1 -0
  146. package/dist/ui/speech-recognition/animated.js +288 -0
  147. package/dist/ui/speech-recognition/animated.js.map +1 -0
  148. package/dist/ui/speech-recognition/animated.mjs +285 -0
  149. package/dist/ui/speech-recognition/animated.mjs.map +1 -0
  150. package/dist/ui/speech-recognition/index.d.ts +4 -0
  151. package/dist/ui/speech-recognition/index.d.ts.map +1 -0
  152. package/dist/ui/speech-recognition/speech-recognition-base.d.ts +6 -0
  153. package/dist/ui/speech-recognition/speech-recognition-base.d.ts.map +1 -0
  154. package/dist/ui/speech-recognition/speech-recognition.d.ts +2 -0
  155. package/dist/ui/speech-recognition/speech-recognition.d.ts.map +1 -0
  156. package/dist/ui/speech-recognition/types.d.ts +31 -0
  157. package/dist/ui/speech-recognition/types.d.ts.map +1 -0
  158. package/dist/ui/speech-recognition/variants.d.ts +11 -0
  159. package/dist/ui/speech-recognition/variants.d.ts.map +1 -0
  160. package/dist/ui/speech-recognition.js +242 -0
  161. package/dist/ui/speech-recognition.js.map +1 -0
  162. package/dist/ui/speech-recognition.mjs +233 -0
  163. package/dist/ui/speech-recognition.mjs.map +1 -0
  164. package/dist/ui/speech-synthesizer/animated/animations.d.ts +8 -0
  165. package/dist/ui/speech-synthesizer/animated/animations.d.ts.map +1 -0
  166. package/dist/ui/speech-synthesizer/animated/index.d.ts +4 -0
  167. package/dist/ui/speech-synthesizer/animated/index.d.ts.map +1 -0
  168. package/dist/ui/speech-synthesizer/animated/speech-synthesizer-animated.d.ts +6 -0
  169. package/dist/ui/speech-synthesizer/animated/speech-synthesizer-animated.d.ts.map +1 -0
  170. package/dist/ui/speech-synthesizer/animated/types.d.ts +9 -0
  171. package/dist/ui/speech-synthesizer/animated/types.d.ts.map +1 -0
  172. package/dist/ui/speech-synthesizer/animated.js +269 -0
  173. package/dist/ui/speech-synthesizer/animated.js.map +1 -0
  174. package/dist/ui/speech-synthesizer/animated.mjs +266 -0
  175. package/dist/ui/speech-synthesizer/animated.mjs.map +1 -0
  176. package/dist/ui/speech-synthesizer/index.d.ts +4 -0
  177. package/dist/ui/speech-synthesizer/index.d.ts.map +1 -0
  178. package/dist/ui/speech-synthesizer/speech-synthesizer-base.d.ts +6 -0
  179. package/dist/ui/speech-synthesizer/speech-synthesizer-base.d.ts.map +1 -0
  180. package/dist/ui/speech-synthesizer/speech-synthesizer.d.ts +2 -0
  181. package/dist/ui/speech-synthesizer/speech-synthesizer.d.ts.map +1 -0
  182. package/dist/ui/speech-synthesizer/types.d.ts +43 -0
  183. package/dist/ui/speech-synthesizer/types.d.ts.map +1 -0
  184. package/dist/ui/speech-synthesizer/variants.d.ts +13 -0
  185. package/dist/ui/speech-synthesizer/variants.d.ts.map +1 -0
  186. package/dist/ui/speech-synthesizer.js +220 -0
  187. package/dist/ui/speech-synthesizer.js.map +1 -0
  188. package/dist/ui/speech-synthesizer.mjs +211 -0
  189. package/dist/ui/speech-synthesizer.mjs.map +1 -0
  190. package/dist/ui/split-button.js +26 -22
  191. package/dist/ui/split-button.js.map +1 -1
  192. package/dist/ui/split-button.mjs +13 -9
  193. package/dist/ui/split-button.mjs.map +1 -1
  194. package/package.json +5 -2
  195. package/src/design-system/index.ts +4 -0
  196. package/src/design-system/qr-code.ts +13 -0
  197. package/src/design-system/qr-scanner.ts +32 -0
  198. package/src/design-system/secret-reveal.ts +1 -1
  199. package/src/design-system/speech-recognition.ts +82 -0
  200. package/src/design-system/speech-synthesizer.ts +90 -0
  201. package/src/ui/qr-code/animated/animations.ts +51 -0
  202. package/src/ui/qr-code/animated/index.ts +5 -0
  203. package/src/ui/qr-code/animated/qr-code-animated.tsx +111 -0
  204. package/src/ui/qr-code/animated/types.ts +10 -0
  205. package/src/ui/qr-code/index.ts +10 -0
  206. package/src/ui/qr-code/qr-code-base.tsx +149 -0
  207. package/src/ui/qr-code/qr-code.test.tsx +58 -0
  208. package/src/ui/qr-code/qr-code.tsx +2 -0
  209. package/src/ui/qr-code/types.ts +22 -0
  210. package/src/ui/qr-code/variants.ts +11 -0
  211. package/src/ui/qr-scanner/index.ts +17 -0
  212. package/src/ui/qr-scanner/qr-scanner-base.tsx +568 -0
  213. package/src/ui/qr-scanner/qr-scanner.test.tsx +61 -0
  214. package/src/ui/qr-scanner/qr-scanner.tsx +2 -0
  215. package/src/ui/qr-scanner/types.ts +32 -0
  216. package/src/ui/qr-scanner/variants.ts +26 -0
  217. package/src/ui/secret-reveal/animated/secret-reveal-animated.tsx +4 -1
  218. package/src/ui/secret-reveal/secret-reveal-base.tsx +4 -1
  219. package/src/ui/speech-recognition/animated/animations.ts +62 -0
  220. package/src/ui/speech-recognition/animated/index.ts +8 -0
  221. package/src/ui/speech-recognition/animated/speech-recognition-animated.tsx +276 -0
  222. package/src/ui/speech-recognition/animated/types.ts +11 -0
  223. package/src/ui/speech-recognition/index.ts +15 -0
  224. package/src/ui/speech-recognition/speech-recognition-base.tsx +276 -0
  225. package/src/ui/speech-recognition/speech-recognition.test.tsx +74 -0
  226. package/src/ui/speech-recognition/speech-recognition.tsx +1 -0
  227. package/src/ui/speech-recognition/types.ts +50 -0
  228. package/src/ui/speech-recognition/variants.ts +47 -0
  229. package/src/ui/speech-synthesizer/animated/animations.ts +62 -0
  230. package/src/ui/speech-synthesizer/animated/index.ts +8 -0
  231. package/src/ui/speech-synthesizer/animated/speech-synthesizer-animated.tsx +260 -0
  232. package/src/ui/speech-synthesizer/animated/types.ts +11 -0
  233. package/src/ui/speech-synthesizer/index.ts +14 -0
  234. package/src/ui/speech-synthesizer/speech-synthesizer-base.tsx +255 -0
  235. package/src/ui/speech-synthesizer/speech-synthesizer.test.tsx +87 -0
  236. package/src/ui/speech-synthesizer/speech-synthesizer.tsx +1 -0
  237. package/src/ui/speech-synthesizer/types.ts +57 -0
  238. package/src/ui/speech-synthesizer/variants.ts +55 -0
  239. package/dist/chunk-DIAA5VH4.mjs.map +0 -1
  240. package/dist/chunk-ENKXB2BA.js +0 -19
  241. package/dist/chunk-H3BJOK22.js.map +0 -1
  242. package/dist/chunk-YY7G4NV3.js.map +0 -1
  243. package/dist/chunk-ZB6C6CJQ.mjs.map +0 -1
@@ -0,0 +1,82 @@
1
+ export const zuiSpeechRecognitionAppearances = {
2
+ default:
3
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-default-btn-bg,var(--zui-brand,oklch(20.8%_0.042_265.755)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-default-btn-bg-dark,var(--zui-brand-dark,oklch(98.4%_0.003_247.858)))]",
4
+ subtle:
5
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-subtle-btn-bg,var(--zui-surface-muted,oklch(92.9%_0.013_255.508)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-subtle-btn-bg-dark,var(--zui-surface-muted-dark,oklch(27.9%_0.041_260.031)))]",
6
+ muted:
7
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-muted-btn-bg,var(--zui-fg-muted,oklch(44.6%_0.043_257.281)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-muted-btn-bg-dark,var(--zui-fg-muted-dark,oklch(86.9%_0.022_252.894)))]",
8
+ primary:
9
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-primary-btn-bg,var(--zui-fg,oklch(20.8%_0.042_265.755)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-primary-btn-bg-dark,var(--zui-fg-dark,oklch(98.4%_0.003_247.858)))]",
10
+ blue: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-blue-btn-bg,var(--zui-color-blue,#2563eb))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-blue-btn-bg-dark,var(--zui-color-blue-dark,#3b82f6))]",
11
+ cyan: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-cyan-btn-bg,var(--zui-color-cyan,#0891b2))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-cyan-btn-bg-dark,var(--zui-color-cyan-dark,#22d3ee))]",
12
+ green:
13
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-green-btn-bg,var(--zui-color-green,#16a34a))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-green-btn-bg-dark,var(--zui-color-green-dark,#22c55e))]",
14
+ lime: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-lime-btn-bg,var(--zui-color-lime,#65a30d))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-lime-btn-bg-dark,var(--zui-color-lime-dark,#a3e635))]",
15
+ emerald:
16
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-emerald-btn-bg,var(--zui-color-emerald,oklch(69.6%_0.17_162.48)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-emerald-btn-bg-dark,var(--zui-color-emerald-dark,oklch(43.2%_0.095_166.913)))]",
17
+ indigo:
18
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-indigo-btn-bg,var(--zui-color-indigo,oklch(39.8%_0.195_277.366)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-indigo-btn-bg-dark,var(--zui-color-indigo-dark,oklch(51.1%_0.262_276.966)))]",
19
+ purple:
20
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-purple-btn-bg,var(--zui-color-purple,oklch(43.8%_0.218_303.724)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-purple-btn-bg-dark,var(--zui-color-purple-dark,oklch(55.8%_0.288_302.321)))]",
21
+ pink: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-pink-btn-bg,var(--zui-color-pink,oklch(45.9%_0.187_3.815)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-pink-btn-bg-dark,var(--zui-color-pink-dark,oklch(59.2%_0.249_0.584)))]",
22
+ rose: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-rose-btn-bg,var(--zui-color-rose,oklch(64.5%_0.246_16.439)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-rose-btn-bg-dark,var(--zui-color-rose-dark,oklch(51.4%_0.222_16.935)))]",
23
+ sky: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-sky-btn-bg,var(--zui-color-sky,oklch(68.5%_0.169_237.323)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-sky-btn-bg-dark,var(--zui-color-sky-dark,oklch(50%_0.134_242.749)))]",
24
+ teal: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-teal-btn-bg,var(--zui-color-teal,oklch(70.4%_0.14_182.503)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-teal-btn-bg-dark,var(--zui-color-teal-dark,oklch(51.1%_0.096_186.391)))]",
25
+ yellow:
26
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-yellow-btn-bg,var(--zui-color-yellow,oklch(79.5%_0.184_86.047)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-yellow-btn-bg-dark,var(--zui-color-yellow-dark,oklch(47.6%_0.114_61.907)))]",
27
+ orange:
28
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-orange-btn-bg,var(--zui-color-orange,oklch(70.5%_0.213_47.604)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-orange-btn-bg-dark,var(--zui-color-orange-dark,oklch(47%_0.157_37.304)))]",
29
+ red: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-red-btn-bg,var(--zui-color-red,#dc2626))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-red-btn-bg-dark,var(--zui-color-red-dark,#ef4444))]",
30
+ slate:
31
+ "[--zui-recognition-btn-bg:var(--zui-speech-recognition-slate-btn-bg,var(--zui-color-slate,#475569))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-slate-btn-bg-dark,var(--zui-color-slate-dark,#64748b))]",
32
+ gray: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-gray-btn-bg,var(--zui-color-gray,oklch(55.1%_0.027_264.364)))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-gray-btn-bg-dark,var(--zui-color-gray-dark,oklch(55.1%_0.027_264.364)))]",
33
+ zinc: "[--zui-recognition-btn-bg:var(--zui-speech-recognition-zinc-btn-bg,var(--zui-color-zinc,#52525b))] dark:[--zui-recognition-btn-bg:var(--zui-speech-recognition-zinc-btn-bg-dark,var(--zui-color-zinc-dark,#71717a))]",
34
+ } as const;
35
+
36
+ export const zuiSpeechRecognitionBase = [
37
+ "inline-flex flex-col gap-2",
38
+ "rounded-[var(--zui-speech-recognition-radius,var(--zui-radius,0.75rem))]",
39
+ "bg-[var(--zui-speech-recognition-bg,var(--zui-surface,#ffffff))] dark:bg-[var(--zui-speech-recognition-bg-dark,var(--zui-surface-dark,#0f172a))]",
40
+ "border border-[var(--zui-speech-recognition-border,var(--zui-border,#e2e8f0))] dark:border-[var(--zui-speech-recognition-border-dark,var(--zui-border-dark,#1e293b))]",
41
+ "p-4",
42
+ ] as const;
43
+
44
+ export const zuiSpeechRecognitionStatusBase = [
45
+ "text-[color:var(--zui-speech-recognition-status-fg,var(--zui-fg-muted,oklch(44.6%_0.043_257.281)))] dark:text-[color:var(--zui-speech-recognition-status-fg-dark,var(--zui-fg-muted-dark,oklch(86.9%_0.022_252.894)))]",
46
+ "text-xs",
47
+ "transition-opacity",
48
+ ] as const;
49
+
50
+ export const zuiSpeechRecognitionTranscriptBase = [
51
+ "text-[color:var(--zui-speech-recognition-transcript-fg,var(--zui-fg,oklch(20.8%_0.042_265.755)))] dark:text-[color:var(--zui-speech-recognition-transcript-fg-dark,var(--zui-fg-dark,oklch(98.4%_0.003_247.858)))]",
52
+ "text-sm",
53
+ "min-h-[1.25rem]",
54
+ "leading-relaxed",
55
+ ] as const;
56
+
57
+ export const zuiSpeechRecognitionSizes = {
58
+ sm: "gap-1.5 p-3",
59
+ md: "gap-2 p-4",
60
+ lg: "gap-3 p-5",
61
+ } as const;
62
+
63
+ export const zuiSpeechRecognitionBtnBase = [
64
+ "inline-flex items-center justify-center rounded-full",
65
+ "size-10",
66
+ "transition-all duration-200",
67
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
68
+ "bg-[var(--zui-recognition-btn-bg,var(--zui-brand,oklch(20.8%_0.042_265.755)))] dark:bg-[var(--zui-recognition-btn-bg,var(--zui-brand-dark,oklch(98.4%_0.003_247.858)))]",
69
+ "text-[color:var(--zui-speech-recognition-btn-fg,var(--zui-bg,#ffffff))] dark:text-[color:var(--zui-speech-recognition-btn-fg-dark,var(--zui-bg-dark,#0f172a))]",
70
+ ] as const;
71
+
72
+ export const zuiSpeechRecognitionBtnSizes = {
73
+ sm: "size-8",
74
+ md: "size-10",
75
+ lg: "size-12",
76
+ } as const;
77
+
78
+ export const zuiSpeechRecognitionBtnActive = [
79
+ "ring-2 ring-[var(--zui-speech-recognition-active-ring,var(--zui-status-error,#dc2626))] dark:ring-[var(--zui-speech-recognition-active-ring-dark,var(--zui-status-error-dark,#ef4444))]",
80
+ "ring-offset-2",
81
+ "animate-pulse",
82
+ ] as const;
@@ -0,0 +1,90 @@
1
+ export const zuiSpeechSynthesizerAppearances = {
2
+ default:
3
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-default-btn-bg,var(--zui-brand,oklch(20.8%_0.042_265.755)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-default-btn-bg-dark,var(--zui-brand-dark,oklch(98.4%_0.003_247.858)))]",
4
+ subtle:
5
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-subtle-btn-bg,var(--zui-surface-muted,oklch(92.9%_0.013_255.508)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-subtle-btn-bg-dark,var(--zui-surface-muted-dark,oklch(27.9%_0.041_260.031)))]",
6
+ muted:
7
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-muted-btn-bg,var(--zui-fg-muted,oklch(44.6%_0.043_257.281)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-muted-btn-bg-dark,var(--zui-fg-muted-dark,oklch(86.9%_0.022_252.894)))]",
8
+ primary:
9
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-primary-btn-bg,var(--zui-fg,oklch(20.8%_0.042_265.755)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-primary-btn-bg-dark,var(--zui-fg-dark,oklch(98.4%_0.003_247.858)))]",
10
+ blue: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-blue-btn-bg,var(--zui-color-blue,#2563eb))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-blue-btn-bg-dark,var(--zui-color-blue-dark,#3b82f6))]",
11
+ cyan: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-cyan-btn-bg,var(--zui-color-cyan,#0891b2))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-cyan-btn-bg-dark,var(--zui-color-cyan-dark,#22d3ee))]",
12
+ green:
13
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-green-btn-bg,var(--zui-color-green,#16a34a))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-green-btn-bg-dark,var(--zui-color-green-dark,#22c55e))]",
14
+ lime: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-lime-btn-bg,var(--zui-color-lime,#65a30d))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-lime-btn-bg-dark,var(--zui-color-lime-dark,#a3e635))]",
15
+ emerald:
16
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-emerald-btn-bg,var(--zui-color-emerald,oklch(69.6%_0.17_162.48)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-emerald-btn-bg-dark,var(--zui-color-emerald-dark,oklch(43.2%_0.095_166.913)))]",
17
+ indigo:
18
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-indigo-btn-bg,var(--zui-color-indigo,oklch(39.8%_0.195_277.366)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-indigo-btn-bg-dark,var(--zui-color-indigo-dark,oklch(51.1%_0.262_276.966)))]",
19
+ purple:
20
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-purple-btn-bg,var(--zui-color-purple,oklch(43.8%_0.218_303.724)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-purple-btn-bg-dark,var(--zui-color-purple-dark,oklch(55.8%_0.288_302.321)))]",
21
+ pink: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-pink-btn-bg,var(--zui-color-pink,oklch(45.9%_0.187_3.815)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-pink-btn-bg-dark,var(--zui-color-pink-dark,oklch(59.2%_0.249_0.584)))]",
22
+ rose: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-rose-btn-bg,var(--zui-color-rose,oklch(64.5%_0.246_16.439)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-rose-btn-bg-dark,var(--zui-color-rose-dark,oklch(51.4%_0.222_16.935)))]",
23
+ sky: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-sky-btn-bg,var(--zui-color-sky,oklch(68.5%_0.169_237.323)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-sky-btn-bg-dark,var(--zui-color-sky-dark,oklch(50%_0.134_242.749)))]",
24
+ teal: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-teal-btn-bg,var(--zui-color-teal,oklch(70.4%_0.14_182.503)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-teal-btn-bg-dark,var(--zui-color-teal-dark,oklch(51.1%_0.096_186.391)))]",
25
+ yellow:
26
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-yellow-btn-bg,var(--zui-color-yellow,oklch(79.5%_0.184_86.047)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-yellow-btn-bg-dark,var(--zui-color-yellow-dark,oklch(47.6%_0.114_61.907)))]",
27
+ orange:
28
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-orange-btn-bg,var(--zui-color-orange,oklch(70.5%_0.213_47.604)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-orange-btn-bg-dark,var(--zui-color-orange-dark,oklch(47%_0.157_37.304)))]",
29
+ red: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-red-btn-bg,var(--zui-color-red,#dc2626))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-red-btn-bg-dark,var(--zui-color-red-dark,#ef4444))]",
30
+ slate:
31
+ "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-slate-btn-bg,var(--zui-color-slate,#475569))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-slate-btn-bg-dark,var(--zui-color-slate-dark,#64748b))]",
32
+ gray: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-gray-btn-bg,var(--zui-color-gray,oklch(55.1%_0.027_264.364)))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-gray-btn-bg-dark,var(--zui-color-gray-dark,oklch(55.1%_0.027_264.364)))]",
33
+ zinc: "[--zui-synth-btn-bg:var(--zui-speech-synthesizer-zinc-btn-bg,var(--zui-color-zinc,#52525b))] dark:[--zui-synth-btn-bg:var(--zui-speech-synthesizer-zinc-btn-bg-dark,var(--zui-color-zinc-dark,#71717a))]",
34
+ } as const;
35
+
36
+ export const zuiSpeechSynthesizerBase = [
37
+ "inline-flex flex-col gap-3",
38
+ "rounded-[var(--zui-speech-synthesizer-radius,var(--zui-radius,0.75rem))]",
39
+ "bg-[var(--zui-speech-synthesizer-bg,var(--zui-surface,#ffffff))] dark:bg-[var(--zui-speech-synthesizer-bg-dark,var(--zui-surface-dark,#0f172a))]",
40
+ "border border-[var(--zui-speech-synthesizer-border,var(--zui-border,#e2e8f0))] dark:border-[var(--zui-speech-synthesizer-border-dark,var(--zui-border-dark,#1e293b))]",
41
+ "p-4",
42
+ ] as const;
43
+
44
+ export const zuiSpeechSynthesizerSizes = {
45
+ sm: "gap-2 p-3",
46
+ md: "gap-3 p-4",
47
+ lg: "gap-4 p-5",
48
+ } as const;
49
+
50
+ export const zuiSpeechSynthesizerBtnBase = [
51
+ "inline-flex items-center justify-center rounded-full",
52
+ "size-10",
53
+ "transition-all duration-200",
54
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
55
+ "bg-[var(--zui-synth-btn-bg,var(--zui-brand,oklch(20.8%_0.042_265.755)))] dark:bg-[var(--zui-synth-btn-bg,var(--zui-brand-dark,oklch(98.4%_0.003_247.858)))]",
56
+ "text-[color:var(--zui-speech-synthesizer-btn-fg,var(--zui-bg,#ffffff))] dark:text-[color:var(--zui-speech-synthesizer-btn-fg-dark,var(--zui-bg-dark,#0f172a))]",
57
+ ] as const;
58
+
59
+ export const zuiSpeechSynthesizerBtnSizes = {
60
+ sm: "size-8",
61
+ md: "size-10",
62
+ lg: "size-12",
63
+ } as const;
64
+
65
+ export const zuiSpeechSynthesizerBtnActive = [
66
+ "ring-2 ring-[var(--zui-speech-synthesizer-active-ring,var(--zui-color-green,#16a34a))] dark:ring-[var(--zui-speech-synthesizer-active-ring-dark,var(--zui-color-green-dark,#22c55e))]",
67
+ "ring-offset-2",
68
+ "animate-pulse",
69
+ ] as const;
70
+
71
+ export const zuiSpeechSynthesizerTextBase = [
72
+ "text-[color:var(--zui-speech-synthesizer-text-fg,var(--zui-fg,oklch(20.8%_0.042_265.755)))] dark:text-[color:var(--zui-speech-synthesizer-text-fg-dark,var(--zui-fg-dark,oklch(98.4%_0.003_247.858)))]",
73
+ "text-sm",
74
+ "leading-relaxed",
75
+ ] as const;
76
+
77
+ export const zuiSpeechSynthesizerControlsBase = [
78
+ "inline-flex items-center gap-2",
79
+ ] as const;
80
+
81
+ export const zuiSpeechSynthesizerProgressBase = [
82
+ "relative w-full h-1 overflow-hidden rounded-full",
83
+ "bg-[var(--zui-speech-synthesizer-track-bg,var(--zui-surface-muted,#0000001a))] dark:bg-[var(--zui-speech-synthesizer-track-bg-dark,var(--zui-surface-muted-dark,#ffffff1a))]",
84
+ ] as const;
85
+
86
+ export const zuiSpeechSynthesizerProgressBarBase = [
87
+ "h-full rounded-full",
88
+ "bg-[var(--zui-synth-btn-bg,var(--zui-brand,oklch(20.8%_0.042_265.755)))] dark:bg-[var(--zui-synth-btn-bg,var(--zui-brand-dark,oklch(98.4%_0.003_247.858)))]",
89
+ "transition-all duration-300",
90
+ ] as const;
@@ -0,0 +1,51 @@
1
+ import type { Transition, Variants } from "framer-motion";
2
+
3
+ export type QrCodeAnimation =
4
+ | "none"
5
+ | "fade-in"
6
+ | "zoom-in"
7
+ | "slide-up"
8
+ | "rotate-in";
9
+
10
+ export type QrCodeAnimationPresets = Record<
11
+ QrCodeAnimation,
12
+ { transition: Transition; variants: Variants }
13
+ >;
14
+
15
+ export const qrCodeAnimationPresets: QrCodeAnimationPresets = {
16
+ none: {
17
+ transition: { duration: 0 },
18
+ variants: {
19
+ initial: { opacity: 1 },
20
+ animate: { opacity: 1 },
21
+ },
22
+ },
23
+ "fade-in": {
24
+ transition: { duration: 0.3, ease: "easeOut" },
25
+ variants: {
26
+ initial: { opacity: 0 },
27
+ animate: { opacity: 1 },
28
+ },
29
+ },
30
+ "zoom-in": {
31
+ transition: { type: "spring", stiffness: 300, damping: 24 },
32
+ variants: {
33
+ initial: { opacity: 0, scale: 0.85 },
34
+ animate: { opacity: 1, scale: 1 },
35
+ },
36
+ },
37
+ "slide-up": {
38
+ transition: { type: "spring", stiffness: 280, damping: 22 },
39
+ variants: {
40
+ initial: { opacity: 0, y: 20 },
41
+ animate: { opacity: 1, y: 0 },
42
+ },
43
+ },
44
+ "rotate-in": {
45
+ transition: { type: "spring", stiffness: 200, damping: 18 },
46
+ variants: {
47
+ initial: { opacity: 0, rotate: -90, scale: 0.8 },
48
+ animate: { opacity: 1, rotate: 0, scale: 1 },
49
+ },
50
+ },
51
+ };
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ export { QrCodeAnimated } from "./qr-code-animated";
4
+ export type { QrCodeAnimation, QrCodeAnimatedProps } from "./types";
5
+ export { qrCodeAnimationPresets } from "./animations";
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import { motion } from "framer-motion";
5
+ import QRCode from "qrcode";
6
+
7
+ import { cn } from "../../../lib/utils";
8
+ import { qrCodeAnimationPresets } from "./animations";
9
+ import type { QrCodeAnimatedProps } from "./types";
10
+ import { QrCodeBase } from "../qr-code-base";
11
+
12
+ export function QrCodeAnimated({
13
+ animation = "none",
14
+ value,
15
+ canvasSize = 200,
16
+ level = "M",
17
+ bgColor = "#ffffff",
18
+ fgColor = "#000000",
19
+ margin = 2,
20
+ caption,
21
+ className,
22
+ ...props
23
+ }: QrCodeAnimatedProps) {
24
+ const canvasRef = useRef<HTMLCanvasElement>(null);
25
+
26
+ useEffect(() => {
27
+ const canvas = canvasRef.current;
28
+ if (!canvas || animation === "none") return;
29
+
30
+ if (!value) {
31
+ const ctx = canvas.getContext("2d");
32
+ if (ctx) {
33
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
34
+ }
35
+ return;
36
+ }
37
+
38
+ QRCode.toCanvas(
39
+ canvas,
40
+ value,
41
+ {
42
+ width: canvasSize,
43
+ margin,
44
+ color: {
45
+ dark: fgColor,
46
+ light: bgColor,
47
+ },
48
+ errorCorrectionLevel: level,
49
+ },
50
+ (error) => {
51
+ if (error) {
52
+ console.error("QR Code generation error:", error);
53
+ }
54
+ },
55
+ );
56
+ }, [value, canvasSize, level, bgColor, fgColor, margin, animation]);
57
+
58
+ if (animation === "none") {
59
+ return (
60
+ <QrCodeBase
61
+ value={value}
62
+ canvasSize={canvasSize}
63
+ level={level}
64
+ bgColor={bgColor}
65
+ fgColor={fgColor}
66
+ margin={margin}
67
+ caption={caption}
68
+ className={className}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ const preset = qrCodeAnimationPresets[animation];
75
+
76
+ return (
77
+ <motion.div
78
+ data-slot="qr-code"
79
+ className={cn(
80
+ "inline-flex flex-col items-center gap-3 rounded-2xl border p-6 border-[color:var(--zui-qr-code-border,var(--zui-border,#0000001a))] dark:border-[color:var(--zui-qr-code-border-dark,var(--zui-border-dark,#ffffff1a))] bg-[var(--zui-qr-code-bg,var(--zui-surface,oklch(98.4%_0.003_247.858)))] dark:bg-[var(--zui-qr-code-bg-dark,var(--zui-surface-dark,oklch(12.9%_0.042_264.695)))]",
81
+ className,
82
+ )}
83
+ variants={preset.variants}
84
+ initial="initial"
85
+ animate="animate"
86
+ transition={preset.transition}
87
+ {...(props as Record<string, unknown>)}
88
+ >
89
+ <div className="overflow-hidden rounded-xl bg-[var(--zui-qr-code-canvas-bg,var(--zui-surface-muted,oklch(92.9%_0.013_255.508)))] dark:bg-[var(--zui-qr-code-canvas-bg-dark,var(--zui-surface-muted-dark,oklch(27.9%_0.041_260.031)))]">
90
+ <canvas
91
+ ref={canvasRef}
92
+ width={canvasSize}
93
+ height={canvasSize}
94
+ data-slot="qr-code-canvas"
95
+ className="block h-auto max-w-full"
96
+ aria-label={`QR code for ${value}`}
97
+ />
98
+ </div>
99
+ {caption ? (
100
+ <span
101
+ data-slot="qr-code-caption"
102
+ className="text-xs text-center text-[color:var(--zui-qr-code-caption-fg,var(--zui-fg-muted,oklch(55.2%_0.046_257.417)))] dark:text-[color:var(--zui-qr-code-caption-fg-dark,var(--zui-fg-muted-dark,oklch(70.8%_0.015_256.243)))] max-w-full truncate px-2"
103
+ >
104
+ {caption}
105
+ </span>
106
+ ) : null}
107
+ </motion.div>
108
+ );
109
+ }
110
+
111
+ QrCodeAnimated.displayName = "QrCodeAnimated";
@@ -0,0 +1,10 @@
1
+ import type { Ref } from "react";
2
+ import type { QrCodeBaseProps } from "../types";
3
+ import type { QrCodeAnimation } from "./animations";
4
+
5
+ export type { QrCodeAnimation };
6
+
7
+ export type QrCodeAnimatedProps = QrCodeBaseProps & {
8
+ animation?: QrCodeAnimation;
9
+ ref?: Ref<HTMLDivElement>;
10
+ };
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ export { QrCode } from "./qr-code";
4
+ export type { QrCodeBaseProps, QrCodeLevel, QrCodeProps } from "./types";
5
+ export { QR_CODE_LEVEL_LABELS } from "./types";
6
+ export {
7
+ qrCodeCanvasWrapperVariants,
8
+ qrCodeCaptionVariants,
9
+ qrCodeVariants,
10
+ } from "./variants";
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import QRCode from "qrcode";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ import type { QrCodeBaseProps } from "./types";
9
+ import {
10
+ qrCodeCanvasWrapperVariants,
11
+ qrCodeCaptionVariants,
12
+ qrCodeVariants,
13
+ } from "./variants";
14
+
15
+ /**
16
+ * QrCodeBase renders a QR code onto a `<canvas>` element using the `qrcode`
17
+ * npm package. It is the static (non-animated) implementation of the public
18
+ * `QrCode` component.
19
+ *
20
+ * --- Props ---
21
+ *
22
+ * - `value` — The string to encode. When empty/falsy the canvas is cleared.
23
+ * - `canvasSize` — Pixel dimensions of the canvas (default: 200). Both width
24
+ * and height are set to this value, producing a square QR code.
25
+ * - `level` — Error correction level (`"L"` | `"M"` | `"Q"` | `"H"`). Higher
26
+ * levels survive more visual damage but hold less data.
27
+ * - `bgColor` / `fgColor` — Background and foreground colours passed to the
28
+ * underlying `QRCode.toCanvas` options.
29
+ * - `margin` — Quiet-zone width in QR modules (default: 2). Passed directly
30
+ * to the `qrcode` library.
31
+ * - `caption` — Optional label rendered below the QR code.
32
+ * - `ref` — React 19 ref forwarded to the root `<div>` element.
33
+ * - `className` / `...rest` — Spread onto the root `<div>` for layout or
34
+ * style overrides.
35
+ *
36
+ * --- Rendering lifecycle ---
37
+ *
38
+ * A `useEffect` watches `[value, canvasSize, level, bgColor, fgColor, margin]`.
39
+ * On every change it:
40
+ * 1. Grabs the canvas element from the ref.
41
+ * 2. If `value` is empty, clears the canvas with `ctx.clearRect` so a stale
42
+ * QR code doesn't persist.
43
+ * 3. Otherwise calls `QRCode.toCanvas()` which draws the QR pattern
44
+ * asynchronously (via callback) onto the canvas element.
45
+ *
46
+ * The outer `<div>` uses design-system variants (`qrCodeVariants`) for
47
+ * consistent theming and carries `data-slot="qr-code"` for scoped styling and
48
+ * testing.
49
+ *
50
+ * --- Accessibility ---
51
+ *
52
+ * The `<canvas>` has an `aria-label` describing it as a "QR code for {value}".
53
+ * Screen readers will announce the purpose, though the raw value is exposed
54
+ * (consider truncating for secrets).
55
+ */
56
+ export function QrCodeBase({
57
+ value,
58
+ canvasSize = 200,
59
+ level = "M",
60
+ bgColor = "#ffffff",
61
+ fgColor = "#000000",
62
+ margin = 2,
63
+ caption,
64
+ className,
65
+ ref,
66
+ ...rest
67
+ }: QrCodeBaseProps) {
68
+ const canvasRef = useRef<HTMLCanvasElement>(null);
69
+
70
+ /**
71
+ * Draw the QR pattern whenever input props change.
72
+ *
73
+ * The conditional `if (!value)` branch handles the "clear" case: when the
74
+ * user deletes the input text, we clear the canvas so a stale QR code isn't
75
+ * displayed. Without this the old QR pattern persists because
76
+ * `QRCode.toCanvas` is never called (it returns early for empty values).
77
+ */
78
+ useEffect(() => {
79
+ const canvas = canvasRef.current;
80
+ if (!canvas) return;
81
+
82
+ if (!value) {
83
+ const ctx = canvas.getContext("2d");
84
+ if (ctx) {
85
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
86
+ }
87
+ return;
88
+ }
89
+
90
+ QRCode.toCanvas(
91
+ canvas,
92
+ value,
93
+ {
94
+ width: canvasSize,
95
+ margin,
96
+ color: {
97
+ dark: fgColor,
98
+ light: bgColor,
99
+ },
100
+ errorCorrectionLevel: level,
101
+ },
102
+ (error) => {
103
+ if (error) {
104
+ console.error("QR Code generation error:", error);
105
+ }
106
+ },
107
+ );
108
+ }, [value, canvasSize, level, bgColor, fgColor, margin]);
109
+
110
+ /**
111
+ * The root element is a `<div>` (not the `<canvas>`) so consumers can apply
112
+ * layout classes, the caption sits outside the canvas, and the component
113
+ * plays well with flex/grid containers.
114
+ *
115
+ * Nested structure:
116
+ * <div data-slot="qr-code"> ← root (ref, className, ...rest)
117
+ * <div> ← canvas wrapper (theme rounding/overflow)
118
+ * <canvas data-slot="qr-code-canvas" />
119
+ * </div>
120
+ * {caption && <span data-slot="qr-code-caption" />}
121
+ * </div>
122
+ */
123
+ return (
124
+ <div
125
+ ref={ref}
126
+ data-slot="qr-code"
127
+ className={cn(qrCodeVariants(), className)}
128
+ {...rest}
129
+ >
130
+ <div className={qrCodeCanvasWrapperVariants()}>
131
+ <canvas
132
+ ref={canvasRef}
133
+ width={canvasSize}
134
+ height={canvasSize}
135
+ data-slot="qr-code-canvas"
136
+ className="block h-auto max-w-full"
137
+ aria-label={`QR code for ${value}`}
138
+ />
139
+ </div>
140
+ {caption ? (
141
+ <span data-slot="qr-code-caption" className={qrCodeCaptionVariants()}>
142
+ {caption}
143
+ </span>
144
+ ) : null}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ QrCodeBase.displayName = "QrCode";
@@ -0,0 +1,58 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { QrCode } from "./qr-code";
6
+
7
+ vi.mock("qrcode", () => ({
8
+ default: {
9
+ toCanvas: vi.fn(
10
+ (
11
+ _canvas: HTMLCanvasElement,
12
+ _value: string,
13
+ _opts: unknown,
14
+ cb?: (err?: Error) => void,
15
+ ) => {
16
+ cb?.();
17
+ },
18
+ ),
19
+ },
20
+ }));
21
+
22
+ describe("QrCode", () => {
23
+ it("should expose displayName", () => {
24
+ expect(QrCode.displayName).toBe("QrCode");
25
+ });
26
+
27
+ it("should stamp data-slot", () => {
28
+ render(<QrCode value="https://example.com" />);
29
+ const root = document.querySelector('[data-slot="qr-code"]');
30
+ expect(root).toBeTruthy();
31
+ expect(root?.getAttribute("data-slot")).toBe("qr-code");
32
+ });
33
+
34
+ it("should render canvas element", () => {
35
+ render(<QrCode value="https://example.com" />);
36
+ const canvas = document.querySelector('[data-slot="qr-code-canvas"]');
37
+ expect(canvas).toBeTruthy();
38
+ });
39
+
40
+ it("should render with caption", () => {
41
+ render(<QrCode value="test" caption="Scan me" />);
42
+ expect(screen.getByText("Scan me")).toBeInTheDocument();
43
+ });
44
+
45
+ it("should forward ref", () => {
46
+ const ref = createRef<HTMLDivElement>();
47
+ render(<QrCode value="test" ref={ref} />);
48
+ expect(ref.current?.getAttribute("data-slot")).toBe("qr-code");
49
+ });
50
+
51
+ it("should apply custom className", () => {
52
+ const { container } = render(
53
+ <QrCode value="test" className="custom-class" />,
54
+ );
55
+ const root = container.querySelector('[data-slot="qr-code"]');
56
+ expect(root?.className).toMatch(/custom-class/);
57
+ });
58
+ });
@@ -0,0 +1,2 @@
1
+ // qr-code.tsx — default static entry (no framer-motion)
2
+ export { QrCodeBase as QrCode } from "./qr-code-base";
@@ -0,0 +1,22 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+
3
+ export type QrCodeLevel = "L" | "M" | "Q" | "H";
4
+
5
+ export interface QrCodeBaseProps extends ComponentPropsWithRef<"div"> {
6
+ value: string;
7
+ canvasSize?: number;
8
+ level?: QrCodeLevel;
9
+ bgColor?: string;
10
+ fgColor?: string;
11
+ margin?: number;
12
+ caption?: ReactNode;
13
+ }
14
+
15
+ export type QrCodeProps = QrCodeBaseProps;
16
+
17
+ export const QR_CODE_LEVEL_LABELS: Record<QrCodeLevel, string> = {
18
+ L: "Low (7%)",
19
+ M: "Medium (15%)",
20
+ Q: "Quartile (25%)",
21
+ H: "High (30%)",
22
+ } as const;
@@ -0,0 +1,11 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiQrCodeBase,
5
+ zuiQrCodeCanvasWrapper,
6
+ zuiQrCodeCaptionBase,
7
+ } from "../../design-system/qr-code";
8
+
9
+ export const qrCodeVariants = cva(zuiQrCodeBase);
10
+ export const qrCodeCanvasWrapperVariants = cva(zuiQrCodeCanvasWrapper);
11
+ export const qrCodeCaptionVariants = cva(zuiQrCodeCaptionBase);
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ export { QrScanner } from "./qr-scanner";
4
+ export type {
5
+ QrScannerBaseProps,
6
+ QrScannerProps,
7
+ QrScannerRef,
8
+ QrScannerVariantProps,
9
+ } from "./types";
10
+ export {
11
+ qrScannerFallbackVariants,
12
+ qrScannerOverlayVariants,
13
+ qrScannerStatusVariants,
14
+ qrScannerVariants,
15
+ qrScannerVideoVariants,
16
+ qrScannerViewfinderVariants,
17
+ } from "./variants";