@juspay/svelte-ui-components 4.24.0 → 4.26.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/dist/MarkdownText/markdown.js +82 -9
- package/dist/MarkdownText/properties.d.ts +46 -2
- package/dist/SpeechSynthesis/controller.svelte.d.ts +89 -0
- package/dist/SpeechSynthesis/controller.svelte.js +385 -0
- package/dist/SpeechSynthesis/types.d.ts +68 -0
- package/dist/SpeechSynthesis/types.js +1 -0
- package/dist/VoiceOrb/VoiceOrb.svelte +400 -0
- package/dist/VoiceOrb/VoiceOrb.svelte.d.ts +4 -0
- package/dist/VoiceOrb/orbMath.d.ts +131 -0
- package/dist/VoiceOrb/orbMath.js +267 -0
- package/dist/VoiceOrb/properties.d.ts +105 -0
- package/dist/VoiceOrb/properties.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist-wc/index.js +548 -173
- package/package.json +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deterministic half of `VoiceOrb`, extracted so it can be tested.
|
|
3
|
+
*
|
|
4
|
+
* This repo's vitest suite runs no canvas — `getContext` is unimplemented in
|
|
5
|
+
* jsdom — so anything left inside the component is unreachable by a test. The
|
|
6
|
+
* geometry, the seeding and the listening envelope are all pure, so they live
|
|
7
|
+
* here and the component keeps only the drawing. Same split as
|
|
8
|
+
* `Pill/pillTone.ts` and `Table/normalizeColumns.ts`.
|
|
9
|
+
*/
|
|
10
|
+
const SIZE_RANDOMNESS = 0.95;
|
|
11
|
+
const BASE_PARTICLE_SIZE = 1.6;
|
|
12
|
+
export const LISTENING_CYCLE_MS = 3000;
|
|
13
|
+
export const LISTENING_SPEED_RANGE = { min: 0.8, max: 1.2 };
|
|
14
|
+
export const LISTENING_SCALE_RANGE = { min: 0.9, max: 1 };
|
|
15
|
+
/**
|
|
16
|
+
* FNV-1a. A string seed has to produce the same number on every mount, or the
|
|
17
|
+
* orb a consumer keyed to a session id would look different on each navigation
|
|
18
|
+
* — which is the whole reason the prop exists.
|
|
19
|
+
*/
|
|
20
|
+
export const hashSeed = (value) => {
|
|
21
|
+
let hash = 2166136261 >>> 0;
|
|
22
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
23
|
+
hash ^= value.charCodeAt(index);
|
|
24
|
+
hash = Math.imul(hash, 16777619);
|
|
25
|
+
}
|
|
26
|
+
return hash >>> 0;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Turns an explicit `seed` prop into the integer `mulberry32` wants, or null
|
|
30
|
+
* when there is nothing explicit to resolve -- the caller then falls back to
|
|
31
|
+
* its own stable per-mount random value rather than treating null as zero.
|
|
32
|
+
*
|
|
33
|
+
* A numeric-looking string is parsed back into the number it names rather
|
|
34
|
+
* than hashed. Every custom-element attribute arrives as a string -- HTML has
|
|
35
|
+
* no way to write a "number" attribute -- so without this, the web
|
|
36
|
+
* component's `<sui-voice-orb seed="42">` would hash `"42"` while the Svelte
|
|
37
|
+
* `seed={42}` it is meant to be the markup spelling of used the number
|
|
38
|
+
* directly, and the same intended seed would paint two different orbs
|
|
39
|
+
* depending on which API reached it. A non-numeric string (a session id)
|
|
40
|
+
* still hashes, unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export const resolveExplicitSeed = (seed) => {
|
|
43
|
+
if (typeof seed === 'number' && Number.isFinite(seed)) {
|
|
44
|
+
return seed >>> 0;
|
|
45
|
+
}
|
|
46
|
+
if (typeof seed !== 'string' || seed.trim().length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const numeric = Number(seed);
|
|
50
|
+
return Number.isFinite(numeric) ? numeric >>> 0 : hashSeed(seed);
|
|
51
|
+
};
|
|
52
|
+
/** Mulberry32: small, fast, and reproducible from one integer. */
|
|
53
|
+
export const mulberry32 = (initial) => {
|
|
54
|
+
let state = initial | 0;
|
|
55
|
+
return () => {
|
|
56
|
+
state = (state + 0x6d2b79f5) | 0;
|
|
57
|
+
let t = state;
|
|
58
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
59
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
60
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Points spread evenly over a sphere.
|
|
65
|
+
*
|
|
66
|
+
* `cos(phi)` is taken uniformly in [-1, 1] rather than `phi` itself: sampling
|
|
67
|
+
* the angle uniformly packs points towards the poles, because the rings of
|
|
68
|
+
* constant `phi` near a pole have far less circumference than those at the
|
|
69
|
+
* equator. The visible symptom is two dense caps and a sparse middle.
|
|
70
|
+
*/
|
|
71
|
+
export const generateOrbParticles = (count, radius, random) => {
|
|
72
|
+
const safeCount = Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
|
|
73
|
+
const safeRadius = Number.isFinite(radius) && radius > 0 ? radius : 140;
|
|
74
|
+
const minMultiplier = 1 - SIZE_RANDOMNESS * 0.7;
|
|
75
|
+
const maxMultiplier = 1 + SIZE_RANDOMNESS * 0.2;
|
|
76
|
+
const particles = [];
|
|
77
|
+
for (let index = 0; index < safeCount; index += 1) {
|
|
78
|
+
const theta = random() * Math.PI * 2;
|
|
79
|
+
const phi = Math.acos(Math.max(-1, Math.min(1, 2 * random() - 1)));
|
|
80
|
+
const sinPhi = Math.sin(phi);
|
|
81
|
+
particles.push({
|
|
82
|
+
baseX: safeRadius * sinPhi * Math.cos(theta),
|
|
83
|
+
baseY: safeRadius * sinPhi * Math.sin(theta),
|
|
84
|
+
baseZ: safeRadius * Math.cos(phi),
|
|
85
|
+
size: BASE_PARTICLE_SIZE * (minMultiplier + random() * (maxMultiplier - minMultiplier))
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return particles;
|
|
89
|
+
};
|
|
90
|
+
/** Rotates about X then Y, which is the order the projection assumes. */
|
|
91
|
+
export const rotateOrbPoint = (point, angleX, angleY) => {
|
|
92
|
+
const cosX = Math.cos(angleX);
|
|
93
|
+
const sinX = Math.sin(angleX);
|
|
94
|
+
const y = point.y * cosX - point.z * sinX;
|
|
95
|
+
const z = point.y * sinX + point.z * cosX;
|
|
96
|
+
const cosY = Math.cos(angleY);
|
|
97
|
+
const sinY = Math.sin(angleY);
|
|
98
|
+
return {
|
|
99
|
+
x: z * sinY + point.x * cosY,
|
|
100
|
+
y,
|
|
101
|
+
z: z * cosY - point.x * sinY
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Perspective depth for a point: 1 at the sphere's centre plane, larger as it
|
|
106
|
+
* comes towards the viewer. Used for both scale and opacity, so a particle
|
|
107
|
+
* behind the sphere reads as further away.
|
|
108
|
+
*/
|
|
109
|
+
export const orbDepthScale = (z, focalLength) => focalLength / (focalLength + z);
|
|
110
|
+
/** The 0..1 breathing envelope, one full cycle per `LISTENING_CYCLE_MS`. */
|
|
111
|
+
export const listeningWave = (elapsedMs) => {
|
|
112
|
+
const normalized = (elapsedMs % LISTENING_CYCLE_MS) / LISTENING_CYCLE_MS;
|
|
113
|
+
return (Math.sin(normalized * Math.PI * 2) + 1) / 2;
|
|
114
|
+
};
|
|
115
|
+
export const listeningSpeed = (elapsedMs, speedMultiplier) => {
|
|
116
|
+
const base = Number.isFinite(speedMultiplier) ? speedMultiplier : 1;
|
|
117
|
+
const wave = listeningWave(elapsedMs);
|
|
118
|
+
return (base *
|
|
119
|
+
(LISTENING_SPEED_RANGE.min + wave * (LISTENING_SPEED_RANGE.max - LISTENING_SPEED_RANGE.min)));
|
|
120
|
+
};
|
|
121
|
+
export const listeningScale = (elapsedMs) => {
|
|
122
|
+
const wave = listeningWave(elapsedMs);
|
|
123
|
+
return LISTENING_SCALE_RANGE.min + wave * (LISTENING_SCALE_RANGE.max - LISTENING_SCALE_RANGE.min);
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Parses the two colour shapes a browser hands back: `getComputedStyle().color`
|
|
127
|
+
* is always `rgb()`/`rgba()`, and a canvas `fillStyle` read back after a write
|
|
128
|
+
* is always `#rrggbb` (or `rgba()` when it carries alpha). Between them those
|
|
129
|
+
* cover every CSS colour syntax, because the canvas does the hard parsing --
|
|
130
|
+
* `oklch()`, `color(display-p3 ...)` and named colours all normalise through it.
|
|
131
|
+
*
|
|
132
|
+
* The library's own `hexToRgb` handles only hex, which is why this exists
|
|
133
|
+
* alongside it rather than replacing it.
|
|
134
|
+
*/
|
|
135
|
+
export const parseCssColor = (value) => {
|
|
136
|
+
const trimmed = value.trim();
|
|
137
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(trimmed);
|
|
138
|
+
if (hex !== null) {
|
|
139
|
+
const digits = hex[1];
|
|
140
|
+
const expanded = digits.length === 3
|
|
141
|
+
? `${digits[0]}${digits[0]}${digits[1]}${digits[1]}${digits[2]}${digits[2]}`
|
|
142
|
+
: digits;
|
|
143
|
+
return {
|
|
144
|
+
r: Number.parseInt(expanded.slice(0, 2), 16),
|
|
145
|
+
g: Number.parseInt(expanded.slice(2, 4), 16),
|
|
146
|
+
b: Number.parseInt(expanded.slice(4, 6), 16)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const functional = /^rgba?\(([^)]*)\)$/i.exec(trimmed);
|
|
150
|
+
if (functional === null) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const channels = functional[1]
|
|
154
|
+
.split(/[,\s/]+/)
|
|
155
|
+
.filter((part) => part.length > 0)
|
|
156
|
+
.map(Number);
|
|
157
|
+
if (channels.length < 3 || channels.slice(0, 3).some((channel) => !Number.isFinite(channel))) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const clamp = (channel) => Math.max(0, Math.min(255, Math.round(channel)));
|
|
161
|
+
return { r: clamp(channels[0]), g: clamp(channels[1]), b: clamp(channels[2]) };
|
|
162
|
+
};
|
|
163
|
+
/*
|
|
164
|
+
* Audio-reactive envelope.
|
|
165
|
+
*
|
|
166
|
+
* The component never opens a microphone. It reads an `AnalyserNode` the
|
|
167
|
+
* consumer already owns, which keeps permissions, the `AudioContext` lifecycle
|
|
168
|
+
* and the choice of source (microphone, an element, a synthesised tone) on the
|
|
169
|
+
* side of the application that has the context to decide them.
|
|
170
|
+
*/
|
|
171
|
+
/** Scale at silence and at full level. Capped at 1 so a shout cannot clip the sphere. */
|
|
172
|
+
export const AUDIO_SCALE_RANGE = { min: 0.82, max: 1 };
|
|
173
|
+
/** Rotation multiplier at silence and at full level, before `speedMultiplier`. */
|
|
174
|
+
export const AUDIO_SPEED_RANGE = { min: 0.5, max: 2.4 };
|
|
175
|
+
/**
|
|
176
|
+
* How far the level must move before `onlevel` fires again.
|
|
177
|
+
*
|
|
178
|
+
* Without a threshold the callback runs every frame forever, including through
|
|
179
|
+
* silence, and a consumer who sets component state from it re-renders 60 times
|
|
180
|
+
* a second for no reason. A hundredth of the range is below anything a meter
|
|
181
|
+
* or a caption can show.
|
|
182
|
+
*/
|
|
183
|
+
export const LEVEL_REPORT_EPSILON = 0.01;
|
|
184
|
+
/** Envelope-follower time constants. Fast onset, unhurried decay. */
|
|
185
|
+
export const AUDIO_ATTACK_MS = 60;
|
|
186
|
+
export const AUDIO_RELEASE_MS = 320;
|
|
187
|
+
/**
|
|
188
|
+
* RMS of one buffer of byte time-domain data, as 0..1.
|
|
189
|
+
*
|
|
190
|
+
* `getByteTimeDomainData` centres silence on 128, so each sample is an
|
|
191
|
+
* excursion from that midpoint rather than a magnitude. Treating the raw byte
|
|
192
|
+
* as amplitude reads silence as roughly full scale.
|
|
193
|
+
*/
|
|
194
|
+
export const rmsFromTimeDomain = (samples) => {
|
|
195
|
+
if (samples.length === 0) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
let total = 0;
|
|
199
|
+
for (const sample of samples) {
|
|
200
|
+
const excursion = (sample - 128) / 128;
|
|
201
|
+
total += excursion * excursion;
|
|
202
|
+
}
|
|
203
|
+
return Math.sqrt(total / samples.length);
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* One-pole envelope follower, stepped by wall-clock rather than by frame, so
|
|
207
|
+
* the response is the same on a 60Hz and a 120Hz display.
|
|
208
|
+
*
|
|
209
|
+
* Separate attack and release constants are the point: speech is a burst
|
|
210
|
+
* followed by a gap, and a symmetric filter either chatters through every
|
|
211
|
+
* syllable or lags the onset that makes the orb feel connected to the voice.
|
|
212
|
+
*/
|
|
213
|
+
export const followEnvelope = (current, target, deltaMs, attackMs, releaseMs) => {
|
|
214
|
+
if (!Number.isFinite(current)) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (!Number.isFinite(target) || !Number.isFinite(deltaMs) || deltaMs <= 0) {
|
|
218
|
+
return current;
|
|
219
|
+
}
|
|
220
|
+
const timeConstant = target > current ? attackMs : releaseMs;
|
|
221
|
+
if (!Number.isFinite(timeConstant) || timeConstant <= 0) {
|
|
222
|
+
return target;
|
|
223
|
+
}
|
|
224
|
+
const coefficient = 1 - Math.exp(-deltaMs / timeConstant);
|
|
225
|
+
return current + (target - current) * coefficient;
|
|
226
|
+
};
|
|
227
|
+
const clampLevel = (level) => Number.isFinite(level) ? Math.max(0, Math.min(1, level)) : 0;
|
|
228
|
+
export const audioScale = (level) => AUDIO_SCALE_RANGE.min + clampLevel(level) * (AUDIO_SCALE_RANGE.max - AUDIO_SCALE_RANGE.min);
|
|
229
|
+
export const audioSpeed = (level, speedMultiplier) => {
|
|
230
|
+
const base = Number.isFinite(speedMultiplier) ? speedMultiplier : 1;
|
|
231
|
+
return (base *
|
|
232
|
+
(AUDIO_SPEED_RANGE.min + clampLevel(level) * (AUDIO_SPEED_RANGE.max - AUDIO_SPEED_RANGE.min)));
|
|
233
|
+
};
|
|
234
|
+
/*
|
|
235
|
+
* `prefers-reduced-motion` support.
|
|
236
|
+
*
|
|
237
|
+
* The two decorative sources -- idle rotation and the sine-wave breathing
|
|
238
|
+
* `listening` rides without an `analyser` -- are what the OS setting exists to
|
|
239
|
+
* suppress. `level` (non-null only while a real `AnalyserNode` is actually
|
|
240
|
+
* driving the orb) is left alone in both functions below: it reports what the
|
|
241
|
+
* microphone is doing right now, which is state, not decoration, and freezing
|
|
242
|
+
* it would take away the only feedback a caller gave the user that the app is
|
|
243
|
+
* listening. Documented in docs/VoiceOrb.md's Accessibility section.
|
|
244
|
+
*/
|
|
245
|
+
/** Effective rotation speed for one frame. See the reduced-motion note above. */
|
|
246
|
+
export const resolveMotionSpeed = (speedMultiplier, variant, listeningElapsedMs, level, reducedMotion) => {
|
|
247
|
+
if (level !== null) {
|
|
248
|
+
return audioSpeed(level, speedMultiplier);
|
|
249
|
+
}
|
|
250
|
+
if (reducedMotion) {
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
if (variant === 'listening') {
|
|
254
|
+
return listeningSpeed(listeningElapsedMs, speedMultiplier);
|
|
255
|
+
}
|
|
256
|
+
return Number.isFinite(speedMultiplier) ? speedMultiplier : 1;
|
|
257
|
+
};
|
|
258
|
+
/** Effective breathing scale for one frame. Same split as `resolveMotionSpeed`. */
|
|
259
|
+
export const resolveMotionScale = (variant, listeningElapsedMs, level, reducedMotion) => {
|
|
260
|
+
if (level !== null) {
|
|
261
|
+
return audioScale(level);
|
|
262
|
+
}
|
|
263
|
+
if (reducedMotion || variant !== 'listening') {
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
return listeningScale(listeningElapsedMs);
|
|
267
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export type VoiceOrbVariant = 'idle' | 'listening';
|
|
2
|
+
export type VoiceOrbProperties = OptionalVoiceOrbProperties & VoiceOrbEventProperties;
|
|
3
|
+
export type MandatoryVoiceOrbProperties = Record<string, never>;
|
|
4
|
+
export type OptionalVoiceOrbProperties = {
|
|
5
|
+
/**
|
|
6
|
+
* Animation state. `'idle'` rotates at a steady rate; `'listening'` breathes,
|
|
7
|
+
* to show an assistant is taking input.
|
|
8
|
+
*
|
|
9
|
+
* What drives the breathing depends on whether you pass `analyser`. With one,
|
|
10
|
+
* `'listening'` is driven by the sound itself. Without one, it rides a sine
|
|
11
|
+
* wave and is decorative — it says "the microphone is open", never "this is
|
|
12
|
+
* what it hears".
|
|
13
|
+
*/
|
|
14
|
+
variant?: VoiceOrbVariant;
|
|
15
|
+
/**
|
|
16
|
+
* An `AnalyserNode` to drive the orb from, making `variant='listening'` react
|
|
17
|
+
* to the sound instead of breathing on a timer. Ignored while `'idle'`.
|
|
18
|
+
*
|
|
19
|
+
* The node stays yours. This component never calls `getUserMedia`, never
|
|
20
|
+
* creates an `AudioContext` and never closes one: microphone permission, the
|
|
21
|
+
* context lifecycle and the choice of source are decisions that need
|
|
22
|
+
* application context, and a component that grabbed a microphone because it
|
|
23
|
+
* was mounted would be a worse component.
|
|
24
|
+
*
|
|
25
|
+
* Any source works — a live microphone, an `<audio>` element, a synthesised
|
|
26
|
+
* tone. Closing the context under a mounted orb is safe: the read is guarded
|
|
27
|
+
* and the orb falls back to the sine wave rather than freezing.
|
|
28
|
+
*/
|
|
29
|
+
analyser?: AnalyserNode | null;
|
|
30
|
+
/**
|
|
31
|
+
* Multiplies the measured level before it drives the orb. Above `1` makes a
|
|
32
|
+
* quiet speaker fill the sphere; below `1` tames a hot input. Defaults to `1`.
|
|
33
|
+
*
|
|
34
|
+
* Only meaningful alongside `analyser`.
|
|
35
|
+
*/
|
|
36
|
+
sensitivity?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Multiplies the rotation rate. `1` is the designed speed; `0` freezes the
|
|
39
|
+
* orb mid-rotation without unmounting it.
|
|
40
|
+
*/
|
|
41
|
+
speedMultiplier?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Seeds particle placement, so the same value always produces the same orb.
|
|
44
|
+
* A number and the numeric string that names it resolve to the same seed
|
|
45
|
+
* -- `seed={42}` and `seed="42"` paint the same orb, which is what lets a
|
|
46
|
+
* custom-element attribute (always a string) agree with the Svelte prop it
|
|
47
|
+
* is the markup spelling of. Any other string is hashed as text. Omitted,
|
|
48
|
+
* every mount looks different.
|
|
49
|
+
*
|
|
50
|
+
* Pass something stable per user or per session if you want the orb to look
|
|
51
|
+
* like the same object across navigations.
|
|
52
|
+
*/
|
|
53
|
+
seed?: string | number;
|
|
54
|
+
/** Rendered height in pixels. Width always fills the container. Defaults to `400`. */
|
|
55
|
+
height?: number;
|
|
56
|
+
/** Particles on the sphere. Defaults to `1000`. */
|
|
57
|
+
particleCount?: number;
|
|
58
|
+
/** Sphere radius in pixels before projection. Defaults to `140`. */
|
|
59
|
+
radius?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Whether pointer movement pushes particles aside. Defaults to `true`.
|
|
62
|
+
* Set `false` for a purely ambient orb that ignores the pointer.
|
|
63
|
+
*/
|
|
64
|
+
interactive?: boolean;
|
|
65
|
+
/** How far from the pointer the repulsion reaches, in pixels. Defaults to `200`. */
|
|
66
|
+
repelRadius?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Persists rotation across mounts under this `localStorage` key prefix, so
|
|
69
|
+
* the orb resumes where it stopped rather than snapping back.
|
|
70
|
+
*
|
|
71
|
+
* Opt-in, and namespaced by the caller, deliberately. Writing to a
|
|
72
|
+
* consumer's storage uninvited is not a component's decision to make, and a
|
|
73
|
+
* fixed key would make two orbs on one page overwrite each other.
|
|
74
|
+
*/
|
|
75
|
+
persistKey?: string;
|
|
76
|
+
/** CSS class string applied to the orb's root element. */
|
|
77
|
+
classes?: string;
|
|
78
|
+
/** Value for the `data-pw` attribute on the root, for test selectors. */
|
|
79
|
+
testId?: string;
|
|
80
|
+
};
|
|
81
|
+
export type VoiceOrbEventProperties = {
|
|
82
|
+
/**
|
|
83
|
+
* Fires once, on the first frame actually painted — not on mount.
|
|
84
|
+
*
|
|
85
|
+
* Named `onfirstframe` rather than `onanimationstart` because the latter is a
|
|
86
|
+
* real `HTMLElement` accessor for the CSS animationstart event: a web-component
|
|
87
|
+
* prop of that name would shadow the host's own, which `prop-parity` rejects.
|
|
88
|
+
*
|
|
89
|
+
* The gap matters: particle generation and the first paint happen after
|
|
90
|
+
* mount, so a loading overlay removed on mount reveals an empty canvas. This
|
|
91
|
+
* fires when there is something to see.
|
|
92
|
+
*/
|
|
93
|
+
onfirstframe?: () => void;
|
|
94
|
+
/**
|
|
95
|
+
* The smoothed audio level, 0..1, as it changes. Only fires while `analyser`
|
|
96
|
+
* is actually driving the orb, plus once on the way back to 0 when it stops,
|
|
97
|
+
* so a meter does not stick at the last value it saw.
|
|
98
|
+
*
|
|
99
|
+
* Rate-limited by change, not by time: it fires when the level moves by at
|
|
100
|
+
* least 0.01, which means silence is silent rather than 60 calls a second.
|
|
101
|
+
* It is still a per-frame callback in the loud case — drive a DOM write or a
|
|
102
|
+
* cheap store from it, not an expensive re-render.
|
|
103
|
+
*/
|
|
104
|
+
onlevel?: (level: number) => void;
|
|
105
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export { default as Tooltip } from './Tooltip/Tooltip.svelte';
|
|
|
37
37
|
export { default as TypewriterText } from './TypewriterText/TypewriterText.svelte';
|
|
38
38
|
export { tooltip } from './Tooltip/tooltip-action';
|
|
39
39
|
export { default as Shimmer } from './Shimmer/Shimmer.svelte';
|
|
40
|
+
export { default as VoiceOrb } from './VoiceOrb/VoiceOrb.svelte';
|
|
40
41
|
export { default as Progress } from './Progress/Progress.svelte';
|
|
41
42
|
export { default as Pill } from './Pill/Pill.svelte';
|
|
42
43
|
export { default as Pagination } from './Pagination/Pagination.svelte';
|
|
@@ -102,6 +103,7 @@ export { default as Gallery } from './Gallery/Gallery.svelte';
|
|
|
102
103
|
export { ChatController } from './Chat/controller.svelte';
|
|
103
104
|
export { partyOf } from './Chat/roles';
|
|
104
105
|
export { SpeechToTextController } from './SpeechToText/controller.svelte';
|
|
106
|
+
export { SpeechSynthesisController } from './SpeechSynthesis/controller.svelte';
|
|
105
107
|
export { createCopyState } from './Snippet/copyState.svelte';
|
|
106
108
|
export type * from './Button/properties';
|
|
107
109
|
export type * from './Modal/properties';
|
|
@@ -186,9 +188,11 @@ export type * from './ChipInput/properties';
|
|
|
186
188
|
export type * from './FileDropzoneTrigger/properties';
|
|
187
189
|
export type * from './_chart/highlight';
|
|
188
190
|
export type * from './_chart/narrationHighlighter';
|
|
191
|
+
export type * from './VoiceOrb/properties';
|
|
189
192
|
export type * from './Chat/properties';
|
|
190
193
|
export type * from './Chat/types';
|
|
191
194
|
export type * from './SpeechToText/types';
|
|
195
|
+
export type * from './SpeechSynthesis/types';
|
|
192
196
|
export type * from './ChatHeader/properties';
|
|
193
197
|
export type * from './ChatMessage/properties';
|
|
194
198
|
export type * from './ChatMessageList/properties';
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ export { default as Tooltip } from './Tooltip/Tooltip.svelte';
|
|
|
37
37
|
export { default as TypewriterText } from './TypewriterText/TypewriterText.svelte';
|
|
38
38
|
export { tooltip } from './Tooltip/tooltip-action';
|
|
39
39
|
export { default as Shimmer } from './Shimmer/Shimmer.svelte';
|
|
40
|
+
export { default as VoiceOrb } from './VoiceOrb/VoiceOrb.svelte';
|
|
40
41
|
export { default as Progress } from './Progress/Progress.svelte';
|
|
41
42
|
export { default as Pill } from './Pill/Pill.svelte';
|
|
42
43
|
export { default as Pagination } from './Pagination/Pagination.svelte';
|
|
@@ -102,6 +103,7 @@ export { default as Gallery } from './Gallery/Gallery.svelte';
|
|
|
102
103
|
export { ChatController } from './Chat/controller.svelte';
|
|
103
104
|
export { partyOf } from './Chat/roles';
|
|
104
105
|
export { SpeechToTextController } from './SpeechToText/controller.svelte';
|
|
106
|
+
export { SpeechSynthesisController } from './SpeechSynthesis/controller.svelte';
|
|
105
107
|
export { createCopyState } from './Snippet/copyState.svelte';
|
|
106
108
|
export { createSoundKit } from './SoundKit/SoundKit';
|
|
107
109
|
export { validateInput, lockBodyScroll, unlockBodyScroll } from './utils';
|