@nadicodeai/ui 4.0.5 → 5.1.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 (42) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +4 -0
  3. package/dist/components/activity-orb.d.ts +21 -0
  4. package/dist/components/activity-orb.d.ts.map +1 -0
  5. package/dist/components/activity-orb.js +26 -0
  6. package/dist/components/threshold/canvas-budget.d.ts +10 -0
  7. package/dist/components/threshold/canvas-budget.d.ts.map +1 -0
  8. package/dist/components/threshold/canvas-budget.js +54 -0
  9. package/dist/components/threshold/index.d.ts +6 -0
  10. package/dist/components/threshold/index.d.ts.map +1 -0
  11. package/dist/components/threshold/index.js +5 -0
  12. package/dist/components/threshold/orb.d.ts +14 -0
  13. package/dist/components/threshold/orb.d.ts.map +1 -0
  14. package/dist/components/threshold/orb.js +17 -0
  15. package/dist/components/threshold/presence.d.ts +27 -0
  16. package/dist/components/threshold/presence.d.ts.map +1 -0
  17. package/dist/components/threshold/presence.js +159 -0
  18. package/dist/components/threshold/room.d.ts +33 -0
  19. package/dist/components/threshold/room.d.ts.map +1 -0
  20. package/dist/components/threshold/room.js +168 -0
  21. package/dist/components/threshold/threshold.css +228 -0
  22. package/dist/components/threshold/typed-line.d.ts +8 -0
  23. package/dist/components/threshold/typed-line.d.ts.map +1 -0
  24. package/dist/components/threshold/typed-line.js +43 -0
  25. package/dist/components/threshold/use-reduced-motion.d.ts +10 -0
  26. package/dist/components/threshold/use-reduced-motion.d.ts.map +1 -0
  27. package/dist/components/threshold/use-reduced-motion.js +32 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/internal/activity-orb.d.ts +27 -0
  32. package/dist/internal/activity-orb.d.ts.map +1 -0
  33. package/dist/internal/activity-orb.js +150 -0
  34. package/dist/internal/agent-visual/color.d.ts.map +1 -1
  35. package/dist/internal/agent-visual/color.js +13 -7
  36. package/dist/threshold.d.ts +2 -0
  37. package/dist/threshold.d.ts.map +1 -0
  38. package/dist/threshold.js +13 -0
  39. package/docs/activity.md +120 -0
  40. package/docs/contract.md +8 -2
  41. package/docs/threshold.md +221 -0
  42. package/package.json +8 -2
@@ -0,0 +1,228 @@
1
+ /*
2
+ * @nadicodeai/ui/threshold.css
3
+ *
4
+ * The threshold primitives' stylesheet. It is a separate import rather than a
5
+ * part of globals.css because the surface that needs it most, the Nadia
6
+ * desktop app, runs inside a shell that does not load Tailwind: it takes this
7
+ * file and the design-system token layer, and nothing else from this package.
8
+ *
9
+ * A threshold is a fixed dark surface. It is true black in every theme, so the
10
+ * ladders below read the dark-mode tokens directly instead of branching on the
11
+ * app's mode; that is the fixed-presentation escape the package contract
12
+ * names, taken once, coherently, for the whole surface.
13
+ *
14
+ * The room's two colours are shader uniforms, not CSS, and live in room.tsx.
15
+ * The only colour written here is the still field the room falls back to.
16
+ */
17
+
18
+ /* The room ---------------------------------------------------------------- */
19
+
20
+ /*
21
+ * Absolute inside its own relative, isolated parent, never fixed against the
22
+ * viewport: a Portal panel holds a room without covering the page. Its still
23
+ * field is a token-built radial that needs no GPU, and the canvas covers it
24
+ * once the shader is painting.
25
+ */
26
+ .nc-threshold-room {
27
+ position: absolute;
28
+ inset: 0;
29
+ z-index: 0;
30
+ overflow: hidden;
31
+ pointer-events: none;
32
+ background: radial-gradient(
33
+ ellipse 55% 46% at 50% 36%,
34
+ color-mix(in oklab, var(--nc-identity-verde) 30%, var(--nc-identity-ink)),
35
+ transparent 90%
36
+ );
37
+ mask-image: radial-gradient(ellipse 65% 58% at 50% 35%, black 10%, transparent 95%);
38
+ transition: opacity var(--nc-duration-open) var(--nc-ease-emphasized);
39
+ }
40
+
41
+ .nc-threshold-room canvas {
42
+ opacity: 0.65;
43
+ }
44
+
45
+ /* Repair and error states quiet the room; they never remove it. */
46
+ .nc-threshold-room[data-dimmed] {
47
+ opacity: 0.22;
48
+ }
49
+
50
+ /* The presence ------------------------------------------------------------ */
51
+
52
+ .nc-threshold-presence {
53
+ position: relative;
54
+ flex: none;
55
+ width: clamp(180px, 38vh, 320px);
56
+ aspect-ratio: 462 / 520;
57
+ transition: opacity var(--nc-duration-confirm) var(--nc-ease-emphasized);
58
+ }
59
+
60
+ /* Her lower edge dissolves into the room rather than ending on a rectangle. */
61
+ .nc-threshold-presence-figure {
62
+ position: relative;
63
+ width: 100%;
64
+ height: 100%;
65
+ mask-image: linear-gradient(black 55%, transparent 98%);
66
+ transition: opacity var(--nc-duration-confirm) var(--nc-ease-emphasized);
67
+ }
68
+
69
+ .nc-threshold-presence-portrait {
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ object-fit: contain;
74
+ }
75
+
76
+ /* While the water is painting her, the plain portrait beneath it is redundant. */
77
+ .nc-threshold-presence-figure[data-surface="water"] .nc-threshold-presence-portrait {
78
+ visibility: hidden;
79
+ }
80
+
81
+ /*
82
+ * The water canvas is cut to her own silhouette. Water's highlight term is
83
+ * added to the whole frame, not only to the sampled image, so an unmasked
84
+ * canvas paints a faintly lit rectangle over the room: the visible tile the
85
+ * rendered comparisons rejected. The mask matches the shader's own `contain`
86
+ * fit and centred origin, so the two agree pixel for pixel.
87
+ */
88
+ .nc-threshold-presence-figure canvas {
89
+ position: absolute;
90
+ inset: 0;
91
+ mask-image: var(--threshold-portrait);
92
+ mask-position: center;
93
+ mask-repeat: no-repeat;
94
+ mask-size: contain;
95
+ }
96
+
97
+ .nc-threshold-presence[data-state="error"] {
98
+ opacity: 0.62;
99
+ }
100
+
101
+ /*
102
+ * Success: one short verde vivo beat over her own silhouette, then nothing.
103
+ * Verde vivo is Nadia's presence accent and this is the only place a threshold
104
+ * spends it, which is why the buttons beside it stay neutral.
105
+ */
106
+ .nc-threshold-presence::after {
107
+ content: "";
108
+ position: absolute;
109
+ inset: 0;
110
+ background: var(--nc-dark-verde-vivo);
111
+ mask-image: var(--threshold-portrait);
112
+ mask-position: center;
113
+ mask-repeat: no-repeat;
114
+ mask-size: contain;
115
+ opacity: 0;
116
+ pointer-events: none;
117
+ }
118
+
119
+ .nc-threshold-presence[data-state="ready"]::after {
120
+ animation: nc-threshold-arrival 360ms var(--nc-ease-out-strong) 1;
121
+ }
122
+
123
+ @keyframes nc-threshold-arrival {
124
+ 0% {
125
+ opacity: 0;
126
+ }
127
+ 35% {
128
+ opacity: 0.22;
129
+ }
130
+ 100% {
131
+ opacity: 0;
132
+ }
133
+ }
134
+
135
+ /* The orb ----------------------------------------------------------------- */
136
+
137
+ .nc-threshold-orb {
138
+ display: block;
139
+ flex: none;
140
+ }
141
+
142
+ /* The line ---------------------------------------------------------------- */
143
+
144
+ /*
145
+ * A hidden copy of the whole sentence holds the width while the visible one
146
+ * fills in, so the orb beside it does not travel with each character.
147
+ */
148
+ .nc-threshold-line {
149
+ display: grid;
150
+ position: relative;
151
+ margin: 0;
152
+ font-family: var(--nc-type-display-md-font-family);
153
+ font-size: var(--nc-type-display-md-font-size);
154
+ font-weight: var(--nc-type-display-md-font-weight);
155
+ line-height: var(--nc-type-display-md-line-height);
156
+ letter-spacing: -0.01em;
157
+ text-align: center;
158
+ text-wrap: balance;
159
+ color: var(--nc-dark-ink);
160
+ }
161
+
162
+ .nc-threshold-line-measure {
163
+ grid-area: 1 / 1;
164
+ visibility: hidden;
165
+ }
166
+
167
+ .nc-threshold-line-spoken {
168
+ grid-area: 1 / 1;
169
+ }
170
+
171
+ .nc-threshold-caret {
172
+ display: inline-block;
173
+ width: 2px;
174
+ height: 1em;
175
+ margin-left: 2px;
176
+ vertical-align: -0.15em;
177
+ background: currentcolor;
178
+ visibility: hidden;
179
+ }
180
+
181
+ .nc-threshold-line[data-typing] .nc-threshold-caret {
182
+ visibility: visible;
183
+ }
184
+
185
+ /* Shared ------------------------------------------------------------------ */
186
+
187
+ /*
188
+ * The threshold ships its own visually-hidden helper: the desktop shell does
189
+ * not load this package's Tailwind layer, and a progress value nobody can read
190
+ * is not an accessible progress value.
191
+ */
192
+ .nc-threshold-sr {
193
+ position: absolute;
194
+ width: 1px;
195
+ height: 1px;
196
+ overflow: hidden;
197
+ clip-path: inset(50%);
198
+ white-space: nowrap;
199
+ }
200
+
201
+ @media (prefers-reduced-motion: reduce) {
202
+ .nc-threshold-room,
203
+ .nc-threshold-presence,
204
+ .nc-threshold-presence-figure {
205
+ transition: none;
206
+ }
207
+
208
+ .nc-threshold-presence[data-state="ready"]::after {
209
+ animation: none;
210
+ }
211
+
212
+ .nc-threshold-caret {
213
+ visibility: hidden;
214
+ }
215
+ }
216
+
217
+ /*
218
+ * The components also carry the preference as `data-still`, which is what a
219
+ * shell that toggles reduced motion without the media query (an in-app
220
+ * setting) sets. Same result, one rule.
221
+ */
222
+ .nc-threshold-presence[data-still] {
223
+ transition: none;
224
+ }
225
+
226
+ .nc-threshold-presence[data-still][data-state="ready"]::after {
227
+ animation: none;
228
+ }
@@ -0,0 +1,8 @@
1
+ export type TypedLineProps = {
2
+ /** Milliseconds per character. The threshold's cadence is 44. */
3
+ cadenceMs?: number;
4
+ className?: string;
5
+ text: string;
6
+ };
7
+ export declare function TypedLine({ cadenceMs, className, text }: TypedLineProps): import("react").JSX.Element;
8
+ //# sourceMappingURL=typed-line.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed-line.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/typed-line.tsx"],"names":[],"mappings":"AAwBA,MAAM,MAAM,cAAc,GAAG;IAC3B,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,wBAAgB,SAAS,CAAC,EAAE,SAAsB,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,cAAc,+BAwCpF"}
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { cn } from "../../lib/utils.js";
5
+ import { useReducedMotion } from "./use-reduced-motion.js";
6
+ /*
7
+ * The line Nadia says while a threshold is open. It types itself, because a
8
+ * first run is the one moment in the product where waiting is the content.
9
+ *
10
+ * Three decisions the captures settled:
11
+ *
12
+ * - the count comes from elapsed time, not from how many callbacks arrived, so
13
+ * a software renderer or a busy main thread cannot stretch a one second
14
+ * sentence into five;
15
+ * - a hidden copy of the whole sentence reserves its width, so nothing beside
16
+ * the line travels sideways with each character;
17
+ * - the live region receives the complete sentence at once, so a screen reader
18
+ * is never read one letter at a time.
19
+ */
20
+ const CADENCE_MS = 44;
21
+ export function TypedLine({ cadenceMs = CADENCE_MS, className, text }) {
22
+ const still = useReducedMotion();
23
+ const [shown, setShown] = useState("");
24
+ useEffect(() => {
25
+ if (still) {
26
+ setShown(text);
27
+ return;
28
+ }
29
+ setShown("");
30
+ const startedAt = performance.now();
31
+ let timer = 0;
32
+ const tick = () => {
33
+ const count = Math.min(text.length, Math.floor((performance.now() - startedAt) / cadenceMs));
34
+ setShown(text.slice(0, count));
35
+ if (count < text.length)
36
+ timer = window.setTimeout(tick, cadenceMs);
37
+ };
38
+ timer = window.setTimeout(tick, cadenceMs);
39
+ return () => window.clearTimeout(timer);
40
+ }, [cadenceMs, still, text]);
41
+ const typing = shown.length < text.length;
42
+ return (_jsxs("p", { className: cn("nc-threshold-line", className), "data-typing": typing || undefined, children: [_jsx("span", { "aria-hidden": "true", className: "nc-threshold-line-measure", children: text }), _jsxs("span", { "aria-hidden": "true", className: "nc-threshold-line-spoken", children: [_jsx("span", { children: shown }), _jsx("span", { className: "nc-threshold-caret" })] }), _jsx("span", { "aria-live": "polite", className: "nc-threshold-sr", children: text })] }));
43
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Live `prefers-reduced-motion`, the one reader the threshold primitives share.
3
+ *
4
+ * The value is read from the media query on every render rather than cached in
5
+ * state, so a preference changed while a threshold is open reaches the room,
6
+ * the presence, the orb and the typed line in the same commit. Server renders
7
+ * report false and the first client render corrects it.
8
+ */
9
+ export declare function useReducedMotion(): boolean;
10
+ //# sourceMappingURL=use-reduced-motion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-reduced-motion.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/use-reduced-motion.ts"],"names":[],"mappings":"AA2BA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C"}
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { useSyncExternalStore } from "react";
3
+ const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
4
+ function query() {
5
+ return typeof window === "undefined" || typeof window.matchMedia !== "function"
6
+ ? undefined
7
+ : window.matchMedia(REDUCED_MOTION_QUERY);
8
+ }
9
+ function subscribe(onChange) {
10
+ const list = query();
11
+ if (!list)
12
+ return () => { };
13
+ list.addEventListener("change", onChange);
14
+ return () => list.removeEventListener("change", onChange);
15
+ }
16
+ function getSnapshot() {
17
+ return query()?.matches ?? false;
18
+ }
19
+ function getServerSnapshot() {
20
+ return false;
21
+ }
22
+ /**
23
+ * Live `prefers-reduced-motion`, the one reader the threshold primitives share.
24
+ *
25
+ * The value is read from the media query on every render rather than cached in
26
+ * state, so a preference changed while a threshold is open reaches the room,
27
+ * the presence, the orb and the typed line in the same commit. Server renders
28
+ * report false and the first client render corrects it.
29
+ */
30
+ export function useReducedMotion() {
31
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
32
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./components/accordion.js";
2
2
  export * from "./components/action-band.js";
3
3
  export * from "./components/action-status.js";
4
+ export * from "./components/activity-orb.js";
4
5
  export * from "./components/alert.js";
5
6
  export * from "./components/alert-dialog.js";
6
7
  export * from "./components/agent-card.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./components/accordion.js";
2
2
  export * from "./components/action-band.js";
3
3
  export * from "./components/action-status.js";
4
+ export * from "./components/activity-orb.js";
4
5
  export * from "./components/alert.js";
5
6
  export * from "./components/alert-dialog.js";
6
7
  export * from "./components/agent-card.js";
@@ -0,0 +1,27 @@
1
+ /** Product activities, not the vendor's agent vocabulary. */
2
+ declare const ACTIVITY_RECIPES: {
3
+ readonly loading: {
4
+ readonly state: "composing";
5
+ readonly speed: 0.92;
6
+ readonly still: 0.6;
7
+ };
8
+ readonly connecting: {
9
+ readonly state: "connecting";
10
+ readonly speed: 0.86;
11
+ readonly still: 0.6;
12
+ };
13
+ readonly processing: {
14
+ readonly state: "working";
15
+ readonly speed: 0.92;
16
+ readonly still: 0.6;
17
+ };
18
+ };
19
+ export type ActivityKind = keyof typeof ACTIVITY_RECIPES;
20
+ export type ActivityOrbSize = 20 | 64 | 88;
21
+ /** Own canvas lifetime; callers supply activity and accessible meaning only. */
22
+ export declare function mountActivityOrb(canvas: HTMLCanvasElement, activity: ActivityKind, size: ActivityOrbSize, initiallyPaused?: boolean): {
23
+ destroy: () => void;
24
+ setPaused: (_paused: boolean) => void;
25
+ };
26
+ export {};
27
+ //# sourceMappingURL=activity-orb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-orb.d.ts","sourceRoot":"","sources":["../../src/internal/activity-orb.ts"],"names":[],"mappings":"AAEA,6DAA6D;AAC7D,QAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;CAIZ,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,gBAAgB,CAAC;AACzD,MAAM,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAoE3C,gFAAgF;AAChF,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,eAAe,EACrB,eAAe,UAAQ;;yBAEuC,OAAO;EA0FtE"}
@@ -0,0 +1,150 @@
1
+ import { MODE_FRAMES, resolvePreset } from "thinking-orbs/engine";
2
+ /** Product activities, not the vendor's agent vocabulary. */
3
+ const ACTIVITY_RECIPES = {
4
+ loading: { state: "composing", speed: 0.92, still: 0.6 },
5
+ connecting: { state: "connecting", speed: 0.86, still: 0.6 },
6
+ processing: { state: "working", speed: 0.92, still: 0.6 },
7
+ };
8
+ /** Preserve the library's geometry, depth ordering and optical size profiles. */
9
+ function activityFrame(activity, size, seconds) {
10
+ const recipe = ACTIVITY_RECIPES[activity];
11
+ const preset = resolvePreset(recipe.state, size === 20 ? 20 : 64);
12
+ return MODE_FRAMES[preset.mode](size, seconds * preset.speed * recipe.speed, preset.opts);
13
+ }
14
+ function colourAt(palette, white, opacity = 1) {
15
+ const depth = Math.min(1, Math.max(0, white));
16
+ // The foreground particles carry cobalto; the supporting structure keeps
17
+ // inherited ink. Opacity lets the same material work on inverted controls.
18
+ const accent = Math.min(1, Math.max(0, (0.4 - depth) / 0.32)) * 0.75;
19
+ const channels = palette.ink.map((ink, i) => Math.round(ink + ((palette.accent[i] ?? ink) - ink) * accent));
20
+ // eslint-disable-next-line no-restricted-syntax -- Canvas serializes channels resolved from currentColor and --nc-info; this adapter defines no palette literals.
21
+ return `rgba(${channels.join(",")},${opacity * (1 - depth)})`;
22
+ }
23
+ function paintActivityFrame(context, activity, size, seconds, palette) {
24
+ const frame = activityFrame(activity, size, seconds);
25
+ context.clearRect(0, 0, size, size);
26
+ for (const line of frame.lines) {
27
+ context.strokeStyle = colourAt(palette, line.white, line.a);
28
+ context.lineWidth = line.w;
29
+ context.beginPath();
30
+ context.moveTo(line.x1, line.y1);
31
+ context.lineTo(line.x2, line.y2);
32
+ context.stroke();
33
+ }
34
+ for (const dot of frame.dots) {
35
+ context.fillStyle = colourAt(palette, dot.white, dot.a);
36
+ context.beginPath();
37
+ context.arc(dot.x, dot.y, dot.r, 0, Math.PI * 2);
38
+ context.fill();
39
+ }
40
+ }
41
+ function readPalette(canvas) {
42
+ const probe = document.createElement("canvas");
43
+ probe.width = 1;
44
+ probe.height = 1;
45
+ const context = probe.getContext("2d", { willReadFrequently: true });
46
+ if (!context)
47
+ return null;
48
+ const style = getComputedStyle(canvas);
49
+ const read = (value) => {
50
+ context.clearRect(0, 0, 1, 1);
51
+ context.fillStyle = value;
52
+ context.fillRect(0, 0, 1, 1);
53
+ const { data } = context.getImageData(0, 0, 1, 1);
54
+ return [data[0] ?? 0, data[1] ?? 0, data[2] ?? 0];
55
+ };
56
+ const ink = read(style.color);
57
+ const accent = read(style.getPropertyValue("--nc-info").trim() || style.color);
58
+ return { ink, accent };
59
+ }
60
+ /** Own canvas lifetime; callers supply activity and accessible meaning only. */
61
+ export function mountActivityOrb(canvas, activity, size, initiallyPaused = false) {
62
+ const unavailable = { destroy: () => { }, setPaused: (_paused) => { } };
63
+ const context = canvas.getContext("2d");
64
+ if (!context)
65
+ return unavailable;
66
+ let palette = readPalette(canvas);
67
+ if (!palette)
68
+ return unavailable;
69
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
70
+ let reduced = query.matches;
71
+ let paused = initiallyPaused;
72
+ let visible = true;
73
+ let disposed = false;
74
+ let request = 0;
75
+ // Each recipe retains a representative pose under reduced motion.
76
+ const still = ACTIVITY_RECIPES[activity].still;
77
+ let seconds = still;
78
+ let previous = null;
79
+ let ratio = 1;
80
+ function paint() {
81
+ if (!palette || !context)
82
+ return;
83
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
84
+ paintActivityFrame(context, activity, size, reduced ? still : seconds, palette);
85
+ }
86
+ function tick(now) {
87
+ request = 0;
88
+ if (disposed || !visible || document.hidden || reduced || paused)
89
+ return;
90
+ if (previous !== null)
91
+ seconds += Math.min((now - previous) / 1000, 0.1);
92
+ previous = now;
93
+ paint();
94
+ request = requestAnimationFrame(tick);
95
+ }
96
+ function play() {
97
+ cancelAnimationFrame(request);
98
+ previous = null;
99
+ paint();
100
+ if (!disposed && visible && !document.hidden && !reduced && !paused) {
101
+ request = requestAnimationFrame(tick);
102
+ }
103
+ }
104
+ function refresh() {
105
+ ratio = Math.min(window.devicePixelRatio || 1, 2);
106
+ canvas.width = Math.round(size * ratio);
107
+ canvas.height = Math.round(size * ratio);
108
+ palette = readPalette(canvas);
109
+ play();
110
+ }
111
+ function motionChanged(event) {
112
+ reduced = event.matches;
113
+ play();
114
+ }
115
+ const visibility = typeof IntersectionObserver === "undefined"
116
+ ? null
117
+ : new IntersectionObserver(([entry]) => {
118
+ visible = entry?.isIntersecting ?? false;
119
+ play();
120
+ });
121
+ visibility?.observe(canvas);
122
+ // Observe ancestors only: changing unrelated content must not repaint this
123
+ // canvas or install a document-wide subtree observer per inline indicator.
124
+ const theme = new MutationObserver(refresh);
125
+ for (let ancestor = canvas; ancestor; ancestor = ancestor.parentElement) {
126
+ theme.observe(ancestor, {
127
+ attributes: true,
128
+ attributeFilter: ["class", "style", "data-theme"],
129
+ });
130
+ }
131
+ query.addEventListener("change", motionChanged);
132
+ document.addEventListener("visibilitychange", play);
133
+ window.addEventListener("resize", refresh);
134
+ refresh();
135
+ return {
136
+ setPaused(value) {
137
+ paused = value;
138
+ play();
139
+ },
140
+ destroy() {
141
+ disposed = true;
142
+ cancelAnimationFrame(request);
143
+ visibility?.disconnect();
144
+ theme.disconnect();
145
+ query.removeEventListener("change", motionChanged);
146
+ document.removeEventListener("visibilitychange", play);
147
+ window.removeEventListener("resize", refresh);
148
+ },
149
+ };
150
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../../src/internal/agent-visual/color.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AA4EpD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAG3F"}
1
+ {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../../src/internal/agent-visual/color.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAmFpD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAG3F"}
@@ -1,4 +1,10 @@
1
1
  const clamp = (value) => Math.min(Math.max(value, 0), 1);
2
+ // CSS compilers may serialize numeric channels as percentages. CSS Color 4
3
+ // gives each channel its own 100% value (OKLCH L: 1, C: 0.4; Lab a/b: 125).
4
+ function numberOrPercentage(value, percentRange) {
5
+ const number = Number.parseFloat(value ?? "");
6
+ return value?.endsWith("%") ? (number / 100) * percentRange : number;
7
+ }
2
8
  function linearToSrgb(value) {
3
9
  const channel = clamp(value);
4
10
  return channel <= 0.0031308 ? channel * 12.92 : 1.055 * channel ** (1 / 2.4) - 0.055;
@@ -12,10 +18,10 @@ function parseOklch(value) {
12
18
  return null;
13
19
  const [channels, alphaValue] = value.slice(start + 1, end).split("/");
14
20
  const [lightnessValue, chromaValue, hueValue] = channels?.trim().split(/\s+/) ?? [];
15
- const lightness = Number.parseFloat(lightnessValue ?? "");
16
- const chroma = Number.parseFloat(chromaValue ?? "");
21
+ const lightness = numberOrPercentage(lightnessValue, 1);
22
+ const chroma = numberOrPercentage(chromaValue, 0.4);
17
23
  const hue = Number.parseFloat(hueValue ?? "");
18
- const alpha = alphaValue ? Number.parseFloat(alphaValue.trim()) : 1;
24
+ const alpha = alphaValue ? numberOrPercentage(alphaValue.trim(), 1) : 1;
19
25
  if (![lightness, chroma, hue, alpha].every(Number.isFinite))
20
26
  return null;
21
27
  const radians = (hue * Math.PI) / 180;
@@ -43,10 +49,10 @@ function parseLab(value) {
43
49
  return null;
44
50
  const [channels, alphaValue] = value.slice(start + 1, end).split("/");
45
51
  const [lightnessValue, aValue, bValue] = channels?.trim().split(/\s+/) ?? [];
46
- const lightness = Number.parseFloat(lightnessValue ?? "");
47
- const a = Number.parseFloat(aValue ?? "");
48
- const b = Number.parseFloat(bValue ?? "");
49
- const alpha = alphaValue ? Number.parseFloat(alphaValue.trim()) : 1;
52
+ const lightness = numberOrPercentage(lightnessValue, 100);
53
+ const a = numberOrPercentage(aValue, 125);
54
+ const b = numberOrPercentage(bValue, 125);
55
+ const alpha = alphaValue ? numberOrPercentage(alphaValue.trim(), 1) : 1;
50
56
  if (![lightness, a, b, alpha].every(Number.isFinite))
51
57
  return null;
52
58
  const delta = 6 / 29;
@@ -0,0 +1,2 @@
1
+ export * from "./components/threshold/index.js";
2
+ //# sourceMappingURL=threshold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threshold.d.ts","sourceRoot":"","sources":["../src/threshold.ts"],"names":[],"mappings":"AAYA,cAAc,8BAA8B,CAAC"}
@@ -0,0 +1,13 @@
1
+ /*
2
+ * `@nadicodeai/ui/threshold`: the primitives every threshold is drawn from.
3
+ *
4
+ * A threshold is the moment before the product is usable: a first run, a
5
+ * sign-in, a repair. The Nadia desktop app draws its room, presence, status
6
+ * orb and typed line from here, so the brand logic lives in one place.
7
+ *
8
+ * The screens themselves, and the state machines behind them, stay in the
9
+ * consuming app. This entry ships no screen.
10
+ *
11
+ * The stylesheet is a separate import: `@nadicodeai/ui/threshold.css`.
12
+ */
13
+ export * from "./components/threshold/index.js";