@juspay/svelte-ui-components 4.32.1 → 4.33.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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Turns one `Frame` into canvas calls. The only place that knows about theme
3
+ * and colour -- a mode never sees `dark` or `tint`, only the 0..1 `ink` it
4
+ * assigns each mark.
5
+ */
6
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
7
+ /**
8
+ * Maps `ink` (0 = full `tint`) to an exact, testable colour string: fading
9
+ * toward white in light theme and toward black in dark theme, so a mark
10
+ * further away can fade into either background without ever reading as the
11
+ * wrong theme's ink.
12
+ */
13
+ export const inkColor = (tint, ink, dark) => {
14
+ const amount = clamp01(ink);
15
+ const fadeTo = dark ? 0 : 255;
16
+ const r = Math.round(tint.r + (fadeTo - tint.r) * amount);
17
+ const g = Math.round(tint.g + (fadeTo - tint.g) * amount);
18
+ const b = Math.round(tint.b + (fadeTo - tint.b) * amount);
19
+ return `rgb(${r} ${g} ${b})`;
20
+ };
21
+ /**
22
+ * Clears the canvas and draws a `Frame`: every stroke, then every dot, each
23
+ * group sorted far-to-near by `depth` so nearer marks are painted last and
24
+ * sit on top.
25
+ */
26
+ export const paintFrame = (ctx, frame, options) => {
27
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
28
+ const strokes = [...frame.strokes].sort((a, b) => a.depth - b.depth);
29
+ for (const stroke of strokes) {
30
+ ctx.strokeStyle = inkColor(options.tint, stroke.ink, options.dark);
31
+ ctx.globalAlpha = clamp01(stroke.alpha);
32
+ ctx.lineWidth = stroke.width;
33
+ ctx.beginPath();
34
+ ctx.moveTo(stroke.x1, stroke.y1);
35
+ ctx.lineTo(stroke.x2, stroke.y2);
36
+ ctx.stroke();
37
+ }
38
+ const dots = [...frame.dots].sort((a, b) => a.depth - b.depth);
39
+ for (const dot of dots) {
40
+ ctx.fillStyle = inkColor(options.tint, dot.ink, options.dark);
41
+ ctx.globalAlpha = clamp01(dot.alpha);
42
+ ctx.beginPath();
43
+ ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2);
44
+ ctx.fill();
45
+ }
46
+ ctx.globalAlpha = 1;
47
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Pure 3D helpers shared by every mode: rotation, perspective projection,
3
+ * even sphere sampling, easing and a seeded per-index hash. Nothing here
4
+ * touches a canvas or keeps state, so a mode stays a plain function of
5
+ * `(t, ctx)` and every helper below is trivial to unit test in isolation.
6
+ */
7
+ import type { ModeSize } from './types';
8
+ /** A point in the mode's own 3D world space, before projection to the canvas. */
9
+ export type Point3 = {
10
+ x: number;
11
+ y: number;
12
+ z: number;
13
+ };
14
+ /** A `Point3` projected onto the canvas: screen px plus normalised depth and raw scale. */
15
+ export type Projected = {
16
+ x: number;
17
+ y: number;
18
+ depth: number;
19
+ scale: number;
20
+ };
21
+ /** The two scalers `modeSizing` hands back: multiply a mode's own base counts and radii by them. */
22
+ export type ModeSizing = {
23
+ count: (base: number) => number;
24
+ radius: (base: number) => number;
25
+ };
26
+ /** Rotates a point about the X axis (mixes y and z). */
27
+ export declare const rotateX: (point: Point3, angle: number) => Point3;
28
+ /** Rotates a point about the Y axis (mixes x and z). */
29
+ export declare const rotateY: (point: Point3, angle: number) => Point3;
30
+ /** Rotates a point about the Z axis (mixes x and y). */
31
+ export declare const rotateZ: (point: Point3, angle: number) => Point3;
32
+ /**
33
+ * Projects a world point with perspective onto a canvas centred at
34
+ * `(originPx, originPx)`. `worldRadius` is the extent the point's `z` is
35
+ * expected to range over (typically the sphere/shape radius the mode drew it
36
+ * at), and is what turns `z` into a 0..1 `depth`: nearer (more negative `z`)
37
+ * is 1, farther is 0.
38
+ */
39
+ export declare const project: (point: Point3, originPx: number, worldRadius: number) => Projected;
40
+ /**
41
+ * The `index`-th of `count` points spread evenly over a sphere of `radius`,
42
+ * via a golden-angle spiral. Deterministic in `index` and `count` alone, and
43
+ * even across the whole surface rather than crowding the poles the way a
44
+ * uniformly-sampled latitude/longitude grid would.
45
+ */
46
+ export declare const sampleSphere: (index: number, count: number, radius: number) => Point3;
47
+ /** Cubic ease, slow-fast-slow, for a motion that starts and ends at rest. */
48
+ export declare const easeInOutCubic: (t: number) => number;
49
+ /** Cubic ease-out, fast-then-slow, for a motion that only needs to settle at the end. */
50
+ export declare const easeOutCubic: (t: number) => number;
51
+ /**
52
+ * A deterministic 0..1 value for an integer seed (a dot's index is the usual
53
+ * one). Gives every mode per-dot variety -- a phase offset, a size jitter --
54
+ * without `Math.random`, so the same `(t, ctx)` always paints the same frame.
55
+ */
56
+ export declare const hashUnit: (seed: number) => number;
57
+ /**
58
+ * Turns `(size, density, dotScale)` into a count scaler and a radius scaler,
59
+ * so every mode derives its dot counts and radii the same way instead of
60
+ * keeping its own per-size table. Counts scale with area (`size` squared);
61
+ * radii grow sub-linearly, with a floor that keeps dots visible at 20px.
62
+ */
63
+ export declare const modeSizing: (size: ModeSize, density: number, dotScale: number) => ModeSizing;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Pure 3D helpers shared by every mode: rotation, perspective projection,
3
+ * even sphere sampling, easing and a seeded per-index hash. Nothing here
4
+ * touches a canvas or keeps state, so a mode stays a plain function of
5
+ * `(t, ctx)` and every helper below is trivial to unit test in isolation.
6
+ */
7
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
8
+ /** Rotates a point about the X axis (mixes y and z). */
9
+ export const rotateX = (point, angle) => {
10
+ const cos = Math.cos(angle);
11
+ const sin = Math.sin(angle);
12
+ return {
13
+ x: point.x,
14
+ y: point.y * cos - point.z * sin,
15
+ z: point.y * sin + point.z * cos
16
+ };
17
+ };
18
+ /** Rotates a point about the Y axis (mixes x and z). */
19
+ export const rotateY = (point, angle) => {
20
+ const cos = Math.cos(angle);
21
+ const sin = Math.sin(angle);
22
+ return {
23
+ x: point.x * cos + point.z * sin,
24
+ y: point.y,
25
+ z: point.z * cos - point.x * sin
26
+ };
27
+ };
28
+ /** Rotates a point about the Z axis (mixes x and y). */
29
+ export const rotateZ = (point, angle) => {
30
+ const cos = Math.cos(angle);
31
+ const sin = Math.sin(angle);
32
+ return {
33
+ x: point.x * cos - point.y * sin,
34
+ y: point.x * sin + point.y * cos,
35
+ z: point.z
36
+ };
37
+ };
38
+ const FOCAL_LENGTH_PX = 320;
39
+ /**
40
+ * Projects a world point with perspective onto a canvas centred at
41
+ * `(originPx, originPx)`. `worldRadius` is the extent the point's `z` is
42
+ * expected to range over (typically the sphere/shape radius the mode drew it
43
+ * at), and is what turns `z` into a 0..1 `depth`: nearer (more negative `z`)
44
+ * is 1, farther is 0.
45
+ */
46
+ export const project = (point, originPx, worldRadius) => {
47
+ const safeRadius = worldRadius > 0 ? worldRadius : 1;
48
+ const perspective = FOCAL_LENGTH_PX / (FOCAL_LENGTH_PX + point.z);
49
+ return {
50
+ x: originPx + point.x * perspective,
51
+ y: originPx + point.y * perspective,
52
+ depth: clamp01((safeRadius - point.z) / (safeRadius * 2)),
53
+ scale: perspective
54
+ };
55
+ };
56
+ const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
57
+ /**
58
+ * The `index`-th of `count` points spread evenly over a sphere of `radius`,
59
+ * via a golden-angle spiral. Deterministic in `index` and `count` alone, and
60
+ * even across the whole surface rather than crowding the poles the way a
61
+ * uniformly-sampled latitude/longitude grid would.
62
+ */
63
+ export const sampleSphere = (index, count, radius) => {
64
+ const safeCount = count > 0 ? Math.floor(count) : 1;
65
+ const wrapped = ((Math.floor(index) % safeCount) + safeCount) % safeCount;
66
+ const y = 1 - (2 * wrapped + 1) / safeCount;
67
+ const ringRadius = Math.sqrt(Math.max(0, 1 - y * y));
68
+ const angle = wrapped * GOLDEN_ANGLE;
69
+ return {
70
+ x: Math.cos(angle) * ringRadius * radius,
71
+ y: y * radius,
72
+ z: Math.sin(angle) * ringRadius * radius
73
+ };
74
+ };
75
+ /** Cubic ease, slow-fast-slow, for a motion that starts and ends at rest. */
76
+ export const easeInOutCubic = (t) => {
77
+ const clamped = clamp01(t);
78
+ return clamped < 0.5 ? 4 * clamped ** 3 : 1 - (-2 * clamped + 2) ** 3 / 2;
79
+ };
80
+ /** Cubic ease-out, fast-then-slow, for a motion that only needs to settle at the end. */
81
+ export const easeOutCubic = (t) => {
82
+ const clamped = clamp01(t);
83
+ return 1 - (1 - clamped) ** 3;
84
+ };
85
+ /**
86
+ * A deterministic 0..1 value for an integer seed (a dot's index is the usual
87
+ * one). Gives every mode per-dot variety -- a phase offset, a size jitter --
88
+ * without `Math.random`, so the same `(t, ctx)` always paints the same frame.
89
+ */
90
+ export const hashUnit = (seed) => {
91
+ let hash = (seed | 0) >>> 0;
92
+ hash = Math.imul(hash ^ (hash >>> 16), 0x45d9f3b);
93
+ hash = Math.imul(hash ^ (hash >>> 16), 0x45d9f3b);
94
+ hash = (hash ^ (hash >>> 16)) >>> 0;
95
+ return hash / 4294967296;
96
+ };
97
+ const REFERENCE_SIZE = 64;
98
+ const RADIUS_GROWTH_EXPONENT = 0.6;
99
+ const MIN_DOT_RADIUS_PX = 0.6;
100
+ /**
101
+ * Turns `(size, density, dotScale)` into a count scaler and a radius scaler,
102
+ * so every mode derives its dot counts and radii the same way instead of
103
+ * keeping its own per-size table. Counts scale with area (`size` squared);
104
+ * radii grow sub-linearly, with a floor that keeps dots visible at 20px.
105
+ */
106
+ export const modeSizing = (size, density, dotScale) => {
107
+ const sizeRatio = size / REFERENCE_SIZE;
108
+ const areaRatio = sizeRatio * sizeRatio;
109
+ const safeDensity = density > 0 ? density : 0.1;
110
+ const safeDotScale = dotScale > 0 ? dotScale : 0.1;
111
+ return {
112
+ count: (base) => Math.max(1, Math.round(base * areaRatio * safeDensity)),
113
+ radius: (base) => Math.max(MIN_DOT_RADIUS_PX, base * sizeRatio ** RADIUS_GROWTH_EXPONENT * safeDotScale)
114
+ };
115
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The shared vocabulary between `ThinkingOrb`'s modes, its painter and its
3
+ * gravity pass. A mode never touches a canvas directly -- it only ever
4
+ * describes one instant as a `Frame`, so it stays a pure, testable function
5
+ * of `(t, ctx)` with nothing environment-specific to mock.
6
+ */
7
+ /** One canvas size `ThinkingOrb` renders at; a mode adapts to whichever it is given. */
8
+ export type ModeSize = 64 | 32 | 20;
9
+ /**
10
+ * One drawn point. `x`/`y` are canvas CSS px, `depth` is 0 (far) to 1 (near),
11
+ * `ink` is 0 to 1 and is turned into a colour by `paintFrame`, and `alpha` is
12
+ * 0 to 1 opacity.
13
+ */
14
+ export type Dot = {
15
+ x: number;
16
+ y: number;
17
+ depth: number;
18
+ radius: number;
19
+ ink: number;
20
+ alpha: number;
21
+ };
22
+ /** One drawn line segment. Same coordinate, depth, ink and alpha units as `Dot`. */
23
+ export type Stroke = {
24
+ x1: number;
25
+ y1: number;
26
+ x2: number;
27
+ y2: number;
28
+ depth: number;
29
+ width: number;
30
+ ink: number;
31
+ alpha: number;
32
+ };
33
+ /** Everything to paint for one instant: strokes and dots, in no particular order. */
34
+ export type Frame = {
35
+ dots: readonly Dot[];
36
+ strokes: readonly Stroke[];
37
+ };
38
+ /**
39
+ * What a mode renders at and how strongly. `density` and `dotScale` are
40
+ * multipliers already floored at 0.1 by the component, never negative or zero.
41
+ */
42
+ export type ModeContext = {
43
+ size: ModeSize;
44
+ density: number;
45
+ dotScale: number;
46
+ };
47
+ /**
48
+ * A mode: a pure function of elapsed, already speed-scaled seconds and the
49
+ * context it draws into. Must be non-blank, finite and in-bounds at every `t`,
50
+ * including `t = 0`.
51
+ */
52
+ export type ModeFn = (t: number, ctx: ModeContext) => Frame;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The shared vocabulary between `ThinkingOrb`'s modes, its painter and its
3
+ * gravity pass. A mode never touches a canvas directly -- it only ever
4
+ * describes one instant as a `Frame`, so it stays a pure, testable function
5
+ * of `(t, ctx)` with nothing environment-specific to mock.
6
+ */
7
+ export {};
@@ -0,0 +1,68 @@
1
+ import type { Rgb } from '../types';
2
+ import type { GravityOptions } from './orb/gravity';
3
+ import type { ModeSize } from './orb/types';
4
+ export type { GravityOptions };
5
+ /**
6
+ * The nine animations `ThinkingOrb` can play. Full descriptions of each are
7
+ * in docs/ThinkingOrb.md.
8
+ */
9
+ export type OrbState = 'working' | 'searching' | 'solving' | 'listening' | 'connecting' | 'weaving' | 'composing' | 'breathing' | 'shaping';
10
+ /** The three fixed canvas sizes `ThinkingOrb` can draw at, in CSS pixels. */
11
+ export type ThinkingOrbSize = ModeSize;
12
+ export type MandatoryThinkingOrbProperties = Record<string, never>;
13
+ export type OptionalThinkingOrbProperties = {
14
+ /**
15
+ * Which animation to show.
16
+ * - `working` — dots travel in short trails along six differently tilted loops around the centre, each loop at its own pace
17
+ * - `searching` — a bright band sweeps around a speckled sphere, running from top to bottom while it goes, and brightens whichever dots it passes over
18
+ * - `solving` — four horizontal bands each turn a quarter-turn out of place, one after another, hold there a moment, then straighten out again starting from the last one that turned, looping the whole way through
19
+ * - `listening` — a wave of brightness rises through a stack of rings, swelling the dots in whichever ring it currently reaches
20
+ * - `connecting` — a small scattering of nodes across a sphere, linked to their closest neighbours, with small bright dots travelling along those links
21
+ * - `weaving` — three spiral strands wrap around a sphere end to end, swinging into and out of each other's path so they appear to cross as it spins
22
+ * - `composing` — a broad ribbon of dots, fading out at its edges, ripples like fabric while the whole thing turns
23
+ * - `breathing` — a single ring of dots slowly swells and settles on a steady cycle, its edge never quite resting
24
+ * - `shaping` — a ring of dots gradually reshapes itself, rounding out and pulling into a triangle, then a square, before smoothing back to round, on a repeating loop
25
+ * @default 'working'
26
+ */
27
+ state?: OrbState;
28
+ /** How big the canvas is, in CSS pixels. Only 64, 32 and 20 have real artwork; any other number falls back to `64`. @default 64 */
29
+ size?: ThinkingOrbSize;
30
+ /** Multiplies how fast the animation's own clock advances: `1` is normal pace, `2` is double speed, `0` holds it still. @default 1 */
31
+ speed?: number;
32
+ /** Stops the animation and leaves whatever frame was last drawn on screen. @default false */
33
+ paused?: boolean;
34
+ /**
35
+ * Sets the colour the dots and lines are drawn in. Left unset, the orb
36
+ * reads `--sui-thinking-orb-color`, falls back to the inherited CSS
37
+ * `color`, and finally to a mid-grey if neither resolves to something the
38
+ * canvas can use.
39
+ */
40
+ color?: Rgb;
41
+ /**
42
+ * Scales how many dots (or strands, or nodes, depending on the state) an
43
+ * animation draws. Anything below `0.1` is treated as `0.1`. @default 1
44
+ */
45
+ dots?: number;
46
+ /** Scales the size of every dot the animation draws. Anything below `0.1` is treated as `0.1`. @default 1 */
47
+ dotSize?: number;
48
+ /**
49
+ * While the pointer sits over the canvas, dots near it get pulled toward
50
+ * it — scoped to this instance's own canvas only. `true` uses the built-in
51
+ * reach and pull; pass a `GravityOptions` object to set your own
52
+ * `radius`/`strength`. Purely decorative, and deliberately has no
53
+ * keyboard equivalent — see docs/ThinkingOrb.md's Accessibility section.
54
+ * @default false
55
+ */
56
+ gravity?: boolean | GravityOptions;
57
+ /** Replaces the default label generated for the current state. See docs/ThinkingOrb.md's Accessibility section. */
58
+ ariaLabel?: string;
59
+ /** Extra CSS class names added to the canvas element. */
60
+ classes?: string;
61
+ /** Sets the `data-pw` attribute on the canvas, so tests can select it. */
62
+ testId?: string;
63
+ };
64
+ export type ThinkingOrbEventProperties = {
65
+ /** Called exactly once, right after the first frame is actually painted — this happens after mount, not at the same moment. */
66
+ onfirstframe?: () => void;
67
+ };
68
+ export type ThinkingOrbProperties = OptionalThinkingOrbProperties & ThinkingOrbEventProperties;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -55,6 +55,7 @@ export { default as Calendar } from './Calendar/Calendar.svelte';
55
55
  export { default as RelativeTime } from './RelativeTime/RelativeTime.svelte';
56
56
  export { default as ThemeSwitcher } from './ThemeSwitcher/ThemeSwitcher.svelte';
57
57
  export { default as ThinkingIndicator } from './ThinkingIndicator/ThinkingIndicator.svelte';
58
+ export { default as ThinkingOrb } from './ThinkingOrb/ThinkingOrb.svelte';
58
59
  export { default as ToolCallLog } from './ToolCallLog/ToolCallLog.svelte';
59
60
  export { default as TaskList } from './TaskList/TaskList.svelte';
60
61
  export { default as Book } from './Book/Book.svelte';
@@ -166,6 +167,7 @@ export type * from './Calendar/properties';
166
167
  export type * from './RelativeTime/properties';
167
168
  export type * from './ThemeSwitcher/properties';
168
169
  export type * from './ThinkingIndicator/properties';
170
+ export type * from './ThinkingOrb/properties';
169
171
  export type * from './ToolCallLog/properties';
170
172
  export type * from './TaskList/properties';
171
173
  export type * from './SoundKit/properties';
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ export { default as Calendar } from './Calendar/Calendar.svelte';
55
55
  export { default as RelativeTime } from './RelativeTime/RelativeTime.svelte';
56
56
  export { default as ThemeSwitcher } from './ThemeSwitcher/ThemeSwitcher.svelte';
57
57
  export { default as ThinkingIndicator } from './ThinkingIndicator/ThinkingIndicator.svelte';
58
+ export { default as ThinkingOrb } from './ThinkingOrb/ThinkingOrb.svelte';
58
59
  export { default as ToolCallLog } from './ToolCallLog/ToolCallLog.svelte';
59
60
  export { default as TaskList } from './TaskList/TaskList.svelte';
60
61
  export { default as Book } from './Book/Book.svelte';
@@ -17,7 +17,7 @@
17
17
  * stylesheet is a crutch for the migration window, not a permanent override.
18
18
  */
19
19
 
20
- /* components whose new default is `block` (87) */
20
+ /* components whose new default is `block` (88) */
21
21
  sui-accordion { --sui-accordion-display: inline; }
22
22
  sui-area-chart { --sui-area-chart-display: inline; }
23
23
  sui-aspect-ratio { --sui-aspect-ratio-display: inline; }
@@ -98,6 +98,7 @@ sui-table { --sui-table-display: inline; }
98
98
  sui-tabs { --sui-tabs-display: inline; }
99
99
  sui-task-list { --sui-task-list-display: inline; }
100
100
  sui-thinking-indicator { --sui-thinking-indicator-display: inline; }
101
+ sui-thinking-orb { --sui-thinking-orb-display: inline; }
101
102
  sui-toast { --sui-toast-display: inline; }
102
103
  sui-toggle { --sui-toggle-display: inline; }
103
104
  sui-tool-call-log { --sui-tool-call-log-display: inline; }
@@ -459,6 +459,11 @@
459
459
  "property": "--sui-thinking-indicator-display",
460
460
  "default": "block"
461
461
  },
462
+ {
463
+ "tag": "sui-thinking-orb",
464
+ "property": "--sui-thinking-orb-display",
465
+ "default": "block"
466
+ },
462
467
  {
463
468
  "tag": "sui-toast",
464
469
  "property": "--sui-toast-display",