@yewdiel21/motion-kit 0.14.0 → 0.15.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 (154) hide show
  1. package/README.md +174 -155
  2. package/bin/motion-kit.mjs +3 -3
  3. package/dist/auto/index.d.ts +6 -3
  4. package/dist/auto/index.js +8 -2
  5. package/dist/auto/index.js.map +1 -1
  6. package/dist/{chunk-EY4BJKC4.js → chunk-6IRJYXJU.js} +2 -2
  7. package/dist/{chunk-CU6SXW3E.js → chunk-A6QQCF6V.js} +1309 -310
  8. package/dist/chunk-A6QQCF6V.js.map +1 -0
  9. package/dist/chunk-JXRS3OEA.js +3638 -0
  10. package/dist/chunk-JXRS3OEA.js.map +1 -0
  11. package/dist/css/components/add-to-cart.css +72 -0
  12. package/dist/css/components/bidirectional-slider.css +168 -0
  13. package/dist/css/components/data-table.css +121 -26
  14. package/dist/css/components/filter-builder.css +10 -15
  15. package/dist/css/components/heatmap-chart.css +391 -0
  16. package/dist/css/components/hover-card.css +8 -2
  17. package/dist/css/components/image-preview.css +90 -2
  18. package/dist/css/components/json-viewer.css +33 -1
  19. package/dist/css/components/nav.css +102 -102
  20. package/dist/css/components/orbit-menu.css +130 -0
  21. package/dist/css/components/pagination.css +10 -5
  22. package/dist/css/components/pie-chart.css +390 -0
  23. package/dist/css/components/pill-button.css +135 -0
  24. package/dist/css/components/radar-chart.css +370 -0
  25. package/dist/css/components/radial-chart.css +398 -0
  26. package/dist/css/components/sankey-chart.css +358 -0
  27. package/dist/css/components/scroll-island.css +111 -0
  28. package/dist/css/components/select.css +136 -0
  29. package/dist/css/components/toolbar.css +122 -0
  30. package/dist/css/components/tree-view.css +79 -0
  31. package/dist/css/components/voice-dots.css +70 -0
  32. package/dist/elements/index.d.ts +180 -7
  33. package/dist/elements/index.js +287 -6
  34. package/dist/elements/index.js.map +1 -1
  35. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  36. package/dist/index.css +1832 -168
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +33 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +117 -11
  41. package/dist/react/index.js +314 -13
  42. package/dist/react/index.js.map +1 -1
  43. package/dist/{reduced-BBUusld6.d.ts → reduced-6X_bns60.d.ts} +1 -1
  44. package/dist/{reveal-DEvp6U6T.d.ts → reveal-BfyKKsw4.d.ts} +97 -4
  45. package/dist/store-CA2Qcun_.d.ts +453 -0
  46. package/package.json +9 -16
  47. package/registry.json +1096 -948
  48. package/src/auto/auto-init.ts +6 -0
  49. package/src/components/add-to-cart/README.md +34 -0
  50. package/src/components/add-to-cart/add-to-cart.css +71 -0
  51. package/src/components/add-to-cart/element.ts +20 -0
  52. package/src/components/add-to-cart/engine.ts +206 -0
  53. package/src/components/add-to-cart/react.tsx +29 -0
  54. package/src/components/bidirectional-slider/README.md +46 -0
  55. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  56. package/src/components/bidirectional-slider/element.ts +42 -0
  57. package/src/components/bidirectional-slider/engine.ts +289 -0
  58. package/src/components/bidirectional-slider/react.tsx +45 -0
  59. package/src/components/chart/README.md +10 -0
  60. package/src/components/chart/element.ts +2 -0
  61. package/src/components/chart/engine.ts +50 -23
  62. package/src/components/chart/parts.ts +237 -0
  63. package/src/components/chart/react.tsx +2 -2
  64. package/src/components/chart/use-chart.ts +55 -0
  65. package/src/components/data-table/README.md +3 -3
  66. package/src/components/data-table/data-table.css +121 -26
  67. package/src/components/data-table/engine.ts +81 -29
  68. package/src/components/editable-table/engine.ts +67 -11
  69. package/src/components/filter-builder/engine.ts +33 -2
  70. package/src/components/filter-builder/filter-builder.css +10 -15
  71. package/src/components/heatmap-chart/README.md +48 -0
  72. package/src/components/heatmap-chart/element.ts +21 -0
  73. package/src/components/heatmap-chart/engine.ts +281 -0
  74. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  75. package/src/components/heatmap-chart/react.tsx +14 -0
  76. package/src/components/hover-card/engine.ts +30 -1
  77. package/src/components/hover-card/hover-card.css +8 -2
  78. package/src/components/image-preview/element.ts +1 -1
  79. package/src/components/image-preview/engine.ts +95 -15
  80. package/src/components/image-preview/image-preview.css +90 -2
  81. package/src/components/image-preview/react.tsx +3 -3
  82. package/src/components/json-viewer/engine.ts +11 -2
  83. package/src/components/json-viewer/json-viewer.css +33 -1
  84. package/src/components/nav/README.md +12 -10
  85. package/src/components/nav/element.ts +5 -5
  86. package/src/components/nav/engine.ts +21 -14
  87. package/src/components/nav/nav.css +102 -102
  88. package/src/components/nav/react.tsx +4 -4
  89. package/src/components/orbit-menu/README.md +46 -0
  90. package/src/components/orbit-menu/element.ts +27 -0
  91. package/src/components/orbit-menu/engine.ts +179 -0
  92. package/src/components/orbit-menu/orbit-menu.css +129 -0
  93. package/src/components/orbit-menu/react.tsx +44 -0
  94. package/src/components/pagination/README.md +1 -1
  95. package/src/components/pagination/engine.ts +50 -4
  96. package/src/components/pagination/pagination.css +10 -5
  97. package/src/components/pie-chart/README.md +48 -0
  98. package/src/components/pie-chart/element.ts +30 -0
  99. package/src/components/pie-chart/engine.ts +283 -0
  100. package/src/components/pie-chart/pie-chart.css +92 -0
  101. package/src/components/pie-chart/react.tsx +14 -0
  102. package/src/components/pill-button/README.md +46 -0
  103. package/src/components/pill-button/element.ts +16 -0
  104. package/src/components/pill-button/engine.ts +77 -0
  105. package/src/components/pill-button/pill-button.css +134 -0
  106. package/src/components/pill-button/react.tsx +32 -0
  107. package/src/components/radar-chart/README.md +48 -0
  108. package/src/components/radar-chart/element.ts +30 -0
  109. package/src/components/radar-chart/engine.ts +327 -0
  110. package/src/components/radar-chart/radar-chart.css +72 -0
  111. package/src/components/radar-chart/react.tsx +14 -0
  112. package/src/components/radial-chart/README.md +48 -0
  113. package/src/components/radial-chart/element.ts +30 -0
  114. package/src/components/radial-chart/engine.ts +290 -0
  115. package/src/components/radial-chart/radial-chart.css +100 -0
  116. package/src/components/radial-chart/react.tsx +14 -0
  117. package/src/components/sankey-chart/README.md +48 -0
  118. package/src/components/sankey-chart/element.ts +31 -0
  119. package/src/components/sankey-chart/engine.ts +401 -0
  120. package/src/components/sankey-chart/react.tsx +14 -0
  121. package/src/components/sankey-chart/sankey-chart.css +60 -0
  122. package/src/components/scroll-island/README.md +34 -0
  123. package/src/components/scroll-island/element.ts +16 -0
  124. package/src/components/scroll-island/engine.ts +252 -0
  125. package/src/components/scroll-island/react.tsx +26 -0
  126. package/src/components/scroll-island/scroll-island.css +110 -0
  127. package/src/components/select/README.md +34 -0
  128. package/src/components/select/element.ts +26 -0
  129. package/src/components/select/engine.ts +267 -0
  130. package/src/components/select/react.tsx +33 -0
  131. package/src/components/select/select.css +135 -0
  132. package/src/components/toolbar/README.md +46 -0
  133. package/src/components/toolbar/element.ts +30 -0
  134. package/src/components/toolbar/engine.ts +230 -0
  135. package/src/components/toolbar/react.tsx +34 -0
  136. package/src/components/toolbar/toolbar.css +121 -0
  137. package/src/components/tree-view/element.ts +1 -1
  138. package/src/components/tree-view/engine.ts +174 -0
  139. package/src/components/tree-view/react.tsx +7 -3
  140. package/src/components/tree-view/tree-view.css +79 -0
  141. package/src/components/voice-dots/README.md +48 -0
  142. package/src/components/voice-dots/element.ts +45 -0
  143. package/src/components/voice-dots/engine.ts +243 -0
  144. package/src/components/voice-dots/react.tsx +52 -0
  145. package/src/components/voice-dots/voice-dots.css +69 -0
  146. package/src/elements/index.ts +39 -0
  147. package/src/index.ts +16 -0
  148. package/src/motion/liquid.ts +14 -4
  149. package/src/react/index.ts +14 -1
  150. package/dist/chunk-6P4P7NU6.js +0 -1401
  151. package/dist/chunk-6P4P7NU6.js.map +0 -1
  152. package/dist/chunk-CU6SXW3E.js.map +0 -1
  153. package/dist/store-DjkTBX7f.d.ts +0 -195
  154. /package/dist/{chunk-EY4BJKC4.js.map → chunk-6IRJYXJU.js.map} +0 -0
@@ -207,3 +207,82 @@ mk-tree-view {
207
207
  animation: none;
208
208
  }
209
209
  }
210
+
211
+ /* Folder and page icons (icons option): the folder's front opens like a lid with its branch, or under a dragged card. */
212
+ :is([data-mk-tree-folder], [data-mk-tree-page]) {
213
+ display: inline-grid;
214
+ flex: none;
215
+ place-items: center;
216
+ width: 1.15rem;
217
+ height: 1.15rem;
218
+ }
219
+
220
+ :is([data-mk-tree-folder], [data-mk-tree-page]) svg {
221
+ width: 100%;
222
+ height: 100%;
223
+ overflow: visible;
224
+ }
225
+
226
+ [data-mk-tree-folder-back] {
227
+ fill: color-mix(in srgb, var(--mk-accent) 70%, var(--mk-surface-1));
228
+ }
229
+
230
+ [data-mk-tree-folder-front] {
231
+ fill: var(--mk-accent);
232
+ transform-box: fill-box;
233
+ transform-origin: 50% 100%;
234
+ transition: transform 0.35s cubic-bezier(0.38, 0.49, 0, 1.5);
235
+ }
236
+
237
+ [role='treeitem'][aria-expanded='true'] > [data-mk-tree-row] [data-mk-tree-folder-front],
238
+ [role='treeitem'][data-mk-tree-over] > [data-mk-tree-row] [data-mk-tree-folder-front] {
239
+ transform: skewX(-14deg) scaleY(0.78);
240
+ }
241
+
242
+ [data-mk-tree-sheet] {
243
+ fill: var(--mk-surface-3);
244
+ stroke: var(--mk-outline);
245
+ stroke-width: 2;
246
+ }
247
+
248
+ [data-mk-tree-fold] {
249
+ fill: var(--mk-outline);
250
+ }
251
+
252
+ /* Dragging: the source row fades, the branch under the card lights up, the card floats with a shadow. */
253
+ [role='treeitem'][data-mk-tree-dragging] > [data-mk-tree-row] {
254
+ opacity: 0.4;
255
+ }
256
+
257
+ [role='treeitem'][data-mk-tree-over] > [data-mk-tree-row] {
258
+ background: color-mix(in srgb, var(--mk-accent) 14%, transparent);
259
+ box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--mk-accent) 55%, transparent);
260
+ }
261
+
262
+ [data-mk-tree-drag] {
263
+ position: fixed;
264
+ z-index: 2147483646;
265
+ display: inline-flex;
266
+ align-items: center;
267
+ gap: 0.5rem;
268
+ max-width: 16rem;
269
+ padding: 0.45rem 0.75rem;
270
+ overflow: hidden;
271
+ border-radius: 10px;
272
+ background: var(--mk-surface-1);
273
+ box-shadow:
274
+ 0 0 0 1px var(--mk-outline-soft),
275
+ 0 14px 30px -10px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
276
+ color: var(--mk-text);
277
+ font-size: 0.875rem;
278
+ font-weight: 500;
279
+ white-space: nowrap;
280
+ text-overflow: ellipsis;
281
+ pointer-events: none;
282
+ }
283
+
284
+ @media (prefers-reduced-motion: reduce) {
285
+ [data-mk-tree-folder-front] {
286
+ transition: none;
287
+ }
288
+ }
@@ -0,0 +1,48 @@
1
+ # voice-dots
2
+
3
+ Puntos que respiran, escuchan, piensan y hablan; pueden seguir el nivel real de una fuente de audio.
4
+
5
+ ## Instalación
6
+
7
+ ```tsx
8
+ import '@yewdiel21/motion-kit/css';
9
+ import { VoiceDots } from '@yewdiel21/motion-kit/react';
10
+ ```
11
+
12
+ ```sh
13
+ npx @yewdiel21/motion-kit add voice-dots --react --elements
14
+ ```
15
+
16
+ ## React
17
+
18
+ ```tsx
19
+ <VoiceDots state="listening" source={stream} level={level} />
20
+ ```
21
+
22
+ Los callbacks del ejemplo (`ask`, `run`, `setTool`, `setVolume`) pertenecen a tu aplicación. React acepta `className`, `style` y atributos HTML. Los componentes con estado exponen `instanceRef` (excepto PillButton).
23
+
24
+ ## HTML / Astro
25
+
26
+ ```html
27
+ <mk-voice-dots state="listening" size="12"></mk-voice-dots>
28
+ <script type="module">
29
+ import { defineElements } from '@yewdiel21/motion-kit/elements';
30
+ defineElements(['mk-voice-dots']);
31
+ </script>
32
+ ```
33
+
34
+ En Astro usa un `<script>` procesado por Astro. En HTML directo el import requiere bundler o import map. También puedes usar `import { createVoiceDots } from '@yewdiel21/motion-kit'` y llamar a la factory con el contenedor y las mismas opciones. Al desmontar, llama a `instance.destroy()`.
35
+
36
+ ## Opciones
37
+
38
+ state: idle, listening, thinking o speaking. count: 4 (3 a 6). size: 12 px. colorful: false. labels: textos de estado. En React source acepta MediaStream o HTMLMediaElement; level acepta 0…1 o un array. Instancia: setState(), setLevel(), listen(source), stop(), state.
39
+
40
+ El componente no solicita acceso al micrófono por sí solo. Obtén el stream tras una acción del usuario con `navigator.mediaDevices.getUserMedia({audio:true})`. `await instance.listen(stream)` devuelve una función para desconectar el análisis; la aplicación debe detener sus tracks al terminar. La fuente se analiza localmente. `stop()` desconecta la fuente. Sin audio, listening acepta `setLevel()` y speaking usa una onda de demostración.
41
+
42
+ Los nombres camelCase se escriben como atributos kebab-case. Los arrays y objetos se pasan como JSON. Los booleanos aceptan atributo vacío o `"false"`.
43
+
44
+ ## Estilos y accesibilidad
45
+
46
+ Variables: --mk-voice-size; colores semánticos del tema..
47
+
48
+ Respeta los colores del tema, foco visible y `prefers-reduced-motion`. Los gráficos incluyen una tabla accesible y selección con flechas, Home, End y Escape. Heatmap usa arriba/abajo para un día e izquierda/derecha para una semana. Toolbar usa flechas y Home/End; OrbitMenu cierra con Escape y devuelve el foco. Los controles conservan nombres accesibles; VoiceDots anuncia los estados sin leer cada fotograma.
@@ -0,0 +1,45 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createVoiceDots, type VoiceDotsInstance, type VoiceState } from './engine';
3
+
4
+ /**
5
+ * <mk-voice-dots class="mk-voice-dots" state="listening"></mk-voice-dots>
6
+ * Dots that move with a voice. element.listen(stream) follows a microphone; emits mk:state ({state}).
7
+ */
8
+ export class MkVoiceDotsElement extends MkElement<VoiceDotsInstance> {
9
+ static options: Record<string, OptionType> = { state: 'string', count: 'number', colorful: 'boolean', size: 'number' };
10
+
11
+ protected create(options: Record<string, unknown>) {
12
+ return createVoiceDots(this, options);
13
+ }
14
+
15
+ /** A new state flows from where the dots are (the engine, and the audio it follows, stay). */
16
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
17
+ if (name === 'state' && this.instance) {
18
+ this.instance.setState((next ?? 'idle') as VoiceState);
19
+ return;
20
+ }
21
+ super.attributeChangedCallback(name, previous, next);
22
+ }
23
+
24
+ get state() {
25
+ return this.instance?.state ?? 'idle';
26
+ }
27
+
28
+ set state(next: VoiceState) {
29
+ this.setAttribute('state', next);
30
+ }
31
+
32
+ setLevel(level: number | number[]) {
33
+ this.instance?.setLevel(level);
34
+ }
35
+
36
+ listen(source: MediaStream | HTMLMediaElement) {
37
+ return this.instance?.listen(source) ?? Promise.resolve(() => {});
38
+ }
39
+
40
+ stop() {
41
+ this.instance?.stop();
42
+ }
43
+ }
44
+
45
+ export const defineVoiceDots = () => defineElement('mk-voice-dots', MkVoiceDotsElement);
@@ -0,0 +1,243 @@
1
+ import { registerMotion } from '../../core/gsap';
2
+ import { whileVisible } from '../../core/loop';
3
+ import { reduced } from '../../core/reduced';
4
+ import { createScope } from '../../core/scope';
5
+ import { inert, type Instance } from '../../core/types';
6
+
7
+ /*
8
+ * Voice dots: four dots that show a voice conversation, like an assistant's.
9
+ * Idle, they breathe one after another. Listening, each dot stretches into a
10
+ * pill with its band of the voice (from a microphone or a playing element
11
+ * through an AnalyserNode, or levels the app gives), springing up fast and
12
+ * settling slower so syllables read as a bounce. Thinking, they gather into a
13
+ * ring and orbit. Speaking, they ride a travelling wave (with the playing
14
+ * audio's level when there is one). Every change of state flows from where the
15
+ * dots are, so nothing jumps; the frame loop only runs while the dots are on
16
+ * screen. Monochrome (the text colour) by default, or the chart colours. A
17
+ * status line says the state to assistive tech; with reduced motion the dots
18
+ * stay still and only their colour and the status change.
19
+ */
20
+
21
+ export type VoiceState = 'idle' | 'listening' | 'thinking' | 'speaking';
22
+
23
+ export interface VoiceDotsOptions {
24
+ /** Default 'idle'. */
25
+ state?: VoiceState;
26
+ /** Dots (default 4, 3…6). */
27
+ count?: number;
28
+ /** The chart colours instead of the text colour. */
29
+ colorful?: boolean;
30
+ /** Dot size in px (default 12). */
31
+ size?: number;
32
+ /** The status text for each state. */
33
+ labels?: Partial<Record<VoiceState, string>>;
34
+ }
35
+
36
+ export interface VoiceDotsInstance extends Instance {
37
+ setState(state: VoiceState): void;
38
+ readonly state: VoiceState;
39
+ /** The voice's level, 0…1: one for all dots or one per dot (from your own analysis). */
40
+ setLevel(level: number | number[]): void;
41
+ /** Follow a microphone stream or a playing <audio>/<video>; resolves to a function that stops following. */
42
+ listen(source: MediaStream | HTMLMediaElement): Promise<() => void>;
43
+ /** Stop following the audio (the state stays). */
44
+ stop(): void;
45
+ }
46
+
47
+ const LABELS: Record<VoiceState, string> = { idle: 'Asistente en espera', listening: 'Escuchando…', thinking: 'Pensando…', speaking: 'Hablando…' };
48
+ const STATES: VoiceState[] = ['idle', 'listening', 'thinking', 'speaking'];
49
+ /** One AudioContext source per media element: the platform allows only one. */
50
+ const mediaSources = new WeakMap<HTMLMediaElement, MediaElementAudioSourceNode>();
51
+ let audio: AudioContext | null = null;
52
+
53
+ export function createVoiceDots(root: HTMLElement | null, options: VoiceDotsOptions = {}): VoiceDotsInstance {
54
+ if (!root) return inert({ setState() {}, state: 'idle' as VoiceState, setLevel() {}, listen: async () => () => {}, stop() {} });
55
+ registerMotion();
56
+ const scope = createScope();
57
+ const count = Math.max(3, Math.min(6, Math.round(options.count ?? 4)));
58
+ const size = Math.max(4, options.size ?? 12);
59
+ const labels = { ...LABELS, ...options.labels };
60
+ let state: VoiceState = STATES.includes(options.state as VoiceState) ? options.state! : 'idle';
61
+
62
+ const row = document.createElement('span');
63
+ row.dataset.mkVoiceRow = '';
64
+ row.setAttribute('aria-hidden', 'true');
65
+ const dots = Array.from({ length: count }, (_, i) => {
66
+ const dot = document.createElement('span');
67
+ dot.dataset.mkVoiceDot = String(i + 1);
68
+ row.append(dot);
69
+ return dot;
70
+ });
71
+ const status = document.createElement('span');
72
+ status.className = 'mk-sr-only';
73
+ status.setAttribute('role', 'status');
74
+ root.append(row, status);
75
+ scope.add(() => {
76
+ row.remove();
77
+ status.remove();
78
+ });
79
+ scope.attr(root, 'data-mk-ready', '');
80
+ scope.attr(root, 'data-mk-state', state);
81
+ if (options.colorful) scope.attr(root, 'data-mk-colorful', '');
82
+ root.style.setProperty('--mk-voice-size', `${size}px`);
83
+ scope.add(() => {
84
+ root.style.removeProperty('--mk-voice-size');
85
+ if (!root.getAttribute('style')) root.removeAttribute('style');
86
+ });
87
+
88
+ // Per dot: what is drawn (height, x, y) and the level it follows.
89
+ const drawn = dots.map(() => ({ h: size, x: 0, y: 0, vh: 0 }));
90
+ const levels = dots.map(() => 0);
91
+ let time = 0;
92
+ const gap = size * 0.9;
93
+ const home = (i: number) => (i - (count - 1) / 2) * (size + gap);
94
+
95
+ const draw = () => {
96
+ dots.forEach((dot, i) => {
97
+ const d = drawn[i]!;
98
+ dot.style.height = `${d.h.toFixed(2)}px`;
99
+ dot.style.transform = `translate(${d.x.toFixed(2)}px, ${d.y.toFixed(2)}px)`;
100
+ });
101
+ };
102
+ const still = () => {
103
+ dots.forEach((_, i) => Object.assign(drawn[i]!, { h: size, x: 0, y: 0, vh: 0 }));
104
+ draw();
105
+ };
106
+
107
+ let analyser: AnalyserNode | null = null;
108
+ let bins: Uint8Array<ArrayBuffer> | null = null;
109
+ let release: (() => void) | null = null;
110
+ let listening = 0;
111
+ /** The voice's bands (about 120 Hz – 3 kHz) into one level per dot. */
112
+ const sample = () => {
113
+ if (!analyser || !bins) return;
114
+ analyser.getByteFrequencyData(bins);
115
+ const hz = (audio?.sampleRate ?? 48000) / 2 / bins.length;
116
+ const low = Math.max(1, Math.floor(120 / hz));
117
+ const high = Math.min(bins.length - 1, Math.ceil(3000 / hz));
118
+ const span = (high - low) / count;
119
+ for (let i = 0; i < count; i++) {
120
+ let sum = 0;
121
+ const from = Math.floor(low + span * i);
122
+ const to = Math.max(from + 1, Math.floor(low + span * (i + 1)));
123
+ for (let k = from; k < to; k++) sum += bins[k]!;
124
+ // Higher bands are quieter in a voice: they get a lift.
125
+ levels[i] = Math.min(1, (sum / (to - from) / 255) * (1.4 + i * 0.35));
126
+ }
127
+ };
128
+
129
+ const tick = (dt: number) => {
130
+ if (reduced()) return;
131
+ time += dt / 60;
132
+ sample();
133
+ const k = 1 - Math.exp(-dt * 0.35);
134
+ dots.forEach((_, i) => {
135
+ const d = drawn[i]!;
136
+ let h = size;
137
+ let x = 0;
138
+ let y = 0;
139
+ if (state === 'idle') {
140
+ h = size * (1 + 0.18 * Math.max(0, Math.sin(time * 3.2 - i * 0.7)));
141
+ y = -2 * Math.max(0, Math.sin(time * 3.2 - i * 0.7));
142
+ } else if (state === 'listening') {
143
+ h = size + levels[i]! * size * 3.2;
144
+ } else if (state === 'thinking') {
145
+ // A ring around the middle, turning.
146
+ const angle = time * 4 + (i / count) * Math.PI * 2;
147
+ const ring = size * 1.2;
148
+ x = Math.cos(angle) * ring - home(i);
149
+ y = Math.sin(angle) * ring;
150
+ } else {
151
+ const wave = 0.5 + 0.5 * Math.sin(time * 9 - i * 0.9);
152
+ const voice = analyser ? levels[i]! : 0.55;
153
+ h = size + wave * voice * size * 2.4;
154
+ }
155
+ // Height on a spring that rises fast and falls slower (a bounce per syllable); position eases.
156
+ const stiffness = h > d.h ? 0.5 : 0.18;
157
+ d.vh = (d.vh + (h - d.h) * stiffness) * 0.6;
158
+ d.h = Math.max(size * 0.8, d.h + d.vh * dt);
159
+ d.x += (x - d.x) * k;
160
+ d.y += (y - d.y) * k;
161
+ });
162
+ draw();
163
+ };
164
+ const stopLoop = whileVisible(root, tick);
165
+ scope.add(stopLoop);
166
+
167
+ const stop = () => {
168
+ listening++;
169
+ release?.();
170
+ release = null;
171
+ analyser = null;
172
+ bins = null;
173
+ levels.fill(0);
174
+ };
175
+ scope.add(stop);
176
+
177
+ const setState = (next: VoiceState) => {
178
+ if (!STATES.includes(next)) return;
179
+ state = next;
180
+ root.setAttribute('data-mk-state', next);
181
+ status.textContent = next === 'idle' ? '' : labels[next];
182
+ if (reduced()) still();
183
+ root.dispatchEvent(new CustomEvent('mk:state', { bubbles: true, detail: { state: next } }));
184
+ };
185
+
186
+ draw();
187
+ if (state !== 'idle') status.textContent = labels[state];
188
+
189
+ return {
190
+ setState,
191
+ get state() {
192
+ return state;
193
+ },
194
+ setLevel(level) {
195
+ const list = Array.isArray(level) ? level : dots.map(() => level);
196
+ dots.forEach((_, i) => {
197
+ const value = Number(list[i % Math.max(1, list.length)]);
198
+ levels[i] = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
199
+ });
200
+ },
201
+ async listen(source) {
202
+ stop();
203
+ const request = listening;
204
+ const Context = globalThis.AudioContext ?? (globalThis as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
205
+ if (!Context || scope.disposed) return () => {};
206
+ audio ??= new Context();
207
+ if (audio.state === 'suspended') await audio.resume().catch(() => {});
208
+ if (scope.disposed || request !== listening) return () => {};
209
+ const node = audio.createAnalyser();
210
+ node.fftSize = 256;
211
+ node.smoothingTimeConstant = 0.55;
212
+ let input: AudioNode;
213
+ if (source instanceof HTMLMediaElement) {
214
+ const existing = mediaSources.get(source) ?? audio.createMediaElementSource(source);
215
+ mediaSources.set(source, existing);
216
+ // Followed before: its direct line to the speakers goes, the analyser's takes over.
217
+ existing.disconnect();
218
+ input = existing;
219
+ // A playing element must still be heard.
220
+ node.connect(audio.destination);
221
+ } else input = audio.createMediaStreamSource(source);
222
+ input.connect(node);
223
+ analyser = node;
224
+ bins = new Uint8Array(new ArrayBuffer(node.frequencyBinCount));
225
+ const mine = () => {
226
+ try {
227
+ input.disconnect(node);
228
+ node.disconnect();
229
+ } catch {
230
+ // Already disconnected.
231
+ }
232
+ // The element keeps playing through the speakers.
233
+ if (source instanceof HTMLMediaElement && audio) input.connect(audio.destination);
234
+ };
235
+ release = mine;
236
+ return () => {
237
+ if (release === mine) stop();
238
+ };
239
+ },
240
+ stop,
241
+ destroy: () => scope.dispose(),
242
+ };
243
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
3
+ import { useIsomorphicLayoutEffect } from '../../react/use-recipe';
4
+ import { createVoiceDots, type VoiceDotsInstance, type VoiceDotsOptions } from './engine';
5
+
6
+ export interface VoiceDotsProps extends HTMLAttributes<HTMLDivElement>, VoiceDotsOptions {
7
+ /** A microphone stream or a playing <audio>/<video> to follow. */
8
+ source?: MediaStream | HTMLMediaElement | null;
9
+ /** The voice's level, 0…1 (one or one per dot), when the app measures it. */
10
+ level?: number | number[];
11
+ instanceRef?: MutableRefObject<VoiceDotsInstance | null>;
12
+ }
13
+
14
+ /** <VoiceDots state={listening ? 'listening' : 'idle'} source={micStream} /> */
15
+ export function VoiceDots({ state = 'idle', count, colorful, size, labels, source, level, instanceRef, className, ...props }: VoiceDotsProps) {
16
+ const ref = useRef<HTMLDivElement>(null);
17
+ const instance = useRef<VoiceDotsInstance | null>(null);
18
+ useIsomorphicLayoutEffect(() => {
19
+ const created = createVoiceDots(ref.current, { state, count, colorful, size, labels });
20
+ instance.current = created;
21
+ if (instanceRef) instanceRef.current = created;
22
+ return () => {
23
+ created.destroy();
24
+ instance.current = null;
25
+ if (instanceRef) instanceRef.current = null;
26
+ };
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ }, [count, colorful, size]);
29
+ useIsomorphicLayoutEffect(() => {
30
+ if (instance.current && instance.current.state !== state) instance.current.setState(state);
31
+ }, [state]);
32
+ useIsomorphicLayoutEffect(() => {
33
+ const dots = instance.current;
34
+ if (!dots || !source) return;
35
+ let stop: (() => void) | null = null;
36
+ let gone = false;
37
+ void dots.listen(source).then((done) => {
38
+ if (gone) done();
39
+ else stop = done;
40
+ }).catch((error: unknown) => {
41
+ if (!gone) ref.current?.dispatchEvent(new CustomEvent('mk:error', { bubbles: true, detail: { error } }));
42
+ });
43
+ return () => {
44
+ gone = true;
45
+ stop?.();
46
+ };
47
+ }, [source, count, colorful, size]);
48
+ useIsomorphicLayoutEffect(() => {
49
+ if (level !== undefined) instance.current?.setLevel(level);
50
+ }, [level]);
51
+ return <div {...props} ref={ref} className={['mk-voice-dots', className].filter(Boolean).join(' ')} />;
52
+ }
@@ -0,0 +1,69 @@
1
+ /* voice-dots — round dots in the text colour (or the chart colours) that stretch into pills with the voice.
2
+ The row is tall enough for the tallest pill, so the layout around it never moves. */
3
+ .mk-voice-dots,
4
+ mk-voice-dots {
5
+ --mk-voice-size: 12px;
6
+ --mk-voice-dot: var(--mk-text);
7
+ display: inline-grid;
8
+ place-items: center;
9
+ min-width: calc(var(--mk-voice-size) * 8);
10
+ height: calc(var(--mk-voice-size) * 4.6);
11
+ }
12
+
13
+ [data-mk-voice-row] {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ gap: calc(var(--mk-voice-size) * 0.9);
17
+ height: 100%;
18
+ }
19
+
20
+ [data-mk-voice-dot] {
21
+ display: block;
22
+ width: var(--mk-voice-size);
23
+ height: var(--mk-voice-size);
24
+ border-radius: var(--mk-radius-full);
25
+ background: var(--mk-voice-dot);
26
+ transition:
27
+ background-color 0.3s ease,
28
+ opacity 0.3s ease;
29
+ will-change: transform, height;
30
+ }
31
+
32
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-state='idle'] [data-mk-voice-dot] {
33
+ opacity: 0.55;
34
+ }
35
+
36
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-colorful] [data-mk-voice-dot='1'] {
37
+ --mk-voice-dot: var(--mk-chart-1, #2a78d6);
38
+ }
39
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-colorful] [data-mk-voice-dot='2'] {
40
+ --mk-voice-dot: var(--mk-chart-8, #e34948);
41
+ }
42
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-colorful] [data-mk-voice-dot='3'] {
43
+ --mk-voice-dot: var(--mk-chart-4, #eda100);
44
+ }
45
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-colorful] [data-mk-voice-dot='4'] {
46
+ --mk-voice-dot: var(--mk-chart-3, #1baf7a);
47
+ }
48
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-colorful] [data-mk-voice-dot='5'] {
49
+ --mk-voice-dot: var(--mk-chart-7, #6250d6);
50
+ }
51
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-colorful] [data-mk-voice-dot='6'] {
52
+ --mk-voice-dot: var(--mk-chart-2, #eb6834);
53
+ }
54
+
55
+ /* Without motion the state shows in the dots' colour. */
56
+ @media (prefers-reduced-motion: reduce) {
57
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-state='listening'] [data-mk-voice-dot] {
58
+ --mk-voice-dot: var(--mk-accent);
59
+ }
60
+ :is(.mk-voice-dots, mk-voice-dots)[data-mk-state='thinking'] [data-mk-voice-dot] {
61
+ opacity: 0.75;
62
+ }
63
+ }
64
+
65
+ @media (forced-colors: active) {
66
+ [data-mk-voice-dot] {
67
+ background: CanvasText;
68
+ }
69
+ }
@@ -66,6 +66,19 @@ import { MkSplitViewElement } from '../components/split-view/element';
66
66
  import { MkDataTableElement } from '../components/data-table/element';
67
67
  import { MkEditableTableElement } from '../components/editable-table/element';
68
68
  import { MkRichTextEditorElement } from '../components/rich-text-editor/element';
69
+ import { MkSelectElement } from '../components/select/element';
70
+ import { MkAddToCartElement } from '../components/add-to-cart/element';
71
+ import { MkScrollIslandElement } from '../components/scroll-island/element';
72
+ import { MkPillButtonElement } from '../components/pill-button/element';
73
+ import { MkToolbarElement } from '../components/toolbar/element';
74
+ import { MkBidirectionalSliderElement } from '../components/bidirectional-slider/element';
75
+ import { MkVoiceDotsElement } from '../components/voice-dots/element';
76
+ import { MkPieChartElement } from '../components/pie-chart/element';
77
+ import { MkRadarChartElement } from '../components/radar-chart/element';
78
+ import { MkRadialChartElement } from '../components/radial-chart/element';
79
+ import { MkSankeyChartElement } from '../components/sankey-chart/element';
80
+ import { MkHeatmapChartElement } from '../components/heatmap-chart/element';
81
+ import { MkOrbitMenuElement } from '../components/orbit-menu/element';
69
82
  // new-component:imports (scripts/new-component.mjs inserts above this line)
70
83
 
71
84
  export { MkElement, SafeHTMLElement, defineElement, type OptionType } from './base';
@@ -137,6 +150,19 @@ export { MkSplitViewElement } from '../components/split-view/element';
137
150
  export { MkDataTableElement } from '../components/data-table/element';
138
151
  export { MkEditableTableElement } from '../components/editable-table/element';
139
152
  export { MkRichTextEditorElement } from '../components/rich-text-editor/element';
153
+ export { MkSelectElement } from '../components/select/element';
154
+ export { MkAddToCartElement } from '../components/add-to-cart/element';
155
+ export { MkScrollIslandElement } from '../components/scroll-island/element';
156
+ export { MkPillButtonElement } from '../components/pill-button/element';
157
+ export { MkToolbarElement } from '../components/toolbar/element';
158
+ export { MkBidirectionalSliderElement } from '../components/bidirectional-slider/element';
159
+ export { MkVoiceDotsElement } from '../components/voice-dots/element';
160
+ export { MkPieChartElement } from '../components/pie-chart/element';
161
+ export { MkRadarChartElement } from '../components/radar-chart/element';
162
+ export { MkRadialChartElement } from '../components/radial-chart/element';
163
+ export { MkSankeyChartElement } from '../components/sankey-chart/element';
164
+ export { MkHeatmapChartElement } from '../components/heatmap-chart/element';
165
+ export { MkOrbitMenuElement } from '../components/orbit-menu/element';
140
166
  // new-component:exports
141
167
 
142
168
  /** Tag name → element class, for every component. */
@@ -208,6 +234,19 @@ export const ELEMENTS = {
208
234
  'mk-data-table': MkDataTableElement,
209
235
  'mk-editable-table': MkEditableTableElement,
210
236
  'mk-rich-text-editor': MkRichTextEditorElement,
237
+ 'mk-select': MkSelectElement,
238
+ 'mk-add-to-cart': MkAddToCartElement,
239
+ 'mk-scroll-island': MkScrollIslandElement,
240
+ 'mk-pill-button': MkPillButtonElement,
241
+ 'mk-toolbar': MkToolbarElement,
242
+ 'mk-bidirectional-slider': MkBidirectionalSliderElement,
243
+ 'mk-voice-dots': MkVoiceDotsElement,
244
+ 'mk-pie-chart': MkPieChartElement,
245
+ 'mk-radar-chart': MkRadarChartElement,
246
+ 'mk-radial-chart': MkRadialChartElement,
247
+ 'mk-sankey-chart': MkSankeyChartElement,
248
+ 'mk-heatmap-chart': MkHeatmapChartElement,
249
+ 'mk-orbit-menu': MkOrbitMenuElement,
211
250
  // new-component:entries
212
251
  } as const;
213
252
 
package/src/index.ts CHANGED
@@ -153,9 +153,12 @@ export {
153
153
  createNav,
154
154
  revealShell,
155
155
  NAV_STYLES,
156
+ NAV_STYLE_ALIASES,
157
+ toNavStyle,
156
158
  type NavOptions,
157
159
  type NavInstance,
158
160
  type NavStyle,
161
+ type NavStyleAlias,
159
162
  type NavigateDetail,
160
163
  } from './components/nav/engine';
161
164
  export { createSteps, type StepsOptions, type StepsInstance, type StepDetail } from './components/steps/engine';
@@ -272,3 +275,16 @@ export {
272
275
  } from './components/data-table/engine';
273
276
  export { createEditableTable, type EditableTableOptions, type EditableTableInstance, type EditableColumn, type RowChange, type EditorKind } from './components/editable-table/engine';
274
277
  export { createRichTextEditor, sanitizeHtml, type RichTextEditorOptions, type RichTextEditorInstance, type RichCommand } from './components/rich-text-editor/engine';
278
+ export { createSelect, type SelectOptions, type SelectInstance, type SelectOption } from './components/select/engine';
279
+ export { createAddToCart, type AddToCartOptions, type AddToCartInstance } from './components/add-to-cart/engine';
280
+ export { createScrollIsland, type ScrollIslandOptions, type ScrollIslandInstance } from './components/scroll-island/engine';
281
+ export { createPillButton, type PillButtonOptions, type PillButtonInstance } from './components/pill-button/engine';
282
+ export { createToolbar, type ToolbarOptions, type ToolbarInstance } from './components/toolbar/engine';
283
+ export { createBidirectionalSlider, type BidirectionalSliderOptions, type BidirectionalSliderInstance } from './components/bidirectional-slider/engine';
284
+ export { createVoiceDots, type VoiceDotsOptions, type VoiceDotsInstance, type VoiceState } from './components/voice-dots/engine';
285
+ export { createPieChart, type PieChartOptions, type PieChartInstance, type PieSlice } from './components/pie-chart/engine';
286
+ export { createRadarChart, type RadarChartOptions, type RadarChartInstance, type RadarSeries } from './components/radar-chart/engine';
287
+ export { createRadialChart, type RadialChartOptions, type RadialChartInstance, type RadialDatum } from './components/radial-chart/engine';
288
+ export { createSankeyChart, type SankeyChartOptions, type SankeyChartInstance, type SankeyNode, type SankeyLink } from './components/sankey-chart/engine';
289
+ export { createHeatmapChart, type HeatmapChartOptions, type HeatmapChartInstance, type HeatmapDatum } from './components/heatmap-chart/engine';
290
+ export { createOrbitMenu, type OrbitMenuOptions, type OrbitMenuInstance } from './components/orbit-menu/engine';