@kitn.ai/ui 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-tooling/construct/public.d.ts +15 -0
  3. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  4. package/dist/construct-cli.es.js +3 -3
  5. package/dist/construct.d.ts +5 -0
  6. package/dist/construct.js +327 -0
  7. package/dist/core-C8fzo39E.js +1 -12
  8. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  9. package/dist/mcp.es.js +1 -1
  10. package/package.json +16 -16
  11. package/bin/route.test.js +0 -58
  12. package/src/agent-tooling/README.md +0 -192
  13. package/src/agent-tooling/archetypes.ts +0 -205
  14. package/src/agent-tooling/catalog/README.md +0 -676
  15. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  16. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  17. package/src/agent-tooling/catalog/invariants.ts +0 -284
  18. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  19. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  20. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  21. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  22. package/src/agent-tooling/construct/cli.ts +0 -147
  23. package/src/agent-tooling/construct/codegen.ts +0 -1635
  24. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  25. package/src/agent-tooling/construct/dev.ts +0 -148
  26. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  27. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  28. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  29. package/src/agent-tooling/construct/schema.ts +0 -452
  30. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  31. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  32. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  33. package/src/agent-tooling/integrations/mastra.ts +0 -182
  34. package/src/agent-tooling/integrations/mock.ts +0 -148
  35. package/src/agent-tooling/integrations/ollama.ts +0 -97
  36. package/src/agent-tooling/integrations/openai.ts +0 -95
  37. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  38. package/src/agent-tooling/integrations/pi.ts +0 -176
  39. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  40. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  41. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  42. package/src/agent-tooling/mcp/manifest.ts +0 -422
  43. package/src/agent-tooling/mcp/server.ts +0 -116
  44. package/src/agent-tooling/mcp/stdio.ts +0 -18
  45. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  46. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  47. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  48. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  49. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  50. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  51. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  52. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  53. package/src/agent-tooling/recipes/index.ts +0 -22
  54. package/src/agent-tooling/recipes/types.ts +0 -32
  55. package/src/agent-tooling/registry.ts +0 -132
  56. package/src/agent-tooling/route-emit.ts +0 -305
  57. package/src/agent-tooling/types.ts +0 -390
  58. package/src/components/artifact-card.tsx +0 -120
  59. package/src/components/artifact.tsx +0 -914
  60. package/src/components/attachment-types.ts +0 -66
  61. package/src/components/attachments.tsx +0 -536
  62. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  63. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  64. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  65. package/src/components/audio-visualizer/index.tsx +0 -528
  66. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  67. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  68. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  69. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  70. package/src/components/audio-visualizer/sizes.ts +0 -54
  71. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  72. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  73. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  74. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  75. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  76. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  77. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  78. package/src/components/card-fallback.tsx +0 -60
  79. package/src/components/card-renderer.tsx +0 -220
  80. package/src/components/card.tsx +0 -212
  81. package/src/components/chain-of-thought.tsx +0 -348
  82. package/src/components/chat-container.tsx +0 -89
  83. package/src/components/chat-scope-picker.tsx +0 -57
  84. package/src/components/chat-thread.tsx +0 -1014
  85. package/src/components/checkpoint.tsx +0 -88
  86. package/src/components/choice-card.tsx +0 -743
  87. package/src/components/coachmark.tsx +0 -249
  88. package/src/components/code-block.tsx +0 -181
  89. package/src/components/composer-dom.ts +0 -159
  90. package/src/components/composer-highlight.ts +0 -242
  91. package/src/components/composer-history.ts +0 -82
  92. package/src/components/composer.tsx +0 -1075
  93. package/src/components/confirm-card.tsx +0 -402
  94. package/src/components/context.tsx +0 -377
  95. package/src/components/conversation-item.tsx +0 -241
  96. package/src/components/conversation-list.tsx +0 -429
  97. package/src/components/conversation-panel.tsx +0 -118
  98. package/src/components/dismissed-stub.tsx +0 -80
  99. package/src/components/embed.tsx +0 -200
  100. package/src/components/empty.tsx +0 -177
  101. package/src/components/feedback-bar.tsx +0 -166
  102. package/src/components/file-tree.tsx +0 -530
  103. package/src/components/file-upload.tsx +0 -167
  104. package/src/components/form-widgets.tsx +0 -525
  105. package/src/components/form.tsx +0 -1296
  106. package/src/components/home-panel.tsx +0 -159
  107. package/src/components/image.tsx +0 -65
  108. package/src/components/link-preview.tsx +0 -195
  109. package/src/components/loader.tsx +0 -367
  110. package/src/components/markdown.tsx +0 -162
  111. package/src/components/message-skills.tsx +0 -40
  112. package/src/components/message.tsx +0 -765
  113. package/src/components/model-switcher.tsx +0 -132
  114. package/src/components/prompt-input.tsx +0 -188
  115. package/src/components/prompt-suggestion.tsx +0 -150
  116. package/src/components/reasoning.tsx +0 -293
  117. package/src/components/response-compare-types.ts +0 -152
  118. package/src/components/response-compare.tsx +0 -455
  119. package/src/components/response-stream.tsx +0 -109
  120. package/src/components/screen.tsx +0 -181
  121. package/src/components/scroll-button.tsx +0 -93
  122. package/src/components/source.tsx +0 -192
  123. package/src/components/tasks-card.tsx +0 -801
  124. package/src/components/text-shimmer.tsx +0 -37
  125. package/src/components/thinking-bar.tsx +0 -50
  126. package/src/components/thread.tsx +0 -228
  127. package/src/components/toast.tsx +0 -503
  128. package/src/components/tool-classify.ts +0 -26
  129. package/src/components/tool-types.ts +0 -49
  130. package/src/components/tool.tsx +0 -190
  131. package/src/components/use-card-resolution.ts +0 -44
  132. package/src/components/voice-input.tsx +0 -222
  133. package/src/components/voice-output.tsx +0 -242
  134. package/src/components/widget-tab-bar.tsx +0 -73
  135. package/src/components/workspace-shell.tsx +0 -285
  136. package/src/diagnostics/hook.ts +0 -352
  137. package/src/diagnostics/index.ts +0 -82
  138. package/src/diagnostics/report-request.ts +0 -296
  139. package/src/elements/agent-card.tsx +0 -67
  140. package/src/elements/artifact.tsx +0 -194
  141. package/src/elements/attachments.tsx +0 -140
  142. package/src/elements/audio-visualizer.tsx +0 -227
  143. package/src/elements/autoloader.ts +0 -89
  144. package/src/elements/avatar.tsx +0 -33
  145. package/src/elements/badge.tsx +0 -32
  146. package/src/elements/button.tsx +0 -149
  147. package/src/elements/card.tsx +0 -133
  148. package/src/elements/cards.tsx +0 -266
  149. package/src/elements/chain-of-thought.tsx +0 -141
  150. package/src/elements/chat-scope-picker.tsx +0 -65
  151. package/src/elements/chat-types.ts +0 -102
  152. package/src/elements/chat-workspace.tsx +0 -188
  153. package/src/elements/chat.tsx +0 -304
  154. package/src/elements/checkbox-group.tsx +0 -164
  155. package/src/elements/checkbox.tsx +0 -134
  156. package/src/elements/checkpoint.tsx +0 -47
  157. package/src/elements/choice.tsx +0 -100
  158. package/src/elements/coachmark.tsx +0 -90
  159. package/src/elements/code-block.tsx +0 -60
  160. package/src/elements/command.tsx +0 -197
  161. package/src/elements/compare.tsx +0 -111
  162. package/src/elements/compiled.css +0 -2
  163. package/src/elements/composer.tsx +0 -162
  164. package/src/elements/confirm-card.tsx +0 -76
  165. package/src/elements/context-meter.tsx +0 -107
  166. package/src/elements/conversation-item.tsx +0 -120
  167. package/src/elements/conversation-list.tsx +0 -203
  168. package/src/elements/css.ts +0 -5
  169. package/src/elements/default-input.tsx +0 -392
  170. package/src/elements/define-entry.ts +0 -12
  171. package/src/elements/define.tsx +0 -563
  172. package/src/elements/diagnostic-events.ts +0 -114
  173. package/src/elements/dialog.tsx +0 -145
  174. package/src/elements/disclosure.ts +0 -95
  175. package/src/elements/dock.tsx +0 -215
  176. package/src/elements/dropdown.tsx +0 -123
  177. package/src/elements/editable-label.tsx +0 -140
  178. package/src/elements/element-data-types.ts +0 -152
  179. package/src/elements/element-diagnostics.ts +0 -392
  180. package/src/elements/element-manifest.json +0 -354
  181. package/src/elements/element-nonscalar.json +0 -187
  182. package/src/elements/element-types.d.ts +0 -4353
  183. package/src/elements/embed.tsx +0 -35
  184. package/src/elements/empty.tsx +0 -29
  185. package/src/elements/feedback-bar.tsx +0 -66
  186. package/src/elements/file-tree.tsx +0 -62
  187. package/src/elements/file-upload.tsx +0 -44
  188. package/src/elements/form.tsx +0 -94
  189. package/src/elements/hover-card.tsx +0 -80
  190. package/src/elements/icon.tsx +0 -41
  191. package/src/elements/image.tsx +0 -32
  192. package/src/elements/input.tsx +0 -492
  193. package/src/elements/kbd.tsx +0 -45
  194. package/src/elements/link-preview.tsx +0 -34
  195. package/src/elements/loader.tsx +0 -25
  196. package/src/elements/markdown.tsx +0 -38
  197. package/src/elements/menu.tsx +0 -241
  198. package/src/elements/message-skills.tsx +0 -83
  199. package/src/elements/message.tsx +0 -361
  200. package/src/elements/model-switcher.tsx +0 -121
  201. package/src/elements/nav.tsx +0 -87
  202. package/src/elements/notice.tsx +0 -53
  203. package/src/elements/pane-grid.tsx +0 -111
  204. package/src/elements/pane-group.tsx +0 -119
  205. package/src/elements/pane.tsx +0 -129
  206. package/src/elements/popover.tsx +0 -81
  207. package/src/elements/progress-bar.tsx +0 -39
  208. package/src/elements/prompt-dock.tsx +0 -82
  209. package/src/elements/prompt-input.tsx +0 -240
  210. package/src/elements/prompt-suggestions.tsx +0 -113
  211. package/src/elements/radio-group.tsx +0 -129
  212. package/src/elements/reasoning.tsx +0 -71
  213. package/src/elements/register-impl.ts +0 -148
  214. package/src/elements/register.ts +0 -76
  215. package/src/elements/remote.tsx +0 -225
  216. package/src/elements/resizable.globals.d.ts +0 -36
  217. package/src/elements/resizable.tsx +0 -704
  218. package/src/elements/response-stream.tsx +0 -40
  219. package/src/elements/screen.tsx +0 -90
  220. package/src/elements/scroll-area.tsx +0 -33
  221. package/src/elements/scroll-button.tsx +0 -179
  222. package/src/elements/search.tsx +0 -185
  223. package/src/elements/segmented.tsx +0 -129
  224. package/src/elements/select.tsx +0 -165
  225. package/src/elements/separator.tsx +0 -36
  226. package/src/elements/setting-item.tsx +0 -50
  227. package/src/elements/settings-group.tsx +0 -37
  228. package/src/elements/skeleton.tsx +0 -45
  229. package/src/elements/slider.tsx +0 -171
  230. package/src/elements/slot-text.ts +0 -73
  231. package/src/elements/slots.ts +0 -787
  232. package/src/elements/source.tsx +0 -126
  233. package/src/elements/status.tsx +0 -42
  234. package/src/elements/styles.css +0 -328
  235. package/src/elements/switch.tsx +0 -118
  236. package/src/elements/tabs.tsx +0 -95
  237. package/src/elements/tasks.tsx +0 -103
  238. package/src/elements/text-shimmer.tsx +0 -28
  239. package/src/elements/thinking-bar.tsx +0 -43
  240. package/src/elements/thread.tsx +0 -139
  241. package/src/elements/toast.tsx +0 -106
  242. package/src/elements/tool.tsx +0 -78
  243. package/src/elements/tooltip.tsx +0 -85
  244. package/src/elements/validate-messages.ts +0 -76
  245. package/src/elements/voice-input.tsx +0 -100
  246. package/src/elements/voice-output.tsx +0 -82
  247. package/src/index.ts +0 -430
  248. package/src/primitives/audio-bands.ts +0 -174
  249. package/src/primitives/card-component-types.ts +0 -63
  250. package/src/primitives/card-contract.ts +0 -80
  251. package/src/primitives/card-data-types.ts +0 -361
  252. package/src/primitives/card-host.tsx +0 -37
  253. package/src/primitives/card-recovery.ts +0 -160
  254. package/src/primitives/card-registry.tsx +0 -78
  255. package/src/primitives/card-resolution.ts +0 -36
  256. package/src/primitives/card-routing.ts +0 -85
  257. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  258. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  259. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  260. package/src/primitives/card-schemas/choice.schema.json +0 -75
  261. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  262. package/src/primitives/card-schemas/embed.schema.json +0 -63
  263. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  264. package/src/primitives/card-schemas/form.schema.json +0 -53
  265. package/src/primitives/card-schemas/link.schema.json +0 -54
  266. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  267. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  268. package/src/primitives/card-tags.ts +0 -61
  269. package/src/primitives/card-validate-cards.ts +0 -255
  270. package/src/primitives/card-validate-schemas.ts +0 -58
  271. package/src/primitives/card-validate.ts +0 -131
  272. package/src/primitives/chat-config.tsx +0 -103
  273. package/src/primitives/composer-model.ts +0 -49
  274. package/src/primitives/composer-triggers.ts +0 -35
  275. package/src/primitives/controllable.ts +0 -21
  276. package/src/primitives/conversation-store.ts +0 -248
  277. package/src/primitives/create-kai-chat.ts +0 -66
  278. package/src/primitives/create-tween.ts +0 -266
  279. package/src/primitives/embed-providers.ts +0 -254
  280. package/src/primitives/field-mask.ts +0 -256
  281. package/src/primitives/field-semantics.ts +0 -115
  282. package/src/primitives/highlighter.ts +0 -157
  283. package/src/primitives/input-mask.ts +0 -853
  284. package/src/primitives/link-preview.ts +0 -87
  285. package/src/primitives/message-feedback.ts +0 -124
  286. package/src/primitives/pdf-preview.ts +0 -121
  287. package/src/primitives/toast-store.ts +0 -338
  288. package/src/primitives/url-scheme-policy.ts +0 -70
  289. package/src/primitives/use-audio-analysis.ts +0 -340
  290. package/src/primitives/use-auto-resize.ts +0 -31
  291. package/src/primitives/use-resize-observer.ts +0 -45
  292. package/src/primitives/use-sequencer.ts +0 -72
  293. package/src/primitives/use-speech-recognition.ts +0 -146
  294. package/src/primitives/use-stick-to-bottom.ts +0 -75
  295. package/src/primitives/use-text-stream.ts +0 -143
  296. package/src/primitives/use-voice-recorder.ts +0 -62
  297. package/src/primitives/visualizer-sequences.ts +0 -350
  298. package/src/remote/host-embed.ts +0 -345
  299. package/src/remote/index.ts +0 -2
  300. package/src/remote/origin.ts +0 -30
  301. package/src/remote/provider-runtime.ts +0 -262
  302. package/src/remote/provider.ts +0 -2
  303. package/src/remote/validate.ts +0 -22
  304. package/src/remote/version.ts +0 -12
  305. package/src/remote/wire.ts +0 -48
  306. package/src/schemas/from-tool-call.ts +0 -151
  307. package/src/schemas/index.ts +0 -341
  308. package/src/schemas/provider-subsets.ts +0 -538
  309. package/src/schemas/registry.ts +0 -355
  310. package/src/schemas/tool-defs.ts +0 -793
  311. package/src/solid.ts +0 -173
  312. package/src/state/index.ts +0 -44
  313. package/src/state/messages.ts +0 -53
  314. package/src/state/mock.ts +0 -297
  315. package/src/state/parts.ts +0 -295
  316. package/src/state/persistence.ts +0 -180
  317. package/src/state/stream.ts +0 -246
  318. package/src/state/suggestions.ts +0 -9
  319. package/src/state/threads.ts +0 -142
  320. package/src/test-utils/fake-clock.ts +0 -88
  321. package/src/types.ts +0 -95
  322. package/src/ui/action-icons.ts +0 -53
  323. package/src/ui/agent-card.tsx +0 -218
  324. package/src/ui/avatar.tsx +0 -23
  325. package/src/ui/badge.tsx +0 -21
  326. package/src/ui/button.tsx +0 -49
  327. package/src/ui/card.tsx +0 -245
  328. package/src/ui/checkbox-group.tsx +0 -153
  329. package/src/ui/checkbox.tsx +0 -63
  330. package/src/ui/collapsible.tsx +0 -163
  331. package/src/ui/command.tsx +0 -103
  332. package/src/ui/dialog.tsx +0 -223
  333. package/src/ui/dock.tsx +0 -630
  334. package/src/ui/dropdown.tsx +0 -636
  335. package/src/ui/editable-label.tsx +0 -112
  336. package/src/ui/hover-card.tsx +0 -345
  337. package/src/ui/icon.tsx +0 -167
  338. package/src/ui/input.tsx +0 -391
  339. package/src/ui/kbd.tsx +0 -118
  340. package/src/ui/nav.tsx +0 -282
  341. package/src/ui/notice.tsx +0 -84
  342. package/src/ui/overlay.tsx +0 -277
  343. package/src/ui/pane-grid.tsx +0 -117
  344. package/src/ui/pane-group.tsx +0 -291
  345. package/src/ui/pane.tsx +0 -193
  346. package/src/ui/popover.tsx +0 -106
  347. package/src/ui/progress-bar.tsx +0 -83
  348. package/src/ui/prompt-dock.tsx +0 -145
  349. package/src/ui/radio.tsx +0 -150
  350. package/src/ui/resizable.tsx +0 -693
  351. package/src/ui/scroll-area.tsx +0 -26
  352. package/src/ui/segmented.tsx +0 -109
  353. package/src/ui/select.tsx +0 -168
  354. package/src/ui/separator.tsx +0 -10
  355. package/src/ui/settings-group.tsx +0 -67
  356. package/src/ui/skeleton.tsx +0 -73
  357. package/src/ui/slider.tsx +0 -178
  358. package/src/ui/status.tsx +0 -48
  359. package/src/ui/switch.tsx +0 -119
  360. package/src/ui/tabs.tsx +0 -168
  361. package/src/ui/textarea.tsx +0 -21
  362. package/src/ui/tooltip.tsx +0 -118
  363. package/src/utils/cn.ts +0 -30
  364. package/src/wire/chunk.ts +0 -248
  365. package/src/wire/consume.ts +0 -599
  366. package/src/wire/diagnostics.ts +0 -727
  367. package/src/wire/encode-probe.ts +0 -214
  368. package/src/wire/encode.ts +0 -947
  369. package/src/wire/files.ts +0 -342
  370. package/src/wire/formats/anthropic.ts +0 -274
  371. package/src/wire/formats/openai.ts +0 -228
  372. package/src/wire/index.ts +0 -95
  373. package/src/wire/media-types.ts +0 -344
  374. package/src/wire/read.ts +0 -396
  375. package/src/wire/sink-helpers.ts +0 -50
  376. package/src/wire/sse.ts +0 -153
@@ -1,112 +0,0 @@
1
- import { createSignal, onCleanup, type Accessor } from 'solid-js';
2
-
3
- /**
4
- * Fit-to-container scaling: the size tiers are fixed
5
- * px by design (`sizes.ts`), so a container narrower than a tier's natural
6
- * footprint used to CLIP the picture at its edges. The dispatcher now scales
7
- * the WHOLE visualization down proportionally to fit, and renders byte-equal
8
- * to the designed px metrics whenever the container is at least natural size.
9
- *
10
- * Why a transform-scale wrapper rather than relative units or reflow:
11
- * - The px tiers ARE the design (LiveKit parity); switching the geometry to
12
- * relative units would change rendering at every container size, not just
13
- * the overflowing ones.
14
- * - Reflowing (fewer bars, smaller gaps) changes what the animation MEANS;
15
- * proportional scale keeps the exact picture, just smaller.
16
- * - A transform also covers the shader variants for free: their canvases size
17
- * their backing store from layout size (`clientWidth`), which a transform
18
- * does not change, so the bitmap stays at full natural resolution and only
19
- * the composited output shrinks. No variant needs an exception.
20
- *
21
- * Both sizes are MEASURED (one ResizeObserver watching both wrappers), never
22
- * restated from `sizes.ts`: the natural footprint depends on variant, size
23
- * tier, barCount/count/radius and the consumer's own part styling, and a
24
- * hand-derived width table would rot the day any of those moved.
25
- *
26
- * Where ResizeObserver does not exist (SSR, jsdom) this is inert: scale stays
27
- * 1 and nothing is written, which is exactly the pre-fix rendering.
28
- */
29
- export function computeFitScale(
30
- available: number | undefined,
31
- natural: number | undefined,
32
- ): number {
33
- if (
34
- available === undefined || natural === undefined ||
35
- !Number.isFinite(available) || !Number.isFinite(natural) ||
36
- available <= 0 || natural <= 0
37
- ) {
38
- return 1;
39
- }
40
- return Math.min(1, available / natural);
41
- }
42
-
43
- export interface FitScale {
44
- /** Ref for the outer wrapper (the host box, clamped by `max-width: 100%`). */
45
- observeOuter: (el: HTMLElement) => void;
46
- /** Ref for the inner wrapper (the natural-size content that gets scaled). */
47
- observeInner: (el: HTMLElement) => void;
48
- /** 1 when the content fits; the proportional shrink factor when it does not. */
49
- scale: Accessor<number>;
50
- /** The scaled content height in px while shrunk, else undefined: the outer
51
- * wrapper adopts it so the page never reserves the unscaled height. */
52
- scaledHeight: Accessor<number | undefined>;
53
- }
54
-
55
- /**
56
- * One ResizeObserver over both wrappers. ResizeObserver reports layout
57
- * (border-box/content-box) sizes, which CSS transforms do not affect, so the
58
- * inner wrapper keeps reporting its NATURAL size even while scaled, and the
59
- * outer wrapper (whose `max-width: 100%` is what lets a narrow parent
60
- * actually constrain it) reports the width the container is offering. The
61
- * ratio of the two is the scale, with `computeFitScale` guarding the
62
- * unmeasured/degenerate cases to 1 so a bad reading can never blank the
63
- * element.
64
- */
65
- export function createFitScale(): FitScale {
66
- const [availableWidth, setAvailableWidth] = createSignal<number>();
67
- const [naturalWidth, setNaturalWidth] = createSignal<number>();
68
- const [naturalHeight, setNaturalHeight] = createSignal<number>();
69
-
70
- let outerEl: HTMLElement | undefined;
71
- let innerEl: HTMLElement | undefined;
72
- let observer: ResizeObserver | undefined;
73
-
74
- const ensureObserver = (): ResizeObserver | undefined => {
75
- if (observer) return observer;
76
- if (typeof ResizeObserver === 'undefined') return undefined;
77
- observer = new ResizeObserver((entries) => {
78
- for (const entry of entries) {
79
- // contentRect, not borderBoxSize: identical for these padding-less
80
- // wrappers and present everywhere this runs.
81
- if (entry.target === outerEl) setAvailableWidth(entry.contentRect.width);
82
- if (entry.target === innerEl) {
83
- setNaturalWidth(entry.contentRect.width);
84
- setNaturalHeight(entry.contentRect.height);
85
- }
86
- }
87
- });
88
- return observer;
89
- };
90
-
91
- onCleanup(() => observer?.disconnect());
92
-
93
- const scale = () => computeFitScale(availableWidth(), naturalWidth());
94
- const scaledHeight = () => {
95
- const k = scale();
96
- const h = naturalHeight();
97
- return k < 1 && h !== undefined && h > 0 ? h * k : undefined;
98
- };
99
-
100
- return {
101
- observeOuter: (el) => {
102
- outerEl = el;
103
- ensureObserver()?.observe(el);
104
- },
105
- observeInner: (el) => {
106
- innerEl = el;
107
- ensureObserver()?.observe(el);
108
- },
109
- scale,
110
- scaledHeight,
111
- };
112
- }
@@ -1,528 +0,0 @@
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 { createFitScale } from './fit-scale';
11
- import { GridVisualizer } from './variant-grid';
12
- import { RadialVisualizer } from './variant-radial';
13
- import { defaultBarCount, defaultGridCount, defaultRadialBarCount, type VisualizerSize } from './sizes';
14
- import type { UniformType } from './shader-canvas';
15
-
16
- export type VisualizerVariant = 'bar' | 'grid' | 'radial' | 'wave' | 'aurora' | 'custom';
17
-
18
- const KNOWN_VARIANTS: readonly VisualizerVariant[] = ['bar', 'grid', 'radial', 'wave', 'aurora', 'custom'];
19
-
20
- /**
21
- * `aura` is LiveKit's name for this look. Ours is `aurora`. Accept theirs so
22
- * markup ported from LiveKit works unchanged, the same way `normalizeState`
23
- * accepts their room-lifecycle state names.
24
- */
25
- const VARIANT_ALIASES: Record<string, VisualizerVariant> = { aura: 'aurora' };
26
-
27
- export function normalizeVariant(input: string | undefined): VisualizerVariant {
28
- if (!input) return 'bar';
29
- if ((KNOWN_VARIANTS as readonly string[]).includes(input)) return input as VisualizerVariant;
30
- return VARIANT_ALIASES[input] ?? 'bar';
31
- }
32
-
33
- /** A consumer-supplied fragment shader, for `variant="custom"`. */
34
- export interface ShaderSpec {
35
- /**
36
- * GLSL source defining `mainImage(out vec4 fragColor, in vec2 fragCoord)`.
37
- *
38
- * MUST output premultiplied colour: `fragColor = vec4(rgb * alpha, alpha);`,
39
- * never `vec4(rgb, alpha)`. The canvas composites using the browser's
40
- * default `premultipliedAlpha: true`; a translucent edge written the
41
- * natural (straight-alpha) way gets a dark fringe where it meets a light
42
- * page background.
43
- */
44
- fragment: string;
45
- /**
46
- * Custom uniforms. The canvas DECLARES these for you; declaring them in the
47
- * shader too is a compile error.
48
- *
49
- * `type` is narrowed to `UniformType`, not a bare string: an unrecognized
50
- * type here is caught by TypeScript for a TS-authored `shader` prop, before
51
- * it can produce `uniform undefined <name>;` in the assembled source (a
52
- * confusing compile error). A consumer reaching this through the
53
- * `<kai-audio-visualizer>` custom element or an attribute-driven wrapper
54
- * bypasses this check entirely (it is JS at that boundary, not TS) --
55
- * `variant-custom.tsx`'s `customUniforms` re-checks `type` at runtime for
56
- * exactly that reason.
57
- */
58
- uniforms?: Record<string, { type: UniformType; value: number | number[] }>;
59
- }
60
-
61
- export interface AudioVisualizerProps {
62
- variant?: VisualizerVariant;
63
- state?: string;
64
- size?: VisualizerSize;
65
- barCount?: number;
66
- /** Grid only: rows and columns of the square grid. */
67
- count?: number;
68
- /** Radial only: distance from center, in px. */
69
- radius?: number;
70
- /** Grid only: ring distance for the connecting animation, in cells. */
71
- spread?: number;
72
- /** Grid only: ms between scripted frames. */
73
- interval?: number;
74
- color?: string;
75
- /** Shader variants only: pattern density, 0..1. */
76
- complexity?: number;
77
- /** Setting this makes the element an announced image instead of decorative. */
78
- label?: string;
79
- /** Live audio to analyze. NOTE: amplitude renders only while
80
- * `state === 'speaking'` unless `listeningAmplitude` is set; every other
81
- * state plays its scripted animation and the analysis output is unused. */
82
- stream?: MediaStream;
83
- /** Same speaking-only amplitude rule as `stream`: see `listeningAmplitude`. */
84
- audioElement?: HTMLMediaElement;
85
- /** Pre-computed levels. Set this and no AudioContext is ever constructed.
86
- * Same speaking-only amplitude rule as `stream`: see `listeningAmplitude`. */
87
- bands?: number[];
88
- /**
89
- * Opt in to rendering live amplitude during `listening` as well, using the
90
- * same presentation as `speaking`. Default off, which keeps LiveKit parity:
91
- * amplitude (from `stream`, `audioElement` or `bands`) renders only while
92
- * `state === 'speaking'`. Set this to show a real mic-level picture while
93
- * the USER is the one talking. Forwarded to every variant; see
94
- * `amplitudeRenderState` in variant-bar for the single mapping they share.
95
- */
96
- listeningAmplitude?: boolean;
97
- /**
98
- * Custom fragment shader for `variant="custom"`. See `ShaderSpec` for the
99
- * full contract -- most importantly, `fragment` MUST output premultiplied
100
- * colour (`vec4(rgb * alpha, alpha)`, not `vec4(rgb, alpha)`), or
101
- * translucent edges get dark fringes on light backgrounds.
102
- */
103
- shader?: ShaderSpec;
104
- /**
105
- * Shader variants only (`wave`/`aurora`/`custom`): keep animating while the
106
- * element is scrolled off screen. Default `false`, which stops the draw loop
107
- * and releases the WebGL context until it comes back -- see `ShaderCanvas`'s
108
- * `animateWhenNotVisible` for the context-budget reasoning and the cost of
109
- * opting out. Does not override `prefers-reduced-motion`.
110
- */
111
- animateWhenNotVisible?: boolean;
112
- class?: string;
113
- /**
114
- * Render each DOM variant's items yourself -- the same render-prop the
115
- * underlying bar/grid/radial component takes, imported rather than
116
- * redeclared so the two cannot drift. Not used by the shader variants
117
- * (`wave`/`aurora`/`custom`): a fragment shader has no per-item DOM for it
118
- * to replace.
119
- */
120
- children?: VariantProps['children'];
121
- /**
122
- * Explicit `'light'` or `'dark'` wins; `'auto'` (the default) follows a
123
- * live `prefers-color-scheme` listener -- the same rule
124
- * `elements/define.tsx`'s `createDarkMode` applies for every `kai-*`
125
- * element. Only the shader variants read this (aurora/wave pick a colour
126
- * pipeline with it): the three DOM variants already get dark-mode styling
127
- * for free via CSS custom properties, which a shader baking colour into a
128
- * GLSL uniform cannot do. `<kai-audio-visualizer>` forwards its own
129
- * already-resolved `theme` attribute through this prop; a bare
130
- * `<AudioVisualizer>` with no wrapping element needs it set directly.
131
- */
132
- theme?: 'light' | 'dark' | 'auto';
133
- }
134
-
135
- /** Tracks `prefers-reduced-motion`, live. */
136
- export function usePrefersReducedMotion(): Accessor<boolean> {
137
- const [reduced, setReduced] = createSignal(false);
138
-
139
- createEffect(() => {
140
- if (typeof matchMedia !== 'function') return;
141
- const mq = matchMedia('(prefers-reduced-motion: reduce)');
142
- setReduced(mq.matches);
143
- const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
144
- mq.addEventListener?.('change', onChange);
145
- onCleanup(() => mq.removeEventListener?.('change', onChange));
146
- });
147
-
148
- return reduced;
149
- }
150
-
151
- /**
152
- * Resolves `theme` (`'light' | 'dark' | 'auto'`) to a boolean, mirroring
153
- * `elements/define.tsx`'s `createDarkMode` rule exactly: an explicit value
154
- * wins, `'auto'` (the default) follows a live `prefers-color-scheme`
155
- * listener.
156
- *
157
- * This is a SEPARATE implementation of that rule, not an import of it:
158
- * `components/` is the framework-agnostic layer `elements/` wraps (see the
159
- * kit's architecture), so it cannot depend on `elements/define.tsx` without
160
- * inverting that direction. When driven through `<kai-audio-visualizer>`,
161
- * the facade has already resolved `'auto'` against its OWN listener (the one
162
- * already wired to the visible `.dark` class) before handing this an
163
- * explicit `'light'`/`'dark'` -- so this hook's own listener only ever
164
- * actually decides anything for a bare `<AudioVisualizer>` used directly,
165
- * with no wrapping element at all.
166
- */
167
- export function useResolvedDark(theme: () => string | undefined): Accessor<boolean> {
168
- const [systemDark, setSystemDark] = createSignal(false);
169
-
170
- createEffect(() => {
171
- if (typeof matchMedia !== 'function') return;
172
- const mq = matchMedia('(prefers-color-scheme: dark)');
173
- setSystemDark(mq.matches);
174
- const onChange = (e: MediaQueryListEvent) => setSystemDark(e.matches);
175
- mq.addEventListener?.('change', onChange);
176
- onCleanup(() => mq.removeEventListener?.('change', onChange));
177
- });
178
-
179
- return () => {
180
- const t = theme() ?? 'auto';
181
- return t === 'dark' || (t === 'auto' && systemDark());
182
- };
183
- }
184
-
185
- /**
186
- * Props a shader variant (`wave`, `aurora`, `custom`) receives from the
187
- * dispatcher: everything a DOM variant gets, minus `children` (a fragment
188
- * shader has no per-item DOM for a render-prop to replace, so the field is
189
- * dropped rather than inherited-but-unused), plus the scalar `volume` reading
190
- * (shaders read one number, not per-band levels) and the shader-specific
191
- * knobs. This is the compile-time contract Tasks 12 to 15 build against --
192
- * keep it honest rather than casting around a looser type at the render site.
193
- */
194
- export interface ShaderVariantProps extends Omit<VariantProps, 'children'> {
195
- volume: number;
196
- /** Pattern density, 0..1. */
197
- complexity?: number;
198
- /** Only meaningful for `variant="custom"`. */
199
- shader?: ShaderSpec;
200
- /**
201
- * Already-resolved: `true` selects the dark colour pipeline, `false`
202
- * selects light -- matching `elements/define.tsx`'s `createDarkMode`
203
- * output exactly (`classList={{ dark: isDark() }}`). This is that SAME
204
- * resolved value forwarded down, not a re-derivation, so a shader baking
205
- * colour into a GLSL uniform never needs its own `prefers-color-scheme`
206
- * listener for the common case. Optional: a shader mounted standalone (no
207
- * dispatcher resolving `theme` above it) may fall back to reading the
208
- * media query itself.
209
- */
210
- dark?: boolean;
211
- /**
212
- * Keep animating while off screen instead of releasing the WebGL context.
213
- * Relayed straight through to `ShaderCanvas`, which owns the behaviour and
214
- * documents the trade-off; the variants are pure conduits for it.
215
- */
216
- animateWhenNotVisible?: boolean;
217
- /**
218
- * Call this if the shader cannot render at all -- most commonly
219
- * `canvas.getContext('webgl')` returning null. Permanent for this mount:
220
- * the dispatcher swaps to the bar fallback and will not retry the shader
221
- * on any later reactive update, only on switching to a different variant.
222
- */
223
- onUnavailable: () => void;
224
- }
225
-
226
- /**
227
- * Shader variants live behind a dynamic import so the WebGL runtime and the
228
- * GLSL strings (about 25 to 30 KB) never reach a consumer who does not ask for
229
- * them.
230
- *
231
- * This MUST stay dynamic. `vite.config.ts` disables tree-shaking on the
232
- * register-all bundle by design, so a static import here would put the whole
233
- * shader path into `kai.es.js` for everyone, including a `<kai-chat>`-only
234
- * user. A dynamic import splits into its own chunk under `treeshake: false`;
235
- * a static one does not (verified empirically). Never convert these entries
236
- * to static imports.
237
- */
238
- const SHADER_VARIANTS: Record<string, () => Promise<{ default: Component<ShaderVariantProps> }>> = {
239
- wave: () => import('./variant-wave'),
240
- aurora: () => import('./variant-aurora'),
241
- custom: () => import('./variant-custom'),
242
- };
243
-
244
- export function AudioVisualizer(props: AudioVisualizerProps): JSX.Element {
245
- const variant = () => normalizeVariant(props.variant);
246
- const size = () => props.size ?? 'md';
247
- const state = (): VisualizerState => normalizeState(props.state);
248
- const reduced = usePrefersReducedMotion();
249
- // Only the shader match arm below reads this; the three DOM variants get
250
- // dark-mode styling for free via CSS custom properties.
251
- const resolvedDark = useResolvedDark(() => props.theme);
252
-
253
- const isShader = () => variant() in SHADER_VARIANTS;
254
-
255
- // How many elements the active variant actually renders. Grid keys off its
256
- // own column default (each column shares one band across every row -- see
257
- // GridVisualizer), radial off its own bar-count default, everything else
258
- // off the bar count -- pulled from `sizes.ts` rather than re-derived here,
259
- // so this stays in sync with what each variant actually renders.
260
- const elementCount = () => {
261
- if (variant() === 'grid') return props.count ?? defaultGridCount(size());
262
- if (variant() === 'radial') return props.barCount ?? defaultRadialBarCount(size());
263
- return props.barCount ?? defaultBarCount(size());
264
- };
265
-
266
- // Only ceil(n/2) bands are requested from the analyser, not one per
267
- // element: useAudioAnalysis's output is mirrored back out to the full
268
- // elementCount below (centre-out for bar/grid, across the ring's vertical
269
- // axis for radial), which is what turns a real voice's natural
270
- // low-to-high spectral tilt into a shape that grows from the centre
271
- // outward instead of always ramping in one direction. See
272
- // mirrorBandsCenterOut / mirrorBandsAroundRing in primitives/audio-bands.ts
273
- // for the full rationale and the real-clip measurements behind it.
274
- const bandCount = () => Math.ceil(elementCount() / 2);
275
-
276
- // A caller-supplied `bands` array short-circuits Web Audio entirely, which is
277
- // what keeps the headless and SSR paths free of an AudioContext.
278
- const source = () => (props.bands ? undefined : props.stream ?? props.audioElement);
279
- // Pass the ACCESSOR, not its current value: useAudioAnalysis reads it inside
280
- // its own effect, so a later variant/size/barCount change rebuilds the
281
- // analyser at the new count instead of leaving it wired to whatever the
282
- // count happened to be at mount.
283
- const analysis = useAudioAnalysis(source, { bands: bandCount });
284
-
285
- // A caller-supplied `bands` array is used exactly as given -- it already
286
- // has whatever shape the caller intends, mirrored or not, and mirroring it
287
- // again here would be a second, unwanted transform on data we don't own.
288
- // The analyser's own output is only ceil(n/2) values (bandCount above);
289
- // mirror it back out to the FULL elementCount so it lines up EXACTLY with
290
- // what each variant's own `normalizeVolumeBands(props.bands, count())`
291
- // expects. Matching the count exactly here, rather than leaning on that
292
- // pad-by-repeating-the-last-value, matters specifically because the mirror
293
- // makes a length mismatch look like a plausible (but wrong) shape instead
294
- // of an obviously broken one.
295
- const bands = () => {
296
- if (props.bands) return props.bands;
297
- return variant() === 'radial'
298
- ? mirrorBandsAroundRing(analysis.bands(), elementCount())
299
- : mirrorBandsCenterOut(analysis.bands(), elementCount());
300
- };
301
-
302
- // The shader variants read `volume`, a scalar, not `bands`. When
303
- // caller-supplied `bands` short-circuits Web Audio above, `analysis.volume()`
304
- // sits at its initial 0 forever (nothing ever computes it), so a shader
305
- // fed `bands` would otherwise look static despite the DOM variants next to
306
- // it correctly animating. Derive it from the same `bands` this component
307
- // is already using -- root-mean-square, matching how `reduceToVolume` in
308
- // `primitives/audio-bands.ts` reduces real analyser output to a scalar, so
309
- // the two paths agree in character. Not reused directly: `reduceToVolume`
310
- // takes a `Uint8Array` of byte frequency data (0..255), not a `number[]` of
311
- // already-normalized 0..1 levels, so contorting it to accept both shapes
312
- // would be worse than the few lines below.
313
- const volume = () => {
314
- if (!props.bands) return analysis.volume();
315
- if (props.bands.length === 0) return 0;
316
- let sum = 0;
317
- for (const b of props.bands) sum += b * b;
318
- return Math.sqrt(sum / props.bands.length);
319
- };
320
-
321
- // Lazily loaded shader component, or undefined until it resolves. Failure is
322
- // not fatal: the bar fallback below stays on screen.
323
- const [Shader, setShader] = createSignal<Component<ShaderVariantProps> | undefined>();
324
- // A mounted shader can ALSO fail after its chunk loads fine -- most notably
325
- // WebGL being unavailable, which only a mounted component can discover (it
326
- // is the one calling `canvas.getContext('webgl')`). This is a separate flag
327
- // from `Shader` itself: `Shader()` staying undefined already makes a failed
328
- // *import* permanent (nothing ever sets it), but a shader that DID load and
329
- // mount needs its own signal to force the fallback despite `Shader()` being
330
- // truthy. `onUnavailable` below is what a mounted shader calls to set it.
331
- const [unavailable, setUnavailable] = createSignal(false);
332
- createEffect(() => {
333
- const v = variant();
334
- const load = SHADER_VARIANTS[v];
335
- setShader(undefined);
336
- // Switching variants gets a fresh attempt: a failure on `aurora` must not
337
- // carry over and permanently block `wave` too.
338
- setUnavailable(false);
339
- if (!load) return;
340
- let cancelled = false;
341
- void load()
342
- .then((m) => { if (!cancelled) setShader(() => m.default); })
343
- .catch((err) => {
344
- // Same `cancelled` check as the `.then` above. The two handlers were
345
- // asymmetric: a rejection arriving after this effect was disposed
346
- // still warned, even though there is no longer a component to fall
347
- // back to and nothing the message could usefully tell anyone. Worse,
348
- // the warning surfaces against whatever is running by the time the
349
- // rejection lands rather than against the mount that asked for the
350
- // chunk, so it reads as a fault in unrelated code -- in a test run,
351
- // in an unrelated test.
352
- if (cancelled) return;
353
- console.warn(
354
- `<kai-audio-visualizer variant="${v}">: failed to load the shader chunk, falling back to bars.`,
355
- err,
356
- );
357
- });
358
- onCleanup(() => { cancelled = true; });
359
- });
360
-
361
- // Props every variant shares, MINUS `bands` -- deliberately. Solid compiles
362
- // a component spread (`{...shared()}` below) into per-key getters that all
363
- // call this SAME function: reading ANY one key re-invokes the whole thing,
364
- // so if `bands()` lived in here, reading `state` (or `size`/`frozen`/
365
- // `color`) would transitively subscribe the reader to `bands()` too, which
366
- // updates ~31 times a second with live or synthetic audio. That is exactly
367
- // what caused two real bugs downstream: `use-sequencer.ts`'s effect reading
368
- // `frozen`/`state` re-ran at band cadence and called `setTick(0)` on every
369
- // run, so the tick could never advance (every scripted animation looked
370
- // dead); `shader-canvas.tsx`'s compile effect reading `precision`/`fragment`
371
- // re-ran the same way and recompiled the GL program 65-70 times in 4
372
- // seconds while restamping its animation clock (`iTime` pinned under 0.33s,
373
- // periodically negative). Both were patched locally with memos in those
374
- // files (kept -- defence in depth, cheap, and they document the hazard),
375
- // but the leak was still here for the next reader: `variant-wave.tsx`,
376
- // `variant-aurora.tsx`, and `variant-custom.tsx` each have their OWN
377
- // state/frozen-driven tween effect with no local memo at all, so they were
378
- // live instances of the identical bug (`.to()` restarts a tween's clock on
379
- // every call -- see `create-tween.ts` -- so a 31Hz re-run means a tween
380
- // never visibly progresses). Fixing it here, at the one place the bundling
381
- // happens, closes all of those at once rather than requiring every current
382
- // and future reader to remember to memoize defensively.
383
- //
384
- // `bands` GENUINELY must stay reactive -- this split is about not dragging
385
- // it into unrelated reads, not about freezing it. It is passed explicitly,
386
- // `bands={bands()}`, at every call site below, exactly like `volume`,
387
- // `complexity`, etc. already are: an explicit prop gets its OWN getter,
388
- // entirely independent of this one.
389
- // `listeningAmplitude` is safe inside this bundle (unlike `bands`): it is a
390
- // boolean a caller sets once, not a signal driven at audio cadence, so
391
- // reading it through the shared getters cannot drag in a hot subscription.
392
- const shared = (): Omit<VariantProps, 'bands'> => ({
393
- state: state(),
394
- size: size(),
395
- frozen: reduced(),
396
- color: props.color,
397
- listeningAmplitude: props.listeningAmplitude,
398
- });
399
-
400
- // The three DOM variants (bar/grid/radial), including the bar fallback
401
- // shown while a shader chunk loads or fails, additionally get the
402
- // caller's render-prop. `children` is bundled here (unlike `bands`) because
403
- // it does not churn -- it is a callback reference a caller sets once, not a
404
- // signal driven by audio, so reading it cannot drag in anything that does.
405
- const domShared = (): Omit<VariantProps, 'bands'> => ({ ...shared(), children: props.children });
406
-
407
- const a11y = () =>
408
- props.label
409
- ? { role: 'img' as const, 'aria-label': props.label }
410
- : { 'aria-hidden': 'true' as const };
411
-
412
- // Fit-to-container: measured by createFitScale (see fit-scale.ts for the
413
- // full design rationale). At or above the natural width this contributes
414
- // nothing but the `max-width` clamp, so the designed px metrics render
415
- // exactly; below it, the inner wrapper scales the whole picture down and
416
- // the outer adopts the scaled height so layout follows the visual.
417
- const fit = createFitScale();
418
-
419
- return (
420
- <div
421
- class={cn('inline-flex', props.class)}
422
- {...a11y()}
423
- ref={fit.observeOuter}
424
- style={{
425
- // What lets a narrow container actually constrain this element: the
426
- // content is fixed-px, so without the clamp the host box would just
427
- // overflow (and clip at the viewport edge, the reported defect).
428
- 'max-width': '100%',
429
- // While shrunk, the inner wrapper's LAYOUT box deliberately keeps its
430
- // natural size (that is what keeps the measurement honest above), so
431
- // it overhangs this box below/right of the scaled visual. `hidden`
432
- // clips that overhang for BOTH painting and hit-testing -- without it
433
- // the stale layout box covered a sibling button and ate its pointer
434
- // events (W5 live finding, mechanism 2). Nothing visible is clipped:
435
- // the scaled visual is exactly this box by construction.
436
- // `vertical-align: top` rides along with `overflow: hidden`, which is
437
- // what makes it necessary: hidden overflow moves an inline-flex box's
438
- // baseline to its bottom border edge, so the surrounding line box
439
- // grows by the font strut's descent below it and the host measures
440
- // ~6px taller than the box (measured in headed Chromium: 107.2 vs
441
- // 101.2). Top alignment opts out of baseline layout entirely. Scoped
442
- // to the scaled state so default (unscaled) inline placement beside
443
- // text keeps its original baseline behavior.
444
- ...(fit.scaledHeight() !== undefined
445
- ? { height: `${fit.scaledHeight()}px`, overflow: 'hidden', 'vertical-align': 'top' }
446
- : {}),
447
- }}
448
- >
449
- <div
450
- ref={fit.observeInner}
451
- style={{
452
- // THE FEEDBACK-LOOP GUARD (W5 live finding, mechanism 1). The inner
453
- // wrapper is a flex item of the outer, and a flex item's default
454
- // `align-self: stretch` makes its LAYOUT height follow the outer's
455
- // -- including the scaled height this component itself adopts below.
456
- // The ResizeObserver measuring this element for its NATURAL size
457
- // then reads back the adopted height, k multiplies in again, and
458
- // the element spirals (measured in headed Chromium: 224px -> 45.7
459
- // -> 0.85 -> 0.38). `flex-start` + `flex: none` pin this box to its
460
- // intrinsic content size in BOTH axes, so the measurement can never
461
- // see anything this component wrote. Unconditional: at scale 1 both
462
- // are visually identical to the defaults for a single item that
463
- // defines its container's size.
464
- flex: '0 0 auto',
465
- 'align-self': 'flex-start',
466
- ...(fit.scale() < 1
467
- ? { transform: `scale(${fit.scale()})`, 'transform-origin': 'top left' }
468
- : {}),
469
- }}
470
- >
471
- <Switch
472
- fallback={<BarVisualizer {...domShared()} bands={bands()} barCount={props.barCount} />}
473
- >
474
- <Match when={variant() === 'grid'}>
475
- <GridVisualizer
476
- {...domShared()}
477
- bands={bands()}
478
- count={props.count}
479
- spread={props.spread}
480
- interval={props.interval}
481
- />
482
- </Match>
483
- <Match when={variant() === 'radial'}>
484
- <RadialVisualizer
485
- {...domShared()}
486
- bands={bands()}
487
- barCount={props.barCount}
488
- radius={props.radius}
489
- />
490
- </Match>
491
- <Match when={isShader()}>
492
- {/*
493
- Bars stand in until the chunk resolves, and permanently if it
494
- cannot -- either because the import itself failed (`Shader()`
495
- never becomes truthy) or because the mounted shader called
496
- `onUnavailable` (checked here via `!unavailable()`, since
497
- `Shader()` alone cannot tell a working component from one that
498
- loaded fine but can't actually render, e.g. no WebGL). Both cases
499
- fall back to the same `BarVisualizer`, and `when` still narrows to
500
- the component reference on the success path, matching the
501
- `(Comp) => ...` extraction below.
502
- */}
503
- <Show
504
- when={!unavailable() && Shader()}
505
- fallback={<BarVisualizer {...domShared()} bands={bands()} barCount={props.barCount} />}
506
- >
507
- {(Comp) => {
508
- const C = Comp();
509
- return (
510
- <C
511
- {...shared()}
512
- bands={bands()}
513
- volume={volume()}
514
- complexity={props.complexity}
515
- shader={props.shader}
516
- dark={resolvedDark()}
517
- animateWhenNotVisible={props.animateWhenNotVisible}
518
- onUnavailable={() => setUnavailable(true)}
519
- />
520
- );
521
- }}
522
- </Show>
523
- </Match>
524
- </Switch>
525
- </div>
526
- </div>
527
- );
528
- }