@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,350 @@
1
+ /**
2
+ * Pure `state -> highlight set` mappers for the audio visualizer variants.
3
+ *
4
+ * Ported from livekit/components-js `packages/shadcn/hooks/agents-ui/`
5
+ * (Apache License 2.0). Upstream runs four separate animator hooks, each with
6
+ * its own requestAnimationFrame loop. We keep the sequences (verbatim) but move
7
+ * the timing into one shared driver, `use-sequencer.ts`.
8
+ *
9
+ * Every generator returns a NON-EMPTY array so callers can index with
10
+ * `sequence[tick % sequence.length]` without a guard.
11
+ */
12
+
13
+ export type VisualizerState =
14
+ | 'idle'
15
+ | 'connecting'
16
+ | 'listening'
17
+ | 'thinking'
18
+ | 'speaking'
19
+ | 'disconnected';
20
+
21
+ export interface Coordinate {
22
+ x: number;
23
+ y: number;
24
+ }
25
+
26
+ const KNOWN: readonly VisualizerState[] = [
27
+ 'idle',
28
+ 'connecting',
29
+ 'listening',
30
+ 'thinking',
31
+ 'speaking',
32
+ // First-class since 2026-08-10 (Rob): the dead-connection look (flat wave,
33
+ // nothing lit) upstream renders for its 'disconnected' state. It was an
34
+ // alias for 'idle' until idle adopted upstream's gentle wave, which left
35
+ // the flat line unreachable.
36
+ 'disconnected',
37
+ ];
38
+
39
+ /**
40
+ * LiveKit's AgentState carries room-lifecycle values that mean nothing here.
41
+ * Accept them so markup ported from LiveKit works unchanged.
42
+ */
43
+ const ALIASES: Record<string, VisualizerState> = {
44
+ // Both are dead-connection looks upstream, so 'failed' rides with
45
+ // 'disconnected' rather than 'idle'.
46
+ failed: 'disconnected',
47
+ initializing: 'connecting',
48
+ 'pre-connect-buffering': 'connecting',
49
+ };
50
+
51
+ export function normalizeState(input: string | undefined): VisualizerState {
52
+ if (!input) return 'idle';
53
+ if ((KNOWN as readonly string[]).includes(input)) return input as VisualizerState;
54
+ return ALIASES[input] ?? 'idle';
55
+ }
56
+
57
+ // ---------------------------------------------------------------- bar
58
+
59
+ export function barSequence(state: VisualizerState, barCount: number): number[][] {
60
+ switch (state) {
61
+ case 'connecting': {
62
+ // A mirrored pair sweeping outward: [0, n-1], [1, n-2], ...
63
+ const seq: number[][] = [];
64
+ for (let x = 0; x < barCount; x++) seq.push([x, barCount - 1 - x]);
65
+ return seq.length ? seq : [[]];
66
+ }
67
+ case 'listening':
68
+ case 'thinking': {
69
+ // Blink the center bar. -1 is a deliberate no-index frame (the "off" beat).
70
+ return [[Math.floor(barCount / 2)], [-1]];
71
+ }
72
+ case 'speaking':
73
+ return [Array.from({ length: barCount }, (_, i) => i)];
74
+ // Dead-connection look: mirrors idle (nothing lit) for now. A dedicated
75
+ // arm so the pending LiveKit disconnected-state measurement can adjust
76
+ // it in one line.
77
+ case 'disconnected':
78
+ return [[]];
79
+ case 'idle':
80
+ default:
81
+ return [[]];
82
+ }
83
+ }
84
+
85
+ export function barInterval(state: VisualizerState, barCount: number): number {
86
+ switch (state) {
87
+ // The whole sweep should take 2s regardless of how many bars it crosses.
88
+ case 'connecting':
89
+ return 2000 / Math.max(1, barCount);
90
+ case 'listening':
91
+ return 500;
92
+ case 'thinking':
93
+ return 150;
94
+ default:
95
+ return 1000;
96
+ }
97
+ }
98
+
99
+ // ---------------------------------------------------------------- grid
100
+
101
+ /**
102
+ * Walk the perimeter of a ring `spread` cells out from center, clockwise.
103
+ *
104
+ * NOTE: This diverges from upstream use-agent-audio-visualizer-grid.ts in two ways:
105
+ * (1) We use separate centerX and centerY (upstream uses row-center for both axes).
106
+ * Upstream never ships non-square grids, so the bug never manifested. Our public
107
+ * API is square-only too now (the grid's single `count` prop, 2026-08-09), but
108
+ * this function keeps taking rows/columns as plain params, so the correct
109
+ * arbitrary-shape handling stays -- it costs nothing and guards any future caller.
110
+ * (2) We distinguish spread === 0 from spread === undefined (upstream treats 0 as falsy).
111
+ * Again, upstream never ships spread=0 explicitly; we do for API completeness.
112
+ */
113
+ function gridRing(rows: number, columns: number, spread: number): Coordinate[] {
114
+ const seq: Coordinate[] = [];
115
+ const centerX = Math.floor(columns / 2);
116
+ const centerY = Math.floor(rows / 2);
117
+ const topLeft = { x: Math.max(0, centerX - spread), y: Math.max(0, centerY - spread) };
118
+ const bottomRight = {
119
+ x: Math.min(columns - 1, centerX + spread),
120
+ y: Math.min(rows - 1, centerY + spread),
121
+ };
122
+
123
+ for (let x = topLeft.x; x <= bottomRight.x; x++) seq.push({ x, y: topLeft.y });
124
+ for (let y = topLeft.y + 1; y <= bottomRight.y; y++) seq.push({ x: bottomRight.x, y });
125
+ for (let x = bottomRight.x - 1; x >= topLeft.x; x--) seq.push({ x, y: bottomRight.y });
126
+ for (let y = bottomRight.y - 1; y > topLeft.y; y--) seq.push({ x: topLeft.x, y });
127
+
128
+ return seq;
129
+ }
130
+
131
+ export function gridSequence(
132
+ state: VisualizerState,
133
+ rows: number,
134
+ columns: number,
135
+ spread?: number,
136
+ ): Coordinate[] {
137
+ const center = { x: Math.floor(columns / 2), y: Math.floor(rows / 2) };
138
+
139
+ switch (state) {
140
+ case 'connecting': {
141
+ const maxSpread = Math.floor(Math.max(rows, columns) / 2);
142
+ const clamped = spread !== undefined ? Math.min(spread, maxSpread) : maxSpread;
143
+ const ring = gridRing(rows, columns, clamped);
144
+ return ring.length ? ring : [center];
145
+ }
146
+ case 'listening': {
147
+ // One lit frame followed by eight dark ones: a slow heartbeat.
148
+ const off = { x: -1, y: -1 };
149
+ return [center, off, off, off, off, off, off, off, off];
150
+ }
151
+ case 'thinking': {
152
+ // Sweep the middle row left to right, then back.
153
+ const y = Math.floor(rows / 2);
154
+ const seq: Coordinate[] = [];
155
+ for (let x = 0; x < columns; x++) seq.push({ x, y });
156
+ for (let x = columns - 1; x >= 0; x--) seq.push({ x, y });
157
+ return seq.length ? seq : [center];
158
+ }
159
+ // Dead-connection look: the same centre resting frame as idle
160
+ // (variant-grid suppresses the highlight for both, so nothing lights).
161
+ // A dedicated arm so the pending LiveKit measurement can adjust it in
162
+ // one line.
163
+ case 'disconnected':
164
+ return [center];
165
+ default:
166
+ return [center];
167
+ }
168
+ }
169
+
170
+ // ---------------------------------------------------------------- radial
171
+
172
+ /** Largest divisor of `n` that is at most `max`. Always at least 1. */
173
+ function largestDivisorAtMost(n: number, max: number): number {
174
+ const gcd = (a: number, b: number): number => {
175
+ while (b !== 0) {
176
+ const t = b;
177
+ b = a % b;
178
+ a = t;
179
+ }
180
+ return a;
181
+ };
182
+ for (let i = max; i >= 1; i--) if (gcd(n, i) === i) return i;
183
+ return 1;
184
+ }
185
+
186
+ /**
187
+ * Partition the ring into interleaved groups. Lighting one group per tick reads
188
+ * as a rotating pattern rather than a single travelling dot. Every index appears
189
+ * in exactly one group.
190
+ */
191
+ function radialGroups(barCount: number): number[][] {
192
+ const divisor =
193
+ barCount > 8
194
+ ? barCount / largestDivisorAtMost(barCount, 4)
195
+ : largestDivisorAtMost(barCount, 2);
196
+ const safe = Math.max(1, Math.floor(divisor));
197
+ const perGroup = Math.floor(barCount / safe);
198
+ return Array.from({ length: safe }, (_, i) =>
199
+ Array.from({ length: perGroup }, (_, j) => j * safe + i),
200
+ );
201
+ }
202
+
203
+ export function radialSequence(state: VisualizerState, barCount: number): number[][] {
204
+ switch (state) {
205
+ case 'connecting': {
206
+ // Pair each bar with the one opposite it, sweeping around the circle.
207
+ const center = Math.floor(barCount / 2);
208
+ const seq: number[][] = [];
209
+ for (let x = 0; x < barCount; x++) seq.push([x, (x + center) % barCount]);
210
+ return seq.length ? seq : [[]];
211
+ }
212
+ case 'listening':
213
+ case 'thinking': {
214
+ const groups = radialGroups(barCount);
215
+ return groups.length ? groups : [[]];
216
+ }
217
+ case 'speaking':
218
+ return [Array.from({ length: barCount }, (_, i) => i)];
219
+ // Dead-connection look: mirrors idle (nothing lit) for now, one-line
220
+ // adjustable once the LiveKit measurement lands.
221
+ case 'disconnected':
222
+ return [[]];
223
+ case 'idle':
224
+ default:
225
+ return [[]];
226
+ }
227
+ }
228
+
229
+ export function radialInterval(state: VisualizerState): number {
230
+ switch (state) {
231
+ case 'connecting':
232
+ case 'listening':
233
+ return 500;
234
+ // Infinity parks the sequencer: `thinking` spins the whole container in CSS
235
+ // with every bar lit, so ticking would fight the animation.
236
+ case 'thinking':
237
+ return Infinity;
238
+ default:
239
+ return 1000;
240
+ }
241
+ }
242
+
243
+ // ---------------------------------------------------------------- shader
244
+
245
+ /**
246
+ * Uniform targets for the aura and custom shaders. An array intensity means
247
+ * "ping-pong between these two" (see create-tween). `speaking` intensity is
248
+ * omitted from the pulse because live volume drives it instantly instead.
249
+ */
250
+ export function shaderTargets(
251
+ state: VisualizerState,
252
+ ): { intensity: number | [number, number]; speed: number } {
253
+ switch (state) {
254
+ case 'listening':
255
+ return { intensity: [0.5, 0.8], speed: 2.5 };
256
+ case 'thinking':
257
+ return { intensity: [0.25, 0.5], speed: 4.0 };
258
+ // Same pulse as thinking, its own speed: uSpeed is the only
259
+ // state-correlated scalar besides intensity that reaches a custom
260
+ // shader's GLSL (see customUniforms in variant-custom.tsx), so the two
261
+ // transitional states sharing 4.0 made them indistinguishable to any
262
+ // consumer shader keying off the kit's uniforms -- the Custom story's
263
+ // per-state looks are the concrete consumer. Every state now maps to a
264
+ // distinct speed: idle 1, listening/speaking 2.5 (told apart by band
265
+ // energy), thinking 4, connecting 6.
266
+ case 'connecting':
267
+ return { intensity: [0.25, 0.5], speed: 6.0 };
268
+ case 'speaking':
269
+ return { intensity: 0.3, speed: 2.5 };
270
+ // Dead-connection look: idle's dim intensity but its OWN speed, so a
271
+ // shader keying off uSpeed can draw a dead line distinct from idle's
272
+ // calm ridge (see the connecting arm's comment: uSpeed is the only
273
+ // other state-correlated scalar reaching consumer GLSL). Still
274
+ // one-line adjustable once the LiveKit measurement lands.
275
+ case 'disconnected':
276
+ return { intensity: 0.3, speed: 0.5 };
277
+ case 'idle':
278
+ default:
279
+ return { intensity: 0.3, speed: 1 };
280
+ }
281
+ }
282
+
283
+ const WAVE_SPEED = 5;
284
+ const WAVE_AMPLITUDE = 0.025;
285
+ const WAVE_FREQUENCY = 10;
286
+
287
+ /** Uniform targets for the wave shader. Same shape, different curve. */
288
+ export function waveTargets(state: VisualizerState): {
289
+ speed: number;
290
+ amplitude: number;
291
+ frequency: number;
292
+ opacity: number | [number, number];
293
+ pulseDuration: number;
294
+ } {
295
+ switch (state) {
296
+ case 'listening':
297
+ return {
298
+ speed: WAVE_SPEED,
299
+ amplitude: WAVE_AMPLITUDE,
300
+ frequency: WAVE_FREQUENCY,
301
+ opacity: [1.0, 0.3],
302
+ pulseDuration: 0.75,
303
+ };
304
+ case 'thinking':
305
+ case 'connecting':
306
+ return {
307
+ speed: WAVE_SPEED * 4,
308
+ amplitude: WAVE_AMPLITUDE / 4,
309
+ frequency: WAVE_FREQUENCY * 4,
310
+ opacity: [1.0, 0.3],
311
+ pulseDuration: 0.4,
312
+ };
313
+ case 'speaking':
314
+ // Upstream's wave switch has NO 'idle' arm: idle falls through to this
315
+ // same speaking/default arm (speed 10, amplitude 0.025, frequency 10,
316
+ // opacity 1), a gentle undulation (~1.05px centreline waviness, ~1.5px/s
317
+ // phase drift, measured on the md tile). Upstream's explicitly FLAT
318
+ // state is 'disconnected' -- the dedicated arm below. We had flattened
319
+ // idle instead; Rob wants upstream's look (2026-08-09).
320
+ case 'idle':
321
+ return {
322
+ speed: WAVE_SPEED * 2,
323
+ amplitude: WAVE_AMPLITUDE,
324
+ frequency: WAVE_FREQUENCY,
325
+ opacity: 1.0,
326
+ pulseDuration: 0,
327
+ };
328
+ // Upstream's EXPLICIT flat state, measured: 'disconnected' is the one
329
+ // arm of their wave switch that zeroes the line. First-class here since
330
+ // 2026-08-10 (Rob: 'the disconnected wave needs to be that kind of flat
331
+ // line'). `default` keeps the same flat targets for out-of-union
332
+ // strings, which normalizeState prevents real callers from reaching.
333
+ case 'disconnected':
334
+ return {
335
+ speed: WAVE_SPEED,
336
+ amplitude: 0,
337
+ frequency: 0,
338
+ opacity: 1.0,
339
+ pulseDuration: 0,
340
+ };
341
+ default:
342
+ return {
343
+ speed: WAVE_SPEED,
344
+ amplitude: 0,
345
+ frequency: 0,
346
+ opacity: 1.0,
347
+ pulseDuration: 0,
348
+ };
349
+ }
350
+ }
@@ -0,0 +1,88 @@
1
+ import { vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ /**
4
+ * Shared fake `requestAnimationFrame` + `performance.now()` clock for tests
5
+ * that need to drive time deterministically instead of racing real timers.
6
+ *
7
+ * requestAnimationFrame's timestamp and performance.now() share a time origin
8
+ * per spec, so production code can read either. Stubbing them off the same
9
+ * fake `now` keeps the two from silently disagreeing.
10
+ *
11
+ * The stub holds a REAL per-registration queue, matching a real browser: a
12
+ * single frame fires every callback that was pending BEFORE that frame, all
13
+ * with the SAME advanced `now`, in registration order -- so two
14
+ * independently-animating things under test (e.g. a `createTween`'s own step
15
+ * loop and `ShaderCanvas`'s separate `draw` loop, both requestAnimationFrame
16
+ * consumers, both mounted at once) tick together on one `advance()` call
17
+ * instead of the second silently stealing the first's slot.
18
+ *
19
+ * This used to be a single PENDING-callback slot, not a queue: a second
20
+ * consumer scheduling a new frame before `advance()` ran overwrote the
21
+ * first rather than queueing behind it. That was fine for anything that only
22
+ * ever has ONE thing pending at a time (the common case, e.g. bar/grid/radial's
23
+ * single sequencer tick, or a lone `createTween`), which is why switching to a
24
+ * queue does not change any of those. It was NOT fine for a mounted shader
25
+ * variant, where a tween and ShaderCanvas's `draw` are both independently
26
+ * pending at once: `draw` always registers last and re-arms itself every
27
+ * `advance()`, so it permanently "won" the single slot and the tween's step
28
+ * never fired -- silently defeating any test trying to prove something holds
29
+ * while UNIFORM VALUES genuinely change every frame (see
30
+ * `variant-custom.test.tsx`'s recompile-guard test for the concrete case this
31
+ * was fixed for).
32
+ *
33
+ * `cancelAnimationFrame` cancels by id now (each `requestAnimationFrame` call
34
+ * gets its own), not by clearing whatever happens to be in the single slot --
35
+ * required once more than one callback can be pending at a time, so a
36
+ * `cancelAnimationFrame(idA)` cannot accidentally drop a DIFFERENT pending
37
+ * callback `idB`.
38
+ *
39
+ * Call once per `describe` block (or at file scope for the whole file); it
40
+ * registers `beforeEach`/`afterEach` hooks scoped to wherever it is called,
41
+ * exactly like inlining the hooks would, and returns the driver bound to
42
+ * that scope's fake clock.
43
+ */
44
+ export function installFakeClock() {
45
+ let now = 0;
46
+ let nextId = 1;
47
+ let pending = new Map<number, (t: number) => void>();
48
+
49
+ beforeEach(() => {
50
+ now = 0;
51
+ nextId = 1;
52
+ pending = new Map();
53
+ vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
54
+ const id = nextId++;
55
+ pending.set(id, cb as (t: number) => void);
56
+ return id;
57
+ });
58
+ vi.stubGlobal('cancelAnimationFrame', (id: number) => {
59
+ pending.delete(id);
60
+ });
61
+ vi.stubGlobal('performance', { now: () => now });
62
+ });
63
+
64
+ afterEach(() => vi.unstubAllGlobals());
65
+
66
+ /**
67
+ * Advance the fake clock and run every callback pending BEFORE this call,
68
+ * all at the new `now` -- one simulated frame. A callback that reschedules
69
+ * itself (the normal RAF-loop pattern) lands in a FRESH pending set for the
70
+ * NEXT `advance()`, not this one: snapshotting the callback list before
71
+ * invoking any of them is what keeps a self-rescheduling loop from looping
72
+ * forever inside a single `advance()` call, matching how a real browser
73
+ * frame never re-enters itself either.
74
+ */
75
+ function advance(ms: number) {
76
+ now += ms;
77
+ const callbacks = Array.from(pending.values());
78
+ pending = new Map();
79
+ for (const cb of callbacks) cb(now);
80
+ }
81
+
82
+ /** Whether at least one requestAnimationFrame callback is currently pending. */
83
+ function isFramePending(): boolean {
84
+ return pending.size > 0;
85
+ }
86
+
87
+ return { advance, isFramePending };
88
+ }
@@ -1 +0,0 @@
1
- import{I as o}from"./Icon-11SRdjJ4.js";import{c as m,m as r}from"./define-DfTe4zwZ.js";var e=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],t=a=>m(o,r(a,{name:"ArrowLeft",iconNode:e})),p=t;export{p as a};
@@ -1 +0,0 @@
1
- import{I as m}from"./Icon-11SRdjJ4.js";import{c as o,m as r}from"./define-DfTe4zwZ.js";var c=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],e=a=>o(m,r(a,{name:"Check",iconNode:c})),f=e;export{f as c};
@@ -1 +0,0 @@
1
- import{I as r}from"./Icon-11SRdjJ4.js";import{c as a,m}from"./define-DfTe4zwZ.js";var e=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],n=o=>a(r,m(o,{name:"ChevronDown",iconNode:e})),p=n;export{p as c};
@@ -1 +0,0 @@
1
- import{I as o}from"./Icon-11SRdjJ4.js";import{c as a,m as r}from"./define-DfTe4zwZ.js";var t=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],e=m=>a(o,r(m,{name:"ChevronRight",iconNode:t})),h=e;export{h as c};
@@ -1 +0,0 @@
1
- import{I as c}from"./Icon-11SRdjJ4.js";import{c as m,m as o}from"./define-DfTe4zwZ.js";var a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],e=r=>m(c,o(r,{name:"Circle",iconNode:a})),i=e;export{i as c};
@@ -1 +0,0 @@
1
- import{I as c}from"./Icon-11SRdjJ4.js";import{c as r,m as a}from"./define-DfTe4zwZ.js";var e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],o=m=>r(c,a(m,{name:"CircleCheck",iconNode:e})),i=o;export{i as c};
@@ -1 +0,0 @@
1
- import{s as c,g as k,r as y,c as I,l as p,m as u,i as v,n as h,a as R,N as E,b as N,t as g,y as P,z as q}from"./define-DfTe4zwZ.js";import{c as C}from"./cn-CU7UAWzk.js";var z=g("<div>"),B=g("<button>"),S=g("<div><div>");const m=P(),w=()=>{const o=q(m);if(!o)throw new Error("Collapsible parts must be used within <Collapsible>");return o};function T(o){var n;const[e,l]=c(o,["open","defaultOpen","onOpenChange","disabled","controllerRef","children","class"]),[t,a]=k(e.defaultOpen??!1),s=()=>e.open!==void 0,r=()=>s()?!!e.open:t(),i=()=>!!e.disabled,f=d=>{var b;r()!==d&&(s()||a(d),(b=e.onOpenChange)==null||b.call(e,d))},x=()=>{i()||f(!r())};(n=e.controllerRef)==null||n.call(e,{open:r,setOpen:f});const O=y();return I(m.Provider,{value:{open:r,toggle:x,contentId:O,disabled:i},get children(){var d=z();return p(d,u({get class(){return e.class}},l,{get"data-expanded"(){return r()?"":void 0},get"data-closed"(){return r()?void 0:""},get"data-state"(){return r()?"open":"closed"}}),!1,!0),v(d,()=>e.children),d}})}function U(o){const n=w(),[e,l]=c(o,["children","class","as","onClick"]),t=()=>({type:"button","aria-expanded":n.open(),"aria-controls":n.contentId,disabled:n.disabled()||void 0,"data-expanded":n.open()?"":void 0,"data-closed":n.open()?void 0:"","data-state":n.open()?"open":"closed",onClick:a=>{n.disabled()||(typeof e.onClick=="function"&&e.onClick(a),n.toggle())},class:e.class,...l});return h(()=>h(()=>!!e.as)()?e.as(t()):(()=>{var a=B();return p(a,u(t),!1,!0),v(a,()=>e.children),a})())}function _(o){const n=w(),[e,l]=c(o,["children","class"]);return(()=>{var t=S(),a=t.firstChild;return p(t,u(l,{get id(){return n.contentId},get"data-expanded"(){return n.open()?"":void 0},get"data-closed"(){return n.open()?void 0:""},get class(){return C("grid transition-[grid-template-rows] duration-200 ease-out",n.open()?"grid-rows-[1fr]":"grid-rows-[0fr]")}}),!1,!0),v(a,()=>e.children),R(s=>{var r=!n.open(),i=C("overflow-hidden",e.class);return r!==s.e&&E(t,"inert",s.e=r),i!==s.t&&N(a,s.t=i),s},{e:void 0,t:void 0}),t})()}export{T as C,U as a,_ as b};