@kitn.ai/ui 0.18.2 → 0.20.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.
- package/LICENSE +6 -0
- package/NOTICE +54 -0
- package/dist/components/audio-visualizer/audio-visualizer.voice-fixture.d.ts +59 -0
- package/dist/components/audio-visualizer/aurora.glsl.d.ts +114 -0
- package/dist/components/audio-visualizer/index.d.ts +155 -0
- package/dist/components/audio-visualizer/shader-canvas.d.ts +130 -0
- package/dist/components/audio-visualizer/sizes.d.ts +25 -0
- package/dist/components/audio-visualizer/variant-aurora.d.ts +89 -0
- package/dist/components/audio-visualizer/variant-bar.d.ts +46 -0
- package/dist/components/audio-visualizer/variant-custom.d.ts +73 -0
- package/dist/components/audio-visualizer/variant-grid.d.ts +22 -0
- package/dist/components/audio-visualizer/variant-radial.d.ts +13 -0
- package/dist/components/audio-visualizer/variant-wave.d.ts +11 -0
- package/dist/components/audio-visualizer/wave.glsl.d.ts +16 -0
- package/dist/components/thread.d.ts +54 -0
- package/dist/create-tween-hvpmSuwW.js +7 -0
- package/dist/custom-elements.json +477 -0
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.d.ts +1 -0
- package/dist/elements/audio-visualizer.js +1 -0
- package/dist/elements/autoloader.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-C2LTALdM.js → Icon-Tj58ES_d.js} +1 -1
- package/dist/elements/chunks/action-icons-ns1Fnedj.js +1 -0
- package/dist/elements/chunks/arrow-left-DQocUFvy.js +1 -0
- package/dist/elements/chunks/{attachments-DLJcWCDV.js → attachments-mCB5F1Fq.js} +1 -1
- package/dist/elements/chunks/audio-visualizer-BqNy7dfm.js +1 -0
- package/dist/elements/chunks/{badge-BTNZTODL.js → badge-B4Zuajl2.js} +1 -1
- package/dist/elements/chunks/{button-DBDZebGr.js → button-ZWNif3--.js} +1 -1
- package/dist/elements/chunks/{card-YfCyI4fH.js → card-BkHbp4BA.js} +1 -1
- package/dist/elements/chunks/chat-thread-CkRN6pZ7.js +1 -0
- package/dist/elements/chunks/check-B9-gn6SS.js +1 -0
- package/dist/elements/chunks/chevron-down-BNsxc2hk.js +1 -0
- package/dist/elements/chunks/chevron-right-Hv00StBO.js +1 -0
- package/dist/elements/chunks/circle-0e3KJTDr.js +1 -0
- package/dist/elements/chunks/circle-check-BNAS0JEy.js +1 -0
- package/dist/elements/chunks/{circle-x-pDtzlghQ.js → circle-x-B7faJFlY.js} +1 -1
- package/dist/elements/chunks/{code-block-DZ6mz6vs.js → code-block-BFUHn6Va.js} +1 -1
- package/dist/elements/chunks/collapsible-DM1Ckd4e.js +1 -0
- package/dist/elements/chunks/{composer-BZyPU6gD.js → composer-BguS0QL_.js} +7 -3
- package/dist/elements/chunks/{context-Cx6eQA_G.js → context-Cb7k4F-v.js} +1 -1
- package/dist/elements/chunks/{conversation-list-C4xTQSpT.js → conversation-list-CvoAkWhN.js} +1 -1
- package/dist/elements/chunks/create-tween-oVvAIG8r.js +7 -0
- package/dist/elements/chunks/{default-input-BZcwyKcP.js → default-input-Cqqilg9t.js} +1 -1
- package/dist/elements/chunks/define-D1din7Ld.js +1 -0
- package/dist/elements/chunks/{disclosure-BcBs--Jf.js → disclosure-CIZ5c8nO.js} +1 -1
- package/dist/elements/chunks/{download-BDQwcFav.js → download-Dsbg3ste.js} +1 -1
- package/dist/elements/chunks/{dropdown-wgs8xCJZ.js → dropdown-BDd1-Zgi.js} +1 -1
- package/dist/elements/chunks/{ellipsis-CUiEJ6R1.js → ellipsis-Dw6qrss-.js} +1 -1
- package/dist/elements/chunks/{external-link-DJUmC7g2.js → external-link-Vt74zAoL.js} +1 -1
- package/dist/elements/chunks/{file-text-DOYfYvrh.js → file-text-CZrZF6_v.js} +1 -1
- package/dist/elements/chunks/{file-tree-DQzNQrbr.js → file-tree-DF4okKyo.js} +1 -1
- package/dist/elements/chunks/{folder-DBDUBANz.js → folder-glP5rha4.js} +1 -1
- package/dist/elements/chunks/hover-card-Cmm6Q53m.js +1 -0
- package/dist/elements/chunks/icon-BLb8beoi.js +1 -0
- package/dist/elements/chunks/{info-DBQl-m6e.js → info-CIsv92JK.js} +1 -1
- package/dist/elements/chunks/{input-DZD67EvW.js → input-Dds1kFyt.js} +1 -1
- package/dist/elements/chunks/{kbd-DbyVrlnU.js → kbd-De_xbgi9.js} +1 -1
- package/dist/elements/chunks/{link-vko7AttA.js → link-i4r_k7z4.js} +1 -1
- package/dist/elements/chunks/{loader-Cg6rO8tQ.js → loader-BmQH4s3V.js} +1 -1
- package/dist/elements/chunks/{markdown-n3Xbj3PR.js → markdown-Zi21WhUz.js} +7 -7
- package/dist/elements/chunks/{message-CgKytiSB.js → message-BqyXDN3-.js} +1 -1
- package/dist/elements/chunks/{message-feedback-Ds3-US0e.js → message-feedback-Djr3z6Yb.js} +1 -1
- package/dist/elements/chunks/message-square-vFAgbDaX.js +1 -0
- package/dist/elements/chunks/{minimize-2-3kCdE0q3.js → minimize-2--ifeCGrl.js} +1 -1
- package/dist/elements/chunks/{model-switcher-BMtXGwLU.js → model-switcher-B94YAKsM.js} +1 -1
- package/dist/elements/chunks/{overlay-B1-MBI6h.js → overlay-kD8dwRv_.js} +1 -1
- package/dist/elements/chunks/{paperclip-DDEfmkHc.js → paperclip-q21dYOSK.js} +1 -1
- package/dist/elements/chunks/{progress-bar-Dwbiyv3s.js → progress-bar-BrAIIaaJ.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-9lvgFcEa.js → prompt-suggestion-Bjn2K-fR.js} +1 -1
- package/dist/elements/chunks/{reasoning-DzHWsdAJ.js → reasoning-DTpCGtnY.js} +1 -1
- package/dist/elements/chunks/resizable-Dp4JExql.js +1 -0
- package/dist/elements/chunks/{rotate-cw-jpisOdOV.js → rotate-cw-CFWWkpj2.js} +1 -1
- package/dist/elements/chunks/{scroll-area-g5Ze3K6f.js → scroll-area-Bb0kHH1j.js} +1 -1
- package/dist/elements/chunks/scroll-button-B0cH5fXf.js +1 -0
- package/dist/elements/chunks/{separator-Bmjc5XyX.js → separator-9kRgr1X9.js} +1 -1
- package/dist/elements/chunks/{settings-Cz1bMJP9.js → settings-B8lmec4D.js} +1 -1
- package/dist/elements/chunks/{settings-group-9imY3kRA.js → settings-group-I0hEuXiN.js} +1 -1
- package/dist/elements/chunks/{share-BdenF0wQ.js → share-C3lTiYGt.js} +1 -1
- package/dist/elements/chunks/skeleton-DSXW5Yya.js +1 -0
- package/dist/elements/chunks/{slots-DkZ-Cmha.js → slots--V19HwUb.js} +1 -1
- package/dist/elements/chunks/{star-DY6z1UUH.js → star-CSJtGxgv.js} +1 -1
- package/dist/elements/chunks/{store-th4VyJXM.js → store-CHyW95TP.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-CEi-tlXr.js → text-shimmer-Cn9TmNpI.js} +1 -1
- package/dist/elements/chunks/textarea-CUwqAX0G.js +1 -0
- package/dist/elements/chunks/{thumbs-up-BR33KLfA.js → thumbs-up-PNVVThEF.js} +1 -1
- package/dist/elements/chunks/{toast-store-BEMda0fv.js → toast-store-DKbQn3Wn.js} +1 -1
- package/dist/elements/chunks/{tool-TMued3WO.js → tool-DjTRpIpl.js} +1 -1
- package/dist/elements/chunks/tooltip-CgBZCVKp.js +1 -0
- package/dist/elements/chunks/{triangle-alert-B07fX3Q8.js → triangle-alert-DsHTHCPm.js} +1 -1
- package/dist/elements/chunks/use-card-resolution-BSu_XHjH.js +1 -0
- package/dist/elements/chunks/variant-aurora-Dc4nQUia.js +218 -0
- package/dist/elements/chunks/variant-custom-y_3kVQhS.js +1 -0
- package/dist/elements/chunks/variant-wave-D5OR1-9Z.js +116 -0
- package/dist/elements/chunks/{video-MKp3LmNT.js → video-B7vmenoC.js} +1 -1
- package/dist/elements/chunks/x-GRUeeQFT.js +1 -0
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/define.d.ts +11 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +1 -1
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slots.d.ts +15 -0
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.d.ts +1 -0
- package/dist/elements/thread.js +1 -0
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +62 -0
- package/dist/index.js +3411 -3328
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +70 -4
- package/dist/llms/llms.txt +3 -3
- package/dist/primitives/audio-bands.d.ts +106 -0
- package/dist/primitives/create-tween.d.ts +33 -0
- package/dist/primitives/use-audio-analysis.d.ts +149 -0
- package/dist/primitives/use-sequencer.d.ts +13 -0
- package/dist/primitives/use-voice-recorder.d.ts +1 -0
- package/dist/primitives/visualizer-sequences.d.ts +39 -0
- package/dist/react/index.d.ts +105 -0
- package/dist/react/use-voice-input.d.ts +15 -0
- package/dist/react.js +269 -240
- package/dist/register-impl-DQyejdfb.js +145 -0
- package/dist/test-utils/fake-clock.d.ts +45 -0
- package/dist/ui/resizable.d.ts +9 -3
- package/dist/variant-aurora-DVtGqVH6.js +218 -0
- package/dist/variant-custom-Dif-IV4u.js +1 -0
- package/dist/variant-wave-DbfNdvSi.js +116 -0
- package/frameworks/react/index.tsx +74 -1
- package/frameworks/react/use-voice-input.tsx +59 -0
- package/llms-full.txt +70 -4
- package/llms.txt +3 -3
- package/package.json +32 -7
- package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +257 -0
- package/src/components/audio-visualizer/aurora.glsl.ts +330 -0
- package/src/components/audio-visualizer/index.tsx +442 -0
- package/src/components/audio-visualizer/shader-canvas.tsx +846 -0
- package/src/components/audio-visualizer/sizes.ts +54 -0
- package/src/components/audio-visualizer/variant-aurora.tsx +299 -0
- package/src/components/audio-visualizer/variant-bar.tsx +143 -0
- package/src/components/audio-visualizer/variant-custom.tsx +338 -0
- package/src/components/audio-visualizer/variant-grid.tsx +170 -0
- package/src/components/audio-visualizer/variant-radial.tsx +158 -0
- package/src/components/audio-visualizer/variant-wave.tsx +125 -0
- package/src/components/audio-visualizer/wave.glsl.ts +130 -0
- package/src/components/component-meta.json +34 -32
- package/src/components/composer.tsx +5 -0
- package/src/components/thread.tsx +177 -0
- package/src/elements/audio-visualizer.tsx +183 -0
- package/src/elements/chat-workspace.tsx +1 -1
- package/src/elements/compiled.css +1 -1
- package/src/elements/define.tsx +12 -1
- package/src/elements/element-manifest.json +8 -0
- package/src/elements/element-meta.json +287 -0
- package/src/elements/element-types.d.ts +62 -0
- package/src/elements/icon-names.json +2 -0
- package/src/elements/menu.tsx +7 -4
- package/src/elements/register-impl.ts +3 -0
- package/src/elements/resizable.tsx +78 -2
- package/src/elements/slots.ts +42 -0
- package/src/elements/thread.tsx +105 -0
- package/src/primitives/audio-bands.ts +174 -0
- package/src/primitives/create-tween.ts +214 -0
- package/src/primitives/use-audio-analysis.ts +321 -0
- package/src/primitives/use-sequencer.ts +53 -0
- package/src/primitives/use-voice-recorder.ts +16 -4
- package/src/primitives/visualizer-sequences.ts +350 -0
- package/src/test-utils/fake-clock.ts +88 -0
- package/src/ui/action-icons.ts +2 -1
- package/src/ui/icon.tsx +4 -0
- package/src/ui/overlay.tsx +8 -1
- package/src/ui/resizable.tsx +47 -9
- package/dist/elements/chunks/action-icons-Dtl8Ox5D.js +0 -1
- package/dist/elements/chunks/arrow-left-GbBceGBE.js +0 -1
- package/dist/elements/chunks/chat-thread-Dz2qbzvI.js +0 -1
- package/dist/elements/chunks/check-fhmRksq_.js +0 -1
- package/dist/elements/chunks/chevron-down-DcRg1aRU.js +0 -1
- package/dist/elements/chunks/chevron-right-ChzBcNJ-.js +0 -1
- package/dist/elements/chunks/circle-B4fTe9v6.js +0 -1
- package/dist/elements/chunks/circle-check-CJV-3Rem.js +0 -1
- package/dist/elements/chunks/collapsible-CWUAShMC.js +0 -1
- package/dist/elements/chunks/define-CNSKFZCd.js +0 -1
- package/dist/elements/chunks/hover-card-BQGdGAQ3.js +0 -1
- package/dist/elements/chunks/icon-CAyRqVrg.js +0 -1
- package/dist/elements/chunks/message-square-MmRkvc-M.js +0 -1
- package/dist/elements/chunks/resizable-Bq_eHxUm.js +0 -1
- package/dist/elements/chunks/skeleton-Pjc5jiaY.js +0 -1
- package/dist/elements/chunks/textarea-CboigarR.js +0 -1
- package/dist/elements/chunks/tooltip-Cft_Rxiw.js +0 -1
- package/dist/elements/chunks/use-card-resolution-M36zVciB.js +0 -1
- package/dist/elements/chunks/x-Ckwicf6g.js +0 -1
- package/dist/register-impl-DcjZDELj.js +0 -141
|
@@ -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
|
+
}
|