@kitn.ai/ui 0.19.0 → 0.20.1

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 (236) hide show
  1. package/LICENSE +6 -0
  2. package/NOTICE +54 -0
  3. package/dist/components/audio-visualizer/audio-visualizer.voice-fixture.d.ts +59 -0
  4. package/dist/components/audio-visualizer/aurora.glsl.d.ts +114 -0
  5. package/dist/components/audio-visualizer/index.d.ts +155 -0
  6. package/dist/components/audio-visualizer/shader-canvas.d.ts +130 -0
  7. package/dist/components/audio-visualizer/sizes.d.ts +25 -0
  8. package/dist/components/audio-visualizer/variant-aurora.d.ts +89 -0
  9. package/dist/components/audio-visualizer/variant-bar.d.ts +46 -0
  10. package/dist/components/audio-visualizer/variant-custom.d.ts +73 -0
  11. package/dist/components/audio-visualizer/variant-grid.d.ts +22 -0
  12. package/dist/components/audio-visualizer/variant-radial.d.ts +13 -0
  13. package/dist/components/audio-visualizer/variant-wave.d.ts +11 -0
  14. package/dist/components/audio-visualizer/wave.glsl.d.ts +16 -0
  15. package/dist/create-tween-hvpmSuwW.js +7 -0
  16. package/dist/custom-elements.json +282 -0
  17. package/dist/elements/agent-card.js +1 -1
  18. package/dist/elements/artifact.js +1 -1
  19. package/dist/elements/attachments.js +1 -1
  20. package/dist/elements/audio-visualizer.d.ts +1 -0
  21. package/dist/elements/audio-visualizer.js +1 -0
  22. package/dist/elements/autoloader.js +1 -1
  23. package/dist/elements/avatar.js +1 -1
  24. package/dist/elements/badge.js +1 -1
  25. package/dist/elements/button.js +1 -1
  26. package/dist/elements/card.js +1 -1
  27. package/dist/elements/cards.js +1 -1
  28. package/dist/elements/chain-of-thought.js +1 -1
  29. package/dist/elements/chat-scope-picker.js +1 -1
  30. package/dist/elements/chat-workspace.js +1 -1
  31. package/dist/elements/chat.js +1 -1
  32. package/dist/elements/checkpoint.js +1 -1
  33. package/dist/elements/choice.js +1 -1
  34. package/dist/elements/chunks/{Icon-11SRdjJ4.js → Icon-Tj58ES_d.js} +1 -1
  35. package/dist/elements/chunks/{action-icons-CtRg17uX.js → action-icons-ns1Fnedj.js} +1 -1
  36. package/dist/elements/chunks/arrow-left-DQocUFvy.js +1 -0
  37. package/dist/elements/chunks/{attachments-CBqQRh60.js → attachments-mCB5F1Fq.js} +1 -1
  38. package/dist/elements/chunks/audio-visualizer-BqNy7dfm.js +1 -0
  39. package/dist/elements/chunks/{badge-CxklEXQm.js → badge-B4Zuajl2.js} +1 -1
  40. package/dist/elements/chunks/{button-BD7lUvVB.js → button-ZWNif3--.js} +1 -1
  41. package/dist/elements/chunks/{card-B-PRGXdj.js → card-BkHbp4BA.js} +1 -1
  42. package/dist/elements/chunks/{chat-thread-DIZbRIKG.js → chat-thread-CkRN6pZ7.js} +1 -1
  43. package/dist/elements/chunks/check-B9-gn6SS.js +1 -0
  44. package/dist/elements/chunks/chevron-down-BNsxc2hk.js +1 -0
  45. package/dist/elements/chunks/chevron-right-Hv00StBO.js +1 -0
  46. package/dist/elements/chunks/circle-0e3KJTDr.js +1 -0
  47. package/dist/elements/chunks/circle-check-BNAS0JEy.js +1 -0
  48. package/dist/elements/chunks/{circle-x-mtjCO_Ok.js → circle-x-B7faJFlY.js} +1 -1
  49. package/dist/elements/chunks/{code-block-BeWVttmU.js → code-block-BFUHn6Va.js} +1 -1
  50. package/dist/elements/chunks/collapsible-DM1Ckd4e.js +1 -0
  51. package/dist/elements/chunks/{composer-CavJww3d.js → composer-BguS0QL_.js} +2 -2
  52. package/dist/elements/chunks/{context-CnlsNJSJ.js → context-Cb7k4F-v.js} +1 -1
  53. package/dist/elements/chunks/{conversation-list-5O_KdI_I.js → conversation-list-CvoAkWhN.js} +1 -1
  54. package/dist/elements/chunks/create-tween-oVvAIG8r.js +7 -0
  55. package/dist/elements/chunks/{default-input-CB46cJJ1.js → default-input-Cqqilg9t.js} +1 -1
  56. package/dist/elements/chunks/define-D1din7Ld.js +1 -0
  57. package/dist/elements/chunks/{disclosure-BtFK3a-M.js → disclosure-CIZ5c8nO.js} +1 -1
  58. package/dist/elements/chunks/{download-qJv1dS3M.js → download-Dsbg3ste.js} +1 -1
  59. package/dist/elements/chunks/{dropdown-UhtAueYR.js → dropdown-BDd1-Zgi.js} +1 -1
  60. package/dist/elements/chunks/{ellipsis-B1sOOqiJ.js → ellipsis-Dw6qrss-.js} +1 -1
  61. package/dist/elements/chunks/{external-link-DToP_VKE.js → external-link-Vt74zAoL.js} +1 -1
  62. package/dist/elements/chunks/{file-text-BwUs_Pj4.js → file-text-CZrZF6_v.js} +1 -1
  63. package/dist/elements/chunks/{file-tree-C6RhmSnX.js → file-tree-DF4okKyo.js} +1 -1
  64. package/dist/elements/chunks/{folder-H_Xp0GYP.js → folder-glP5rha4.js} +1 -1
  65. package/dist/elements/chunks/{hover-card-Dn3yrk1l.js → hover-card-Cmm6Q53m.js} +1 -1
  66. package/dist/elements/chunks/{icon-BrxJQXzC.js → icon-BLb8beoi.js} +1 -1
  67. package/dist/elements/chunks/{info-11eT2q5H.js → info-CIsv92JK.js} +1 -1
  68. package/dist/elements/chunks/{input-B9oZKDfb.js → input-Dds1kFyt.js} +1 -1
  69. package/dist/elements/chunks/{kbd-D2fx3rWh.js → kbd-De_xbgi9.js} +1 -1
  70. package/dist/elements/chunks/{link-B-RNYWNw.js → link-i4r_k7z4.js} +1 -1
  71. package/dist/elements/chunks/{loader-BJh_aRlp.js → loader-BmQH4s3V.js} +1 -1
  72. package/dist/elements/chunks/{markdown-BIpjrDFv.js → markdown-Zi21WhUz.js} +7 -7
  73. package/dist/elements/chunks/{message-CPfqj8Gi.js → message-BqyXDN3-.js} +1 -1
  74. package/dist/elements/chunks/{message-feedback-8uj0aVYh.js → message-feedback-Djr3z6Yb.js} +1 -1
  75. package/dist/elements/chunks/message-square-vFAgbDaX.js +1 -0
  76. package/dist/elements/chunks/{minimize-2-C9qkiTJg.js → minimize-2--ifeCGrl.js} +1 -1
  77. package/dist/elements/chunks/{model-switcher-DRrMcRXH.js → model-switcher-B94YAKsM.js} +1 -1
  78. package/dist/elements/chunks/{overlay-C8_FUL8q.js → overlay-kD8dwRv_.js} +1 -1
  79. package/dist/elements/chunks/{paperclip-MA9hxLhW.js → paperclip-q21dYOSK.js} +1 -1
  80. package/dist/elements/chunks/{progress-bar-y5G6HDjg.js → progress-bar-BrAIIaaJ.js} +1 -1
  81. package/dist/elements/chunks/{prompt-suggestion-LalWvCJy.js → prompt-suggestion-Bjn2K-fR.js} +1 -1
  82. package/dist/elements/chunks/reasoning-DTpCGtnY.js +1 -0
  83. package/dist/elements/chunks/{resizable-xhYF5lCQ.js → resizable-Dp4JExql.js} +1 -1
  84. package/dist/elements/chunks/{rotate-cw-2ndEXjgS.js → rotate-cw-CFWWkpj2.js} +1 -1
  85. package/dist/elements/chunks/{scroll-area-BzOPDXrZ.js → scroll-area-Bb0kHH1j.js} +1 -1
  86. package/dist/elements/chunks/scroll-button-B0cH5fXf.js +1 -0
  87. package/dist/elements/chunks/{separator-CPvL5Xdl.js → separator-9kRgr1X9.js} +1 -1
  88. package/dist/elements/chunks/{settings-hmzIuNXD.js → settings-B8lmec4D.js} +1 -1
  89. package/dist/elements/chunks/{settings-group-BiIym17O.js → settings-group-I0hEuXiN.js} +1 -1
  90. package/dist/elements/chunks/{share-BbziTAzn.js → share-C3lTiYGt.js} +1 -1
  91. package/dist/elements/chunks/{skeleton-Bxhs1thL.js → skeleton-DSXW5Yya.js} +1 -1
  92. package/dist/elements/chunks/{star-BHL3Va5p.js → star-CSJtGxgv.js} +1 -1
  93. package/dist/elements/chunks/{store-tpnnWlxY.js → store-CHyW95TP.js} +1 -1
  94. package/dist/elements/chunks/{text-shimmer-1DCKivKx.js → text-shimmer-Cn9TmNpI.js} +1 -1
  95. package/dist/elements/chunks/{textarea-Czaowo9F.js → textarea-CUwqAX0G.js} +1 -1
  96. package/dist/elements/chunks/{thumbs-up-CdLqA13R.js → thumbs-up-PNVVThEF.js} +1 -1
  97. package/dist/elements/chunks/{toast-store-BvBquUFA.js → toast-store-DKbQn3Wn.js} +1 -1
  98. package/dist/elements/chunks/{tool-4P_ne8Go.js → tool-DjTRpIpl.js} +1 -1
  99. package/dist/elements/chunks/tooltip-CgBZCVKp.js +1 -0
  100. package/dist/elements/chunks/{triangle-alert-BllqxXq8.js → triangle-alert-DsHTHCPm.js} +1 -1
  101. package/dist/elements/chunks/{use-card-resolution-73xiddIW.js → use-card-resolution-BSu_XHjH.js} +1 -1
  102. package/dist/elements/chunks/variant-aurora-Dc4nQUia.js +218 -0
  103. package/dist/elements/chunks/variant-custom-y_3kVQhS.js +1 -0
  104. package/dist/elements/chunks/variant-wave-D5OR1-9Z.js +116 -0
  105. package/dist/elements/chunks/{video-O9rA6k7S.js → video-B7vmenoC.js} +1 -1
  106. package/dist/elements/chunks/x-GRUeeQFT.js +1 -0
  107. package/dist/elements/coachmark.js +1 -1
  108. package/dist/elements/code-block.js +1 -1
  109. package/dist/elements/command.js +1 -1
  110. package/dist/elements/compare.js +1 -1
  111. package/dist/elements/composer.js +1 -1
  112. package/dist/elements/confirm-card.js +1 -1
  113. package/dist/elements/context-meter.js +1 -1
  114. package/dist/elements/conversation-list.js +1 -1
  115. package/dist/elements/define.d.ts +11 -0
  116. package/dist/elements/dialog.js +1 -1
  117. package/dist/elements/editable-label.js +1 -1
  118. package/dist/elements/embed.js +1 -1
  119. package/dist/elements/empty.js +1 -1
  120. package/dist/elements/feedback-bar.js +1 -1
  121. package/dist/elements/file-tree.js +1 -1
  122. package/dist/elements/file-upload.js +1 -1
  123. package/dist/elements/form.js +1 -1
  124. package/dist/elements/hover-card.js +1 -1
  125. package/dist/elements/icon.js +1 -1
  126. package/dist/elements/image.js +1 -1
  127. package/dist/elements/input.js +1 -1
  128. package/dist/elements/kbd.js +1 -1
  129. package/dist/elements/link-preview.js +1 -1
  130. package/dist/elements/loader.js +1 -1
  131. package/dist/elements/markdown.js +1 -1
  132. package/dist/elements/menu.js +1 -1
  133. package/dist/elements/message-skills.js +1 -1
  134. package/dist/elements/message.js +1 -1
  135. package/dist/elements/model-switcher.js +1 -1
  136. package/dist/elements/nav.js +1 -1
  137. package/dist/elements/notice.js +1 -1
  138. package/dist/elements/pane-group.js +1 -1
  139. package/dist/elements/pane.js +1 -1
  140. package/dist/elements/popover.js +1 -1
  141. package/dist/elements/progress-bar.js +1 -1
  142. package/dist/elements/prompt-dock.js +1 -1
  143. package/dist/elements/prompt-input.js +1 -1
  144. package/dist/elements/prompt-suggestions.js +1 -1
  145. package/dist/elements/reasoning.js +1 -1
  146. package/dist/elements/remote.js +1 -1
  147. package/dist/elements/resizable.js +1 -1
  148. package/dist/elements/response-stream.js +1 -1
  149. package/dist/elements/screen.js +1 -1
  150. package/dist/elements/scroll-area.js +1 -1
  151. package/dist/elements/scroll-button.js +1 -1
  152. package/dist/elements/search.js +1 -1
  153. package/dist/elements/segmented.js +1 -1
  154. package/dist/elements/separator.js +1 -1
  155. package/dist/elements/setting-item.js +1 -1
  156. package/dist/elements/settings-group.js +1 -1
  157. package/dist/elements/skeleton.js +1 -1
  158. package/dist/elements/slots.d.ts +12 -0
  159. package/dist/elements/source.js +1 -1
  160. package/dist/elements/status.js +1 -1
  161. package/dist/elements/switch.js +1 -1
  162. package/dist/elements/tabs.js +1 -1
  163. package/dist/elements/tasks.js +1 -1
  164. package/dist/elements/text-shimmer.js +1 -1
  165. package/dist/elements/thinking-bar.js +1 -1
  166. package/dist/elements/thread.js +1 -1
  167. package/dist/elements/toast.js +1 -1
  168. package/dist/elements/tool.js +1 -1
  169. package/dist/elements/tooltip.js +1 -1
  170. package/dist/elements/voice-input.js +1 -1
  171. package/dist/elements/voice-output.js +1 -1
  172. package/dist/elements.d.ts +38 -0
  173. package/dist/index.js +18 -17
  174. package/dist/index.server.js +16404 -0
  175. package/dist/kai.es.js +1 -1
  176. package/dist/llms/llms-full.txt +40 -4
  177. package/dist/llms/llms.txt +3 -3
  178. package/dist/primitives/audio-bands.d.ts +106 -0
  179. package/dist/primitives/create-tween.d.ts +33 -0
  180. package/dist/primitives/use-audio-analysis.d.ts +149 -0
  181. package/dist/primitives/use-sequencer.d.ts +13 -0
  182. package/dist/primitives/use-voice-recorder.d.ts +1 -0
  183. package/dist/primitives/visualizer-sequences.d.ts +39 -0
  184. package/dist/react/index.d.ts +41 -0
  185. package/dist/react.js +159 -153
  186. package/dist/register-impl-DQyejdfb.js +145 -0
  187. package/dist/test-utils/fake-clock.d.ts +45 -0
  188. package/dist/variant-aurora-DVtGqVH6.js +218 -0
  189. package/dist/variant-custom-Dif-IV4u.js +1 -0
  190. package/dist/variant-wave-DbfNdvSi.js +116 -0
  191. package/frameworks/react/index.tsx +42 -0
  192. package/llms-full.txt +40 -4
  193. package/llms.txt +3 -3
  194. package/package.json +11 -3
  195. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +257 -0
  196. package/src/components/audio-visualizer/aurora.glsl.ts +330 -0
  197. package/src/components/audio-visualizer/index.tsx +442 -0
  198. package/src/components/audio-visualizer/shader-canvas.tsx +846 -0
  199. package/src/components/audio-visualizer/sizes.ts +54 -0
  200. package/src/components/audio-visualizer/variant-aurora.tsx +299 -0
  201. package/src/components/audio-visualizer/variant-bar.tsx +143 -0
  202. package/src/components/audio-visualizer/variant-custom.tsx +338 -0
  203. package/src/components/audio-visualizer/variant-grid.tsx +170 -0
  204. package/src/components/audio-visualizer/variant-radial.tsx +158 -0
  205. package/src/components/audio-visualizer/variant-wave.tsx +125 -0
  206. package/src/components/audio-visualizer/wave.glsl.ts +130 -0
  207. package/src/components/component-meta.json +24 -24
  208. package/src/elements/audio-visualizer.tsx +183 -0
  209. package/src/elements/compiled.css +1 -1
  210. package/src/elements/define.tsx +12 -1
  211. package/src/elements/element-manifest.json +4 -0
  212. package/src/elements/element-meta.json +170 -0
  213. package/src/elements/element-types.d.ts +38 -0
  214. package/src/elements/register-impl.ts +1 -0
  215. package/src/elements/slots.ts +35 -0
  216. package/src/primitives/audio-bands.ts +174 -0
  217. package/src/primitives/create-tween.ts +214 -0
  218. package/src/primitives/use-audio-analysis.ts +321 -0
  219. package/src/primitives/use-sequencer.ts +53 -0
  220. package/src/primitives/use-voice-recorder.ts +16 -4
  221. package/src/primitives/visualizer-sequences.ts +350 -0
  222. package/src/test-utils/fake-clock.ts +88 -0
  223. package/dist/elements/chunks/arrow-left-CifsGq8x.js +0 -1
  224. package/dist/elements/chunks/check-CUKS495w.js +0 -1
  225. package/dist/elements/chunks/chevron-down-AELmYT1j.js +0 -1
  226. package/dist/elements/chunks/chevron-right-C8g26oKh.js +0 -1
  227. package/dist/elements/chunks/circle-DGcI8Cft.js +0 -1
  228. package/dist/elements/chunks/circle-check-DuU0IcBQ.js +0 -1
  229. package/dist/elements/chunks/collapsible-DGjADCN6.js +0 -1
  230. package/dist/elements/chunks/define-DfTe4zwZ.js +0 -1
  231. package/dist/elements/chunks/message-square-CCSQ4M9g.js +0 -1
  232. package/dist/elements/chunks/reasoning-CW3SDdb4.js +0 -1
  233. package/dist/elements/chunks/scroll-button-B1x4cYqM.js +0 -1
  234. package/dist/elements/chunks/tooltip-CBeFqJKk.js +0 -1
  235. package/dist/elements/chunks/x-DeS3rQhx.js +0 -1
  236. package/dist/register-impl-CaaHJcea.js +0 -145
@@ -0,0 +1,442 @@
1
+ import {
2
+ createSignal, createEffect, onCleanup, Show, Switch, Match,
3
+ type Accessor, type Component, type JSX,
4
+ } from 'solid-js';
5
+ import { cn } from '../../utils/cn';
6
+ import { useAudioAnalysis } from '../../primitives/use-audio-analysis';
7
+ import { mirrorBandsCenterOut, mirrorBandsAroundRing } from '../../primitives/audio-bands';
8
+ import { normalizeState, type VisualizerState } from '../../primitives/visualizer-sequences';
9
+ import { BarVisualizer, type VariantProps } from './variant-bar';
10
+ import { GridVisualizer } from './variant-grid';
11
+ import { RadialVisualizer } from './variant-radial';
12
+ import { defaultBarCount, defaultGridCount, defaultRadialBarCount, type VisualizerSize } from './sizes';
13
+ import type { UniformType } from './shader-canvas';
14
+
15
+ export type VisualizerVariant = 'bar' | 'grid' | 'radial' | 'wave' | 'aurora' | 'custom';
16
+
17
+ const KNOWN_VARIANTS: readonly VisualizerVariant[] = ['bar', 'grid', 'radial', 'wave', 'aurora', 'custom'];
18
+
19
+ /**
20
+ * `aura` is LiveKit's name for this look. Ours is `aurora`. Accept theirs so
21
+ * markup ported from LiveKit works unchanged, the same way `normalizeState`
22
+ * accepts their room-lifecycle state names.
23
+ */
24
+ const VARIANT_ALIASES: Record<string, VisualizerVariant> = { aura: 'aurora' };
25
+
26
+ export function normalizeVariant(input: string | undefined): VisualizerVariant {
27
+ if (!input) return 'bar';
28
+ if ((KNOWN_VARIANTS as readonly string[]).includes(input)) return input as VisualizerVariant;
29
+ return VARIANT_ALIASES[input] ?? 'bar';
30
+ }
31
+
32
+ /** A consumer-supplied fragment shader, for `variant="custom"`. */
33
+ export interface ShaderSpec {
34
+ /**
35
+ * GLSL source defining `mainImage(out vec4 fragColor, in vec2 fragCoord)`.
36
+ *
37
+ * MUST output premultiplied colour: `fragColor = vec4(rgb * alpha, alpha);`,
38
+ * never `vec4(rgb, alpha)`. The canvas composites using the browser's
39
+ * default `premultipliedAlpha: true`; a translucent edge written the
40
+ * natural (straight-alpha) way gets a dark fringe where it meets a light
41
+ * page background.
42
+ */
43
+ fragment: string;
44
+ /**
45
+ * Custom uniforms. The canvas DECLARES these for you; declaring them in the
46
+ * shader too is a compile error.
47
+ *
48
+ * `type` is narrowed to `UniformType`, not a bare string: an unrecognized
49
+ * type here is caught by TypeScript for a TS-authored `shader` prop, before
50
+ * it can produce `uniform undefined <name>;` in the assembled source (a
51
+ * confusing compile error). A consumer reaching this through the
52
+ * `<kai-audio-visualizer>` custom element or an attribute-driven wrapper
53
+ * bypasses this check entirely (it is JS at that boundary, not TS) --
54
+ * `variant-custom.tsx`'s `customUniforms` re-checks `type` at runtime for
55
+ * exactly that reason.
56
+ */
57
+ uniforms?: Record<string, { type: UniformType; value: number | number[] }>;
58
+ }
59
+
60
+ export interface AudioVisualizerProps {
61
+ variant?: VisualizerVariant;
62
+ state?: string;
63
+ size?: VisualizerSize;
64
+ barCount?: number;
65
+ /** Grid only: rows and columns of the square grid. */
66
+ count?: number;
67
+ /** Radial only: distance from center, in px. */
68
+ radius?: number;
69
+ /** Grid only: ring distance for the connecting animation, in cells. */
70
+ spread?: number;
71
+ /** Grid only: ms between scripted frames. */
72
+ interval?: number;
73
+ color?: string;
74
+ /** Shader variants only: pattern density, 0..1. */
75
+ complexity?: number;
76
+ /** Setting this makes the element an announced image instead of decorative. */
77
+ label?: string;
78
+ stream?: MediaStream;
79
+ audioElement?: HTMLMediaElement;
80
+ /** Pre-computed levels. Set this and no AudioContext is ever constructed. */
81
+ bands?: number[];
82
+ /**
83
+ * Custom fragment shader for `variant="custom"`. See `ShaderSpec` for the
84
+ * full contract -- most importantly, `fragment` MUST output premultiplied
85
+ * colour (`vec4(rgb * alpha, alpha)`, not `vec4(rgb, alpha)`), or
86
+ * translucent edges get dark fringes on light backgrounds.
87
+ */
88
+ shader?: ShaderSpec;
89
+ /**
90
+ * Shader variants only (`wave`/`aurora`/`custom`): keep animating while the
91
+ * element is scrolled off screen. Default `false`, which stops the draw loop
92
+ * and releases the WebGL context until it comes back -- see `ShaderCanvas`'s
93
+ * `animateWhenNotVisible` for the context-budget reasoning and the cost of
94
+ * opting out. Does not override `prefers-reduced-motion`.
95
+ */
96
+ animateWhenNotVisible?: boolean;
97
+ class?: string;
98
+ /**
99
+ * Render each DOM variant's items yourself -- the same render-prop the
100
+ * underlying bar/grid/radial component takes, imported rather than
101
+ * redeclared so the two cannot drift. Not used by the shader variants
102
+ * (`wave`/`aurora`/`custom`): a fragment shader has no per-item DOM for it
103
+ * to replace.
104
+ */
105
+ children?: VariantProps['children'];
106
+ /**
107
+ * Explicit `'light'` or `'dark'` wins; `'auto'` (the default) follows a
108
+ * live `prefers-color-scheme` listener -- the same rule
109
+ * `elements/define.tsx`'s `createDarkMode` applies for every `kai-*`
110
+ * element. Only the shader variants read this (aurora/wave pick a colour
111
+ * pipeline with it): the three DOM variants already get dark-mode styling
112
+ * for free via CSS custom properties, which a shader baking colour into a
113
+ * GLSL uniform cannot do. `<kai-audio-visualizer>` forwards its own
114
+ * already-resolved `theme` attribute through this prop; a bare
115
+ * `<AudioVisualizer>` with no wrapping element needs it set directly.
116
+ */
117
+ theme?: 'light' | 'dark' | 'auto';
118
+ }
119
+
120
+ /** Tracks `prefers-reduced-motion`, live. */
121
+ export function usePrefersReducedMotion(): Accessor<boolean> {
122
+ const [reduced, setReduced] = createSignal(false);
123
+
124
+ createEffect(() => {
125
+ if (typeof matchMedia !== 'function') return;
126
+ const mq = matchMedia('(prefers-reduced-motion: reduce)');
127
+ setReduced(mq.matches);
128
+ const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
129
+ mq.addEventListener?.('change', onChange);
130
+ onCleanup(() => mq.removeEventListener?.('change', onChange));
131
+ });
132
+
133
+ return reduced;
134
+ }
135
+
136
+ /**
137
+ * Resolves `theme` (`'light' | 'dark' | 'auto'`) to a boolean, mirroring
138
+ * `elements/define.tsx`'s `createDarkMode` rule exactly: an explicit value
139
+ * wins, `'auto'` (the default) follows a live `prefers-color-scheme`
140
+ * listener.
141
+ *
142
+ * This is a SEPARATE implementation of that rule, not an import of it:
143
+ * `components/` is the framework-agnostic layer `elements/` wraps (see the
144
+ * kit's architecture), so it cannot depend on `elements/define.tsx` without
145
+ * inverting that direction. When driven through `<kai-audio-visualizer>`,
146
+ * the facade has already resolved `'auto'` against its OWN listener (the one
147
+ * already wired to the visible `.dark` class) before handing this an
148
+ * explicit `'light'`/`'dark'` -- so this hook's own listener only ever
149
+ * actually decides anything for a bare `<AudioVisualizer>` used directly,
150
+ * with no wrapping element at all.
151
+ */
152
+ export function useResolvedDark(theme: () => string | undefined): Accessor<boolean> {
153
+ const [systemDark, setSystemDark] = createSignal(false);
154
+
155
+ createEffect(() => {
156
+ if (typeof matchMedia !== 'function') return;
157
+ const mq = matchMedia('(prefers-color-scheme: dark)');
158
+ setSystemDark(mq.matches);
159
+ const onChange = (e: MediaQueryListEvent) => setSystemDark(e.matches);
160
+ mq.addEventListener?.('change', onChange);
161
+ onCleanup(() => mq.removeEventListener?.('change', onChange));
162
+ });
163
+
164
+ return () => {
165
+ const t = theme() ?? 'auto';
166
+ return t === 'dark' || (t === 'auto' && systemDark());
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Props a shader variant (`wave`, `aurora`, `custom`) receives from the
172
+ * dispatcher: everything a DOM variant gets, minus `children` (a fragment
173
+ * shader has no per-item DOM for a render-prop to replace, so the field is
174
+ * dropped rather than inherited-but-unused), plus the scalar `volume` reading
175
+ * (shaders read one number, not per-band levels) and the shader-specific
176
+ * knobs. This is the compile-time contract Tasks 12 to 15 build against --
177
+ * keep it honest rather than casting around a looser type at the render site.
178
+ */
179
+ export interface ShaderVariantProps extends Omit<VariantProps, 'children'> {
180
+ volume: number;
181
+ /** Pattern density, 0..1. */
182
+ complexity?: number;
183
+ /** Only meaningful for `variant="custom"`. */
184
+ shader?: ShaderSpec;
185
+ /**
186
+ * Already-resolved: `true` selects the dark colour pipeline, `false`
187
+ * selects light -- matching `elements/define.tsx`'s `createDarkMode`
188
+ * output exactly (`classList={{ dark: isDark() }}`). This is that SAME
189
+ * resolved value forwarded down, not a re-derivation, so a shader baking
190
+ * colour into a GLSL uniform never needs its own `prefers-color-scheme`
191
+ * listener for the common case. Optional: a shader mounted standalone (no
192
+ * dispatcher resolving `theme` above it) may fall back to reading the
193
+ * media query itself.
194
+ */
195
+ dark?: boolean;
196
+ /**
197
+ * Keep animating while off screen instead of releasing the WebGL context.
198
+ * Relayed straight through to `ShaderCanvas`, which owns the behaviour and
199
+ * documents the trade-off; the variants are pure conduits for it.
200
+ */
201
+ animateWhenNotVisible?: boolean;
202
+ /**
203
+ * Call this if the shader cannot render at all -- most commonly
204
+ * `canvas.getContext('webgl')` returning null. Permanent for this mount:
205
+ * the dispatcher swaps to the bar fallback and will not retry the shader
206
+ * on any later reactive update, only on switching to a different variant.
207
+ */
208
+ onUnavailable: () => void;
209
+ }
210
+
211
+ /**
212
+ * Shader variants live behind a dynamic import so the WebGL runtime and the
213
+ * GLSL strings (about 25 to 30 KB) never reach a consumer who does not ask for
214
+ * them.
215
+ *
216
+ * This MUST stay dynamic. `vite.config.ts` disables tree-shaking on the
217
+ * register-all bundle by design, so a static import here would put the whole
218
+ * shader path into `kai.es.js` for everyone, including a `<kai-chat>`-only
219
+ * user. A dynamic import splits into its own chunk under `treeshake: false`;
220
+ * a static one does not (verified empirically). Never convert these entries
221
+ * to static imports.
222
+ */
223
+ const SHADER_VARIANTS: Record<string, () => Promise<{ default: Component<ShaderVariantProps> }>> = {
224
+ wave: () => import('./variant-wave'),
225
+ aurora: () => import('./variant-aurora'),
226
+ custom: () => import('./variant-custom'),
227
+ };
228
+
229
+ export function AudioVisualizer(props: AudioVisualizerProps): JSX.Element {
230
+ const variant = () => normalizeVariant(props.variant);
231
+ const size = () => props.size ?? 'md';
232
+ const state = (): VisualizerState => normalizeState(props.state);
233
+ const reduced = usePrefersReducedMotion();
234
+ // Only the shader match arm below reads this; the three DOM variants get
235
+ // dark-mode styling for free via CSS custom properties.
236
+ const resolvedDark = useResolvedDark(() => props.theme);
237
+
238
+ const isShader = () => variant() in SHADER_VARIANTS;
239
+
240
+ // How many elements the active variant actually renders. Grid keys off its
241
+ // own column default (each column shares one band across every row -- see
242
+ // GridVisualizer), radial off its own bar-count default, everything else
243
+ // off the bar count -- pulled from `sizes.ts` rather than re-derived here,
244
+ // so this stays in sync with what each variant actually renders.
245
+ const elementCount = () => {
246
+ if (variant() === 'grid') return props.count ?? defaultGridCount(size());
247
+ if (variant() === 'radial') return props.barCount ?? defaultRadialBarCount(size());
248
+ return props.barCount ?? defaultBarCount(size());
249
+ };
250
+
251
+ // Only ceil(n/2) bands are requested from the analyser, not one per
252
+ // element: useAudioAnalysis's output is mirrored back out to the full
253
+ // elementCount below (centre-out for bar/grid, across the ring's vertical
254
+ // axis for radial), which is what turns a real voice's natural
255
+ // low-to-high spectral tilt into a shape that grows from the centre
256
+ // outward instead of always ramping in one direction. See
257
+ // mirrorBandsCenterOut / mirrorBandsAroundRing in primitives/audio-bands.ts
258
+ // for the full rationale and the real-clip measurements behind it.
259
+ const bandCount = () => Math.ceil(elementCount() / 2);
260
+
261
+ // A caller-supplied `bands` array short-circuits Web Audio entirely, which is
262
+ // what keeps the headless and SSR paths free of an AudioContext.
263
+ const source = () => (props.bands ? undefined : props.stream ?? props.audioElement);
264
+ // Pass the ACCESSOR, not its current value: useAudioAnalysis reads it inside
265
+ // its own effect, so a later variant/size/barCount change rebuilds the
266
+ // analyser at the new count instead of leaving it wired to whatever the
267
+ // count happened to be at mount.
268
+ const analysis = useAudioAnalysis(source, { bands: bandCount });
269
+
270
+ // A caller-supplied `bands` array is used exactly as given -- it already
271
+ // has whatever shape the caller intends, mirrored or not, and mirroring it
272
+ // again here would be a second, unwanted transform on data we don't own.
273
+ // The analyser's own output is only ceil(n/2) values (bandCount above);
274
+ // mirror it back out to the FULL elementCount so it lines up EXACTLY with
275
+ // what each variant's own `normalizeVolumeBands(props.bands, count())`
276
+ // expects. Matching the count exactly here, rather than leaning on that
277
+ // pad-by-repeating-the-last-value, matters specifically because the mirror
278
+ // makes a length mismatch look like a plausible (but wrong) shape instead
279
+ // of an obviously broken one.
280
+ const bands = () => {
281
+ if (props.bands) return props.bands;
282
+ return variant() === 'radial'
283
+ ? mirrorBandsAroundRing(analysis.bands(), elementCount())
284
+ : mirrorBandsCenterOut(analysis.bands(), elementCount());
285
+ };
286
+
287
+ // The shader variants read `volume`, a scalar, not `bands`. When
288
+ // caller-supplied `bands` short-circuits Web Audio above, `analysis.volume()`
289
+ // sits at its initial 0 forever (nothing ever computes it), so a shader
290
+ // fed `bands` would otherwise look static despite the DOM variants next to
291
+ // it correctly animating. Derive it from the same `bands` this component
292
+ // is already using -- root-mean-square, matching how `reduceToVolume` in
293
+ // `primitives/audio-bands.ts` reduces real analyser output to a scalar, so
294
+ // the two paths agree in character. Not reused directly: `reduceToVolume`
295
+ // takes a `Uint8Array` of byte frequency data (0..255), not a `number[]` of
296
+ // already-normalized 0..1 levels, so contorting it to accept both shapes
297
+ // would be worse than the few lines below.
298
+ const volume = () => {
299
+ if (!props.bands) return analysis.volume();
300
+ if (props.bands.length === 0) return 0;
301
+ let sum = 0;
302
+ for (const b of props.bands) sum += b * b;
303
+ return Math.sqrt(sum / props.bands.length);
304
+ };
305
+
306
+ // Lazily loaded shader component, or undefined until it resolves. Failure is
307
+ // not fatal: the bar fallback below stays on screen.
308
+ const [Shader, setShader] = createSignal<Component<ShaderVariantProps> | undefined>();
309
+ // A mounted shader can ALSO fail after its chunk loads fine -- most notably
310
+ // WebGL being unavailable, which only a mounted component can discover (it
311
+ // is the one calling `canvas.getContext('webgl')`). This is a separate flag
312
+ // from `Shader` itself: `Shader()` staying undefined already makes a failed
313
+ // *import* permanent (nothing ever sets it), but a shader that DID load and
314
+ // mount needs its own signal to force the fallback despite `Shader()` being
315
+ // truthy. `onUnavailable` below is what a mounted shader calls to set it.
316
+ const [unavailable, setUnavailable] = createSignal(false);
317
+ createEffect(() => {
318
+ const v = variant();
319
+ const load = SHADER_VARIANTS[v];
320
+ setShader(undefined);
321
+ // Switching variants gets a fresh attempt: a failure on `aurora` must not
322
+ // carry over and permanently block `wave` too.
323
+ setUnavailable(false);
324
+ if (!load) return;
325
+ let cancelled = false;
326
+ void load()
327
+ .then((m) => { if (!cancelled) setShader(() => m.default); })
328
+ .catch((err) => {
329
+ console.warn(
330
+ `<kai-audio-visualizer variant="${v}">: failed to load the shader chunk, falling back to bars.`,
331
+ err,
332
+ );
333
+ });
334
+ onCleanup(() => { cancelled = true; });
335
+ });
336
+
337
+ // Props every variant shares, MINUS `bands` -- deliberately. Solid compiles
338
+ // a component spread (`{...shared()}` below) into per-key getters that all
339
+ // call this SAME function: reading ANY one key re-invokes the whole thing,
340
+ // so if `bands()` lived in here, reading `state` (or `size`/`frozen`/
341
+ // `color`) would transitively subscribe the reader to `bands()` too, which
342
+ // updates ~31 times a second with live or synthetic audio. That is exactly
343
+ // what caused two real bugs downstream: `use-sequencer.ts`'s effect reading
344
+ // `frozen`/`state` re-ran at band cadence and called `setTick(0)` on every
345
+ // run, so the tick could never advance (every scripted animation looked
346
+ // dead); `shader-canvas.tsx`'s compile effect reading `precision`/`fragment`
347
+ // re-ran the same way and recompiled the GL program 65-70 times in 4
348
+ // seconds while restamping its animation clock (`iTime` pinned under 0.33s,
349
+ // periodically negative). Both were patched locally with memos in those
350
+ // files (kept -- defence in depth, cheap, and they document the hazard),
351
+ // but the leak was still here for the next reader: `variant-wave.tsx`,
352
+ // `variant-aurora.tsx`, and `variant-custom.tsx` each have their OWN
353
+ // state/frozen-driven tween effect with no local memo at all, so they were
354
+ // live instances of the identical bug (`.to()` restarts a tween's clock on
355
+ // every call -- see `create-tween.ts` -- so a 31Hz re-run means a tween
356
+ // never visibly progresses). Fixing it here, at the one place the bundling
357
+ // happens, closes all of those at once rather than requiring every current
358
+ // and future reader to remember to memoize defensively.
359
+ //
360
+ // `bands` GENUINELY must stay reactive -- this split is about not dragging
361
+ // it into unrelated reads, not about freezing it. It is passed explicitly,
362
+ // `bands={bands()}`, at every call site below, exactly like `volume`,
363
+ // `complexity`, etc. already are: an explicit prop gets its OWN getter,
364
+ // entirely independent of this one.
365
+ const shared = (): Omit<VariantProps, 'bands'> => ({
366
+ state: state(),
367
+ size: size(),
368
+ frozen: reduced(),
369
+ color: props.color,
370
+ });
371
+
372
+ // The three DOM variants (bar/grid/radial), including the bar fallback
373
+ // shown while a shader chunk loads or fails, additionally get the
374
+ // caller's render-prop. `children` is bundled here (unlike `bands`) because
375
+ // it does not churn -- it is a callback reference a caller sets once, not a
376
+ // signal driven by audio, so reading it cannot drag in anything that does.
377
+ const domShared = (): Omit<VariantProps, 'bands'> => ({ ...shared(), children: props.children });
378
+
379
+ const a11y = () =>
380
+ props.label
381
+ ? { role: 'img' as const, 'aria-label': props.label }
382
+ : { 'aria-hidden': 'true' as const };
383
+
384
+ return (
385
+ <div class={cn('inline-flex', props.class)} {...a11y()}>
386
+ <Switch
387
+ fallback={<BarVisualizer {...domShared()} bands={bands()} barCount={props.barCount} />}
388
+ >
389
+ <Match when={variant() === 'grid'}>
390
+ <GridVisualizer
391
+ {...domShared()}
392
+ bands={bands()}
393
+ count={props.count}
394
+ spread={props.spread}
395
+ interval={props.interval}
396
+ />
397
+ </Match>
398
+ <Match when={variant() === 'radial'}>
399
+ <RadialVisualizer
400
+ {...domShared()}
401
+ bands={bands()}
402
+ barCount={props.barCount}
403
+ radius={props.radius}
404
+ />
405
+ </Match>
406
+ <Match when={isShader()}>
407
+ {/*
408
+ Bars stand in until the chunk resolves, and permanently if it
409
+ cannot -- either because the import itself failed (`Shader()`
410
+ never becomes truthy) or because the mounted shader called
411
+ `onUnavailable` (checked here via `!unavailable()`, since
412
+ `Shader()` alone cannot tell a working component from one that
413
+ loaded fine but can't actually render, e.g. no WebGL). Both cases
414
+ fall back to the same `BarVisualizer`, and `when` still narrows to
415
+ the component reference on the success path, matching the
416
+ `(Comp) => ...` extraction below.
417
+ */}
418
+ <Show
419
+ when={!unavailable() && Shader()}
420
+ fallback={<BarVisualizer {...domShared()} bands={bands()} barCount={props.barCount} />}
421
+ >
422
+ {(Comp) => {
423
+ const C = Comp();
424
+ return (
425
+ <C
426
+ {...shared()}
427
+ bands={bands()}
428
+ volume={volume()}
429
+ complexity={props.complexity}
430
+ shader={props.shader}
431
+ dark={resolvedDark()}
432
+ animateWhenNotVisible={props.animateWhenNotVisible}
433
+ onUnavailable={() => setUnavailable(true)}
434
+ />
435
+ );
436
+ }}
437
+ </Show>
438
+ </Match>
439
+ </Switch>
440
+ </div>
441
+ );
442
+ }