@atmos.build/ui 0.1.0 → 0.1.2
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/components/diff-stat-format.d.ts +9 -0
- package/dist/components/diff-stat-format.js +13 -0
- package/dist/components/diff-stat-label.d.ts +27 -0
- package/dist/components/diff-stat-label.js +24 -0
- package/dist/components/naut-avatar-engine.d.ts +21 -5
- package/dist/components/naut-avatar-engine.js +79 -10
- package/dist/components/naut-avatar.d.ts +2 -0
- package/dist/components/naut-avatar.js +106 -19
- package/dist/components/naut-pointer-gaze.d.ts +16 -0
- package/dist/components/naut-pointer-gaze.js +31 -0
- package/dist/components/naut-scene.d.ts +138 -0
- package/dist/components/naut-scene.js +169 -0
- package/dist/components/shimmer-text.d.ts +3 -3
- package/dist/components/shimmer-text.js +51 -15
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp-app/vite.d.ts +3 -1
- package/dist/mcp-app/vite.js +106 -1
- package/package.json +6 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a diff's line counts are written down.
|
|
3
|
+
*
|
|
4
|
+
* Exact below a thousand and abbreviated above it: a reviewer wants to know a hunk touched
|
|
5
|
+
* seven lines, and only that a rewrite touched roughly sixteen hundred. Abbreviation goes
|
|
6
|
+
* through `Intl.NumberFormat` rather than a hardcoded `k`, so a five-figure refactor stays the
|
|
7
|
+
* same width as a one-line fix in every locale.
|
|
8
|
+
*/
|
|
9
|
+
export declare function compactFormatter(locale?: string): (value: number) => string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a diff's line counts are written down.
|
|
3
|
+
*
|
|
4
|
+
* Exact below a thousand and abbreviated above it: a reviewer wants to know a hunk touched
|
|
5
|
+
* seven lines, and only that a rewrite touched roughly sixteen hundred. Abbreviation goes
|
|
6
|
+
* through `Intl.NumberFormat` rather than a hardcoded `k`, so a five-figure refactor stays the
|
|
7
|
+
* same width as a one-line fix in every locale.
|
|
8
|
+
*/
|
|
9
|
+
export function compactFormatter(locale) {
|
|
10
|
+
const exact = new Intl.NumberFormat(locale);
|
|
11
|
+
const compact = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 });
|
|
12
|
+
return (value) => (value < 1000 ? exact.format(value) : compact.format(value));
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface DiffStatLabelProps extends Omit<React.ComponentProps<'span'>, 'children'> {
|
|
3
|
+
additions: number;
|
|
4
|
+
deletions: number;
|
|
5
|
+
/**
|
|
6
|
+
* The whole label as one string, for readers who get it announced rather than scanned.
|
|
7
|
+
* Required because the visible form is punctuation and digits, which is not a sentence in
|
|
8
|
+
* any language — the caller is the only place that can translate it.
|
|
9
|
+
*/
|
|
10
|
+
label: string;
|
|
11
|
+
/** Formatting locale. Left to the runtime's own when the caller has no opinion. */
|
|
12
|
+
locale?: string;
|
|
13
|
+
size?: 'sm' | 'md';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* How much a diff changed, in the two numbers a reviewer actually scans for.
|
|
17
|
+
*
|
|
18
|
+
* The colours are the diff surface's own `--diff-added` / `--diff-removed` rather than the
|
|
19
|
+
* status palette's success and danger: this label sits next to the lines it counts, and a
|
|
20
|
+
* green borrowed from "operation succeeded" reads as a different kind of statement.
|
|
21
|
+
*
|
|
22
|
+
* Counts go through `Intl.NumberFormat` rather than a hand-rolled `k`, which means the width
|
|
23
|
+
* is the locale's decision and not ours — German short notation does not abbreviate thousands
|
|
24
|
+
* at all, so a German reader sees `24.019` where an English one sees `24K`. Showing them an
|
|
25
|
+
* English abbreviation instead would be narrower and wrong.
|
|
26
|
+
*/
|
|
27
|
+
export declare function DiffStatLabel({ additions, deletions, label, locale, size, className, ...props }: DiffStatLabelProps): React.JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../lib/cn.js';
|
|
5
|
+
import { compactFormatter } from './diff-stat-format.js';
|
|
6
|
+
const SIZES = { sm: 'gap-1 text-[11px]', md: 'gap-1.5 text-xs' };
|
|
7
|
+
/**
|
|
8
|
+
* How much a diff changed, in the two numbers a reviewer actually scans for.
|
|
9
|
+
*
|
|
10
|
+
* The colours are the diff surface's own `--diff-added` / `--diff-removed` rather than the
|
|
11
|
+
* status palette's success and danger: this label sits next to the lines it counts, and a
|
|
12
|
+
* green borrowed from "operation succeeded" reads as a different kind of statement.
|
|
13
|
+
*
|
|
14
|
+
* Counts go through `Intl.NumberFormat` rather than a hand-rolled `k`, which means the width
|
|
15
|
+
* is the locale's decision and not ours — German short notation does not abbreviate thousands
|
|
16
|
+
* at all, so a German reader sees `24.019` where an English one sees `24K`. Showing them an
|
|
17
|
+
* English abbreviation instead would be narrower and wrong.
|
|
18
|
+
*/
|
|
19
|
+
export function DiffStatLabel({ additions, deletions, label, locale, size = 'sm', className, ...props }) {
|
|
20
|
+
const format = React.useMemo(() => compactFormatter(locale), [locale]);
|
|
21
|
+
const added = Math.max(0, Math.trunc(additions));
|
|
22
|
+
const removed = Math.max(0, Math.trunc(deletions));
|
|
23
|
+
return (_jsx("span", { "data-slot": "diff-stat-label", className: cn('inline-flex shrink-0 items-center font-mono tabular-nums', SIZES[size], className), "aria-label": label, ...props, children: added === 0 && removed === 0 ? (_jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "\u2014" })) : (_jsxs(_Fragment, { children: [added > 0 ? (_jsxs("span", { className: "text-diff-added", "aria-hidden": true, children: ["+", format(added)] })) : null, removed > 0 ? (_jsxs("span", { className: "text-diff-removed", "aria-hidden": true, children: ["\u2212", format(removed)] })) : null] })) }));
|
|
24
|
+
}
|
|
@@ -31,12 +31,20 @@ export interface NautAvatarOptions {
|
|
|
31
31
|
seed?: number;
|
|
32
32
|
idle?: boolean;
|
|
33
33
|
}
|
|
34
|
+
export interface NautAvatarAttributeElement {
|
|
35
|
+
setAttribute(name: string, value: string): void;
|
|
36
|
+
}
|
|
37
|
+
export interface NautAvatarEyeElement extends NautAvatarAttributeElement {
|
|
38
|
+
style: {
|
|
39
|
+
opacity: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
34
42
|
export interface NautAvatarElements {
|
|
35
|
-
root:
|
|
36
|
-
body:
|
|
37
|
-
eyes:
|
|
38
|
-
eyeL:
|
|
39
|
-
eyeR:
|
|
43
|
+
root: NautAvatarAttributeElement;
|
|
44
|
+
body: NautAvatarAttributeElement;
|
|
45
|
+
eyes: NautAvatarAttributeElement;
|
|
46
|
+
eyeL: NautAvatarEyeElement;
|
|
47
|
+
eyeR: NautAvatarEyeElement;
|
|
40
48
|
}
|
|
41
49
|
/**
|
|
42
50
|
* Framework-agnostic renderer: owns the animation state and writes attributes onto the
|
|
@@ -62,9 +70,13 @@ export declare class NautAvatarEngine {
|
|
|
62
70
|
private readonly face;
|
|
63
71
|
private targetFace;
|
|
64
72
|
private readonly idleState;
|
|
73
|
+
/** An expression holds one eye shape forever; idle opens the eyes between beats. */
|
|
74
|
+
private readonly eyeRest;
|
|
65
75
|
private el;
|
|
76
|
+
private readonly rendered;
|
|
66
77
|
constructor(opts?: NautAvatarOptions);
|
|
67
78
|
attach(el: NautAvatarElements): void;
|
|
79
|
+
get needsResponsiveFrameRate(): boolean;
|
|
68
80
|
setShape(shape: NautShape): void;
|
|
69
81
|
setColor(color: NautColor): void;
|
|
70
82
|
setExpression(key: NautExpression, instant?: boolean): void;
|
|
@@ -73,7 +85,11 @@ export declare class NautAvatarEngine {
|
|
|
73
85
|
settle(): void;
|
|
74
86
|
update(dt: number): void;
|
|
75
87
|
private updateIdle;
|
|
88
|
+
/** Eases the eyes back to their neutral open shape and back into the expression. */
|
|
89
|
+
private restEyes;
|
|
76
90
|
private render;
|
|
91
|
+
private writeAttribute;
|
|
92
|
+
private writeStyle;
|
|
77
93
|
}
|
|
78
94
|
/** Stable pseudo-random pick so the same id always gets the same appearance. */
|
|
79
95
|
export declare function hashSeed(input: string): number;
|
|
@@ -148,6 +148,7 @@ const NEUTRAL_HEAD = {
|
|
|
148
148
|
sy: 1,
|
|
149
149
|
};
|
|
150
150
|
const eyeKey = (param, side) => `${param}${side}`;
|
|
151
|
+
const EYE_KEYS = EYE_PARAMS.flatMap((param) => [eyeKey(param, 'L'), eyeKey(param, 'R')]);
|
|
151
152
|
function expandFace(spec) {
|
|
152
153
|
const out = {};
|
|
153
154
|
for (const [k, v] of Object.entries(spec)) {
|
|
@@ -223,6 +224,7 @@ const EXPRESSIONS = {
|
|
|
223
224
|
},
|
|
224
225
|
},
|
|
225
226
|
sleepy: {
|
|
227
|
+
fixedEyes: true,
|
|
226
228
|
face: { topCut: 0.55, botCut: 0, round: 0.6, gazeY: 0.25, headY: 3 },
|
|
227
229
|
loop: (t, o) => {
|
|
228
230
|
const s = Math.max(0, Math.sin(t * 1.2));
|
|
@@ -240,6 +242,7 @@ const EXPRESSIONS = {
|
|
|
240
242
|
},
|
|
241
243
|
},
|
|
242
244
|
error: {
|
|
245
|
+
fixedEyes: true,
|
|
243
246
|
face: { cross: 1, w: 15, h: 15, pill: 0, round: 0.2 },
|
|
244
247
|
loop: (t, o) => {
|
|
245
248
|
o.headX += Math.sin(t * 90) * Math.exp(-(t % 2.4) * 3) * 3;
|
|
@@ -252,7 +255,10 @@ const EXPRESSIONS = {
|
|
|
252
255
|
o.roll += Math.sin(t * 0.7) * 3;
|
|
253
256
|
},
|
|
254
257
|
},
|
|
255
|
-
wink: {
|
|
258
|
+
wink: {
|
|
259
|
+
fixedEyes: true,
|
|
260
|
+
face: { topCutR: 0.5, botCutR: 0.44, smileL: 0.35, round: 0.6, roll: -3 },
|
|
261
|
+
},
|
|
256
262
|
focused: {
|
|
257
263
|
face: { topCut: 0.28, botCut: 0.22, round: 0.25, w: 8.5, h: 17 },
|
|
258
264
|
loop: (t, o) => {
|
|
@@ -276,6 +282,7 @@ const EXPRESSIONS = {
|
|
|
276
282
|
},
|
|
277
283
|
smug: { face: { topCut: 0.42, topSlant: -8, round: 0.3, gazeX: 0.45, roll: -4, smile: 0.25 } },
|
|
278
284
|
talking: {
|
|
285
|
+
fixedEyes: true,
|
|
279
286
|
face: {},
|
|
280
287
|
loop: (t, o) => {
|
|
281
288
|
const s = Math.abs(Math.sin(t * 9)) * 0.55 + Math.abs(Math.sin(t * 5.3)) * 0.45;
|
|
@@ -286,6 +293,7 @@ const EXPRESSIONS = {
|
|
|
286
293
|
},
|
|
287
294
|
},
|
|
288
295
|
loading: {
|
|
296
|
+
fixedEyes: true,
|
|
289
297
|
face: { w: 7, h: 7, round: 1 },
|
|
290
298
|
loop: (t, o) => {
|
|
291
299
|
o.gazeX += Math.cos(t * 3) * 0.75;
|
|
@@ -599,6 +607,11 @@ function nodePath(nodes) {
|
|
|
599
607
|
}
|
|
600
608
|
const cloneNodes = (nodes) => nodes.map((n) => [n[0], n[1], n[2], n[3], n[4], n[5]]);
|
|
601
609
|
export const NAUT_VIEWBOX = '-126 -126 252 252';
|
|
610
|
+
/** How long a mood wears its own eye shape before the face rests, and how long it rests. */
|
|
611
|
+
const HOLD_EXPRESSION_S = 3.4;
|
|
612
|
+
const HOLD_EXPRESSION_JITTER_S = 2.6;
|
|
613
|
+
const OPEN_EYES_S = 1.8;
|
|
614
|
+
const OPEN_EYES_JITTER_S = 1.4;
|
|
602
615
|
/**
|
|
603
616
|
* Framework-agnostic renderer: owns the animation state and writes attributes onto the
|
|
604
617
|
* SVG elements it is given. Call `update(dt)` once per frame.
|
|
@@ -630,7 +643,10 @@ export class NautAvatarEngine {
|
|
|
630
643
|
curGazeY: 0,
|
|
631
644
|
curRoll: 0,
|
|
632
645
|
};
|
|
646
|
+
/** An expression holds one eye shape forever; idle opens the eyes between beats. */
|
|
647
|
+
eyeRest = { open: 0, cur: 0, next: HOLD_EXPRESSION_S };
|
|
633
648
|
el = null;
|
|
649
|
+
rendered = new Map();
|
|
634
650
|
constructor(opts = {}) {
|
|
635
651
|
this.rand = rng(opts.seed ?? 1);
|
|
636
652
|
this.shape = opts.shape ?? 'circle';
|
|
@@ -639,6 +655,7 @@ export class NautAvatarEngine {
|
|
|
639
655
|
this.idle = opts.idle ?? true;
|
|
640
656
|
this.time = this.rand() * 100;
|
|
641
657
|
this.idleState.nextBlink = 2 + this.rand() * 3;
|
|
658
|
+
this.eyeRest.next = HOLD_EXPRESSION_S + this.rand() * HOLD_EXPRESSION_JITTER_S;
|
|
642
659
|
this.nodes = cloneNodes(SHAPES[this.shape].nodes);
|
|
643
660
|
this.sphere = SHAPES[this.shape].sphere;
|
|
644
661
|
this.eyeYOff = SHAPES[this.shape].eyeY;
|
|
@@ -648,6 +665,17 @@ export class NautAvatarEngine {
|
|
|
648
665
|
}
|
|
649
666
|
attach(el) {
|
|
650
667
|
this.el = el;
|
|
668
|
+
this.rendered.clear();
|
|
669
|
+
}
|
|
670
|
+
get needsResponsiveFrameRate() {
|
|
671
|
+
const targetGazeX = this.gazeTarget?.x ?? this.idleState.gazeX;
|
|
672
|
+
const targetGazeY = this.gazeTarget?.y ?? this.idleState.gazeY;
|
|
673
|
+
return (this.gestures.length > 0 ||
|
|
674
|
+
Math.abs(this.idleState.curGazeX - targetGazeX) > 0.01 ||
|
|
675
|
+
Math.abs(this.idleState.curGazeY - targetGazeY) > 0.01 ||
|
|
676
|
+
Math.abs(this.idleState.curRoll - this.idleState.roll) > 0.02 ||
|
|
677
|
+
Math.abs(this.eyeRest.cur - this.eyeRest.open) > 0.01 ||
|
|
678
|
+
FACE_KEYS.some((key) => Math.abs(this.face[key] - this.targetFace[key]) > 0.002));
|
|
651
679
|
}
|
|
652
680
|
setShape(shape) {
|
|
653
681
|
this.shape = shape;
|
|
@@ -662,8 +690,14 @@ export class NautAvatarEngine {
|
|
|
662
690
|
if (changed || instant)
|
|
663
691
|
this.emotionTime = 0;
|
|
664
692
|
this.targetFace = { ...NEUTRAL, ...expandFace(def.face) };
|
|
665
|
-
if (instant)
|
|
693
|
+
if (changed || instant) {
|
|
694
|
+
this.eyeRest.open = 0;
|
|
695
|
+
this.eyeRest.next = HOLD_EXPRESSION_S + this.rand() * HOLD_EXPRESSION_JITTER_S;
|
|
696
|
+
}
|
|
697
|
+
if (instant) {
|
|
666
698
|
Object.assign(this.face, this.targetFace);
|
|
699
|
+
this.eyeRest.cur = 0;
|
|
700
|
+
}
|
|
667
701
|
if (def.enter && !instant && changed)
|
|
668
702
|
this.play(def.enter);
|
|
669
703
|
}
|
|
@@ -673,6 +707,8 @@ export class NautAvatarEngine {
|
|
|
673
707
|
/** Snap every tween to its target (used for static renders / reduced motion). */
|
|
674
708
|
settle() {
|
|
675
709
|
Object.assign(this.face, this.targetFace);
|
|
710
|
+
this.eyeRest.open = 0;
|
|
711
|
+
this.eyeRest.cur = 0;
|
|
676
712
|
this.nodes = cloneNodes(SHAPES[this.shape].nodes);
|
|
677
713
|
this.sphere = SHAPES[this.shape].sphere;
|
|
678
714
|
this.eyeYOff = SHAPES[this.shape].eyeY;
|
|
@@ -697,6 +733,7 @@ export class NautAvatarEngine {
|
|
|
697
733
|
if (def.loop)
|
|
698
734
|
def.loop(this.emotionTime, o);
|
|
699
735
|
this.updateIdle(dt, o);
|
|
736
|
+
this.restEyes(dt, o, def);
|
|
700
737
|
for (const g of this.gestures) {
|
|
701
738
|
g.t += dt / GESTURES[g.key].dur;
|
|
702
739
|
GESTURES[g.key].fn(clamp(g.t, 0, 1), o);
|
|
@@ -747,14 +784,34 @@ export class NautAvatarEngine {
|
|
|
747
784
|
o.gazeY += s.curGazeY;
|
|
748
785
|
o.roll += s.curRoll;
|
|
749
786
|
}
|
|
787
|
+
/** Eases the eyes back to their neutral open shape and back into the expression. */
|
|
788
|
+
restEyes(dt, o, def) {
|
|
789
|
+
const rest = this.eyeRest;
|
|
790
|
+
if (this.idle && !def.fixedEyes) {
|
|
791
|
+
rest.next -= dt;
|
|
792
|
+
if (rest.next <= 0) {
|
|
793
|
+
rest.open = rest.open > 0.5 ? 0 : 1;
|
|
794
|
+
rest.next = rest.open
|
|
795
|
+
? OPEN_EYES_S + this.rand() * OPEN_EYES_JITTER_S
|
|
796
|
+
: HOLD_EXPRESSION_S + this.rand() * HOLD_EXPRESSION_JITTER_S;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else
|
|
800
|
+
rest.open = 0;
|
|
801
|
+
rest.cur = approach(rest.cur, rest.open, 3, dt);
|
|
802
|
+
if (rest.cur < 0.002)
|
|
803
|
+
return;
|
|
804
|
+
for (const k of EYE_KEYS)
|
|
805
|
+
o[k] = lerp(o[k], NEUTRAL[k], rest.cur);
|
|
806
|
+
}
|
|
750
807
|
render(o) {
|
|
751
808
|
const el = this.el;
|
|
752
809
|
if (!el)
|
|
753
810
|
return;
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
811
|
+
this.writeAttribute('root.transform', el.root, 'transform', `translate(${o.headX.toFixed(1)} ${o.headY.toFixed(1)}) scale(${o.sx.toFixed(3)} ${o.sy.toFixed(3)})`);
|
|
812
|
+
this.writeAttribute('body.d', el.body, 'd', nodePath(this.nodes));
|
|
813
|
+
this.writeAttribute('body.fill', el.body, 'fill', NAUT_COLORS[this.color]);
|
|
814
|
+
this.writeAttribute('eyes.transform', el.eyes, 'transform', `rotate(${o.roll.toFixed(1)})`);
|
|
758
815
|
const yaw = clamp(o.gazeX, -1.3, 1.3) * 0.85 + o.spin;
|
|
759
816
|
const pitch = -clamp(o.gazeY, -1.3, 1.3) * 0.7;
|
|
760
817
|
const R = this.sphere;
|
|
@@ -766,7 +823,7 @@ export class NautAvatarEngine {
|
|
|
766
823
|
const poly = eyePolygon(p, sgn);
|
|
767
824
|
const eye = side === 'L' ? el.eyeL : el.eyeR;
|
|
768
825
|
if (!poly.length) {
|
|
769
|
-
eye.
|
|
826
|
+
this.writeAttribute(`eye${side}.d`, eye, 'd', '');
|
|
770
827
|
continue;
|
|
771
828
|
}
|
|
772
829
|
const fr = eyeFrame(sgn * o.eyeSep, o.eyeLat, yaw, pitch);
|
|
@@ -778,12 +835,24 @@ export class NautAvatarEngine {
|
|
|
778
835
|
const vx = lerp(0, fr.V[0], curve);
|
|
779
836
|
const vy = lerp(1, fr.V[1], curve);
|
|
780
837
|
const d = poly
|
|
781
|
-
.map(([lx, ly], i) => `${i ? 'L' : 'M'} ${(cx + ux * lx + vx * ly).toFixed(
|
|
838
|
+
.map(([lx, ly], i) => `${i ? 'L' : 'M'} ${(cx + ux * lx + vx * ly).toFixed(1)} ${(cy + uy * lx + vy * ly).toFixed(1)}`)
|
|
782
839
|
.join(' ');
|
|
783
|
-
eye.
|
|
784
|
-
eye.style
|
|
840
|
+
this.writeAttribute(`eye${side}.d`, eye, 'd', d + ' Z');
|
|
841
|
+
this.writeStyle(`eye${side}.opacity`, eye.style, 'opacity', fr.P[2] > 0 ? '1' : '0');
|
|
785
842
|
}
|
|
786
843
|
}
|
|
844
|
+
writeAttribute(key, element, name, value) {
|
|
845
|
+
if (this.rendered.get(key) === value)
|
|
846
|
+
return;
|
|
847
|
+
this.rendered.set(key, value);
|
|
848
|
+
element.setAttribute(name, value);
|
|
849
|
+
}
|
|
850
|
+
writeStyle(key, style, name, value) {
|
|
851
|
+
if (this.rendered.get(key) === value)
|
|
852
|
+
return;
|
|
853
|
+
this.rendered.set(key, value);
|
|
854
|
+
style[name] = value;
|
|
855
|
+
}
|
|
787
856
|
}
|
|
788
857
|
/** Stable pseudo-random pick so the same id always gets the same appearance. */
|
|
789
858
|
export function hashSeed(input) {
|
|
@@ -17,6 +17,8 @@ export interface NautAvatarProps extends Omit<React.SVGProps<SVGSVGElement>, 're
|
|
|
17
17
|
x: number;
|
|
18
18
|
y: number;
|
|
19
19
|
} | null;
|
|
20
|
+
/** Glance at a mouse that comes near the face. Ignored once `gaze` is given. */
|
|
21
|
+
followPointer?: boolean;
|
|
20
22
|
/** Any stable id — keeps a list of Nauts from blinking in unison. */
|
|
21
23
|
seed?: string | number;
|
|
22
24
|
}
|
|
@@ -4,32 +4,104 @@ import * as React from 'react';
|
|
|
4
4
|
import { useReducedMotion } from 'motion/react';
|
|
5
5
|
import { cn } from '../lib/cn.js';
|
|
6
6
|
import { NAUT_VIEWBOX, NautAvatarEngine, hashSeed, } from './naut-avatar-engine.js';
|
|
7
|
+
import { pointerGaze } from './naut-pointer-gaze.js';
|
|
7
8
|
export { NAUT_COLORS, NAUT_COLOR_KEYS, NAUT_EXPRESSIONS, NAUT_GESTURES, NAUT_SHAPES, isNautColor, isNautShape, nautAppearanceForSeed, } from './naut-avatar-engine.js';
|
|
9
|
+
/** A face can be carried across the page by something other than a mouse or a scroll. */
|
|
10
|
+
const RECT_MAX_AGE_MS = 500;
|
|
11
|
+
const IDLE_FRAME_MS = 1000 / 3;
|
|
12
|
+
const RESPONSIVE_FRAME_MS = 1000 / 20;
|
|
13
|
+
const POINTER_SETTLE_MS = 350;
|
|
14
|
+
/** Where the mouse is, shared by every face so one listener serves the page. */
|
|
15
|
+
const pointer = {
|
|
16
|
+
x: 0,
|
|
17
|
+
y: 0,
|
|
18
|
+
active: false,
|
|
19
|
+
lastMovedAt: 0,
|
|
20
|
+
/** Bumps whenever cached face rects could have gone stale. */
|
|
21
|
+
moved: 0,
|
|
22
|
+
onPointer(event) {
|
|
23
|
+
if (event.pointerType === 'touch') {
|
|
24
|
+
pointer.active = false;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
pointer.x = event.clientX;
|
|
28
|
+
pointer.y = event.clientY;
|
|
29
|
+
pointer.active = true;
|
|
30
|
+
pointer.lastMovedAt = performance.now();
|
|
31
|
+
pointer.moved++;
|
|
32
|
+
},
|
|
33
|
+
onLeave() {
|
|
34
|
+
pointer.active = false;
|
|
35
|
+
},
|
|
36
|
+
onLayout() {
|
|
37
|
+
pointer.moved++;
|
|
38
|
+
},
|
|
39
|
+
listen() {
|
|
40
|
+
window.addEventListener('pointermove', pointer.onPointer, { passive: true });
|
|
41
|
+
window.addEventListener('pointerdown', pointer.onPointer, { passive: true });
|
|
42
|
+
document.addEventListener('mouseleave', pointer.onLeave);
|
|
43
|
+
window.addEventListener('scroll', pointer.onLayout, { passive: true, capture: true });
|
|
44
|
+
window.addEventListener('resize', pointer.onLayout, { passive: true });
|
|
45
|
+
},
|
|
46
|
+
stop() {
|
|
47
|
+
window.removeEventListener('pointermove', pointer.onPointer);
|
|
48
|
+
window.removeEventListener('pointerdown', pointer.onPointer);
|
|
49
|
+
document.removeEventListener('mouseleave', pointer.onLeave);
|
|
50
|
+
window.removeEventListener('scroll', pointer.onLayout, { capture: true });
|
|
51
|
+
window.removeEventListener('resize', pointer.onLayout);
|
|
52
|
+
pointer.active = false;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
function aimAtPointer(entry, now) {
|
|
56
|
+
const svg = entry.svg;
|
|
57
|
+
if (!pointer.active || !svg) {
|
|
58
|
+
entry.engine.gazeTarget = null;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (entry.rectAt !== pointer.moved || now - entry.rectTime > RECT_MAX_AGE_MS) {
|
|
62
|
+
entry.rect = svg.getBoundingClientRect();
|
|
63
|
+
entry.rectAt = pointer.moved;
|
|
64
|
+
entry.rectTime = now;
|
|
65
|
+
}
|
|
66
|
+
entry.engine.gazeTarget = entry.rect ? pointerGaze(entry.rect, pointer.x, pointer.y) : null;
|
|
67
|
+
}
|
|
8
68
|
/** One animation frame loop drives every mounted avatar; offscreen ones sit out. */
|
|
9
69
|
const ticker = {
|
|
10
70
|
entries: new Set(),
|
|
11
71
|
frame: 0,
|
|
12
|
-
last: 0,
|
|
13
72
|
tick(now) {
|
|
14
|
-
const
|
|
15
|
-
ticker.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
73
|
+
const pointerResponsive = pointer.active && now - pointer.lastMovedAt < POINTER_SETTLE_MS;
|
|
74
|
+
for (const entry of ticker.entries) {
|
|
75
|
+
if (!entry.visible)
|
|
76
|
+
continue;
|
|
77
|
+
const responsive = (entry.follow && pointerResponsive) || entry.engine.needsResponsiveFrameRate;
|
|
78
|
+
const frameMs = responsive ? RESPONSIVE_FRAME_MS : IDLE_FRAME_MS;
|
|
79
|
+
if (now - entry.lastUpdate < frameMs)
|
|
80
|
+
continue;
|
|
81
|
+
const dt = Math.min((now - entry.lastUpdate) / 1000, 0.1);
|
|
82
|
+
entry.lastUpdate = now;
|
|
83
|
+
if (entry.follow)
|
|
84
|
+
aimAtPointer(entry, now);
|
|
85
|
+
entry.engine.update(dt);
|
|
86
|
+
}
|
|
19
87
|
ticker.frame = ticker.entries.size ? requestAnimationFrame(ticker.tick) : 0;
|
|
20
88
|
},
|
|
21
89
|
add(entry) {
|
|
90
|
+
if (!ticker.entries.size)
|
|
91
|
+
pointer.listen();
|
|
22
92
|
ticker.entries.add(entry);
|
|
23
93
|
if (!ticker.frame) {
|
|
24
|
-
ticker.last = performance.now();
|
|
25
94
|
ticker.frame = requestAnimationFrame(ticker.tick);
|
|
26
95
|
}
|
|
27
96
|
},
|
|
28
97
|
remove(entry) {
|
|
29
98
|
ticker.entries.delete(entry);
|
|
30
|
-
if (!ticker.entries.size
|
|
31
|
-
|
|
32
|
-
ticker.frame
|
|
99
|
+
if (!ticker.entries.size) {
|
|
100
|
+
pointer.stop();
|
|
101
|
+
if (ticker.frame) {
|
|
102
|
+
cancelAnimationFrame(ticker.frame);
|
|
103
|
+
ticker.frame = 0;
|
|
104
|
+
}
|
|
33
105
|
}
|
|
34
106
|
},
|
|
35
107
|
};
|
|
@@ -39,7 +111,7 @@ const seedNumber = (seed) => typeof seed === 'number' ? seed : hashSeed(seed ??
|
|
|
39
111
|
* look around. The eyes are holes, so set `--naut-avatar-eye` to the surface the
|
|
40
112
|
* avatar sits on (defaults to `--background`).
|
|
41
113
|
*/
|
|
42
|
-
export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle', color = 'ink', expression = 'neutral', idle = true, gaze = null, seed, className, style, ...props }, forwardedRef) {
|
|
114
|
+
export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle', color = 'ink', expression = 'neutral', idle = true, gaze = null, followPointer = true, seed, className, style, ...props }, forwardedRef) {
|
|
43
115
|
const reducedMotion = useReducedMotion();
|
|
44
116
|
const animated = idle && !reducedMotion;
|
|
45
117
|
const svgRef = React.useRef(null);
|
|
@@ -49,6 +121,16 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
49
121
|
const eyeLRef = React.useRef(null);
|
|
50
122
|
const eyeRRef = React.useRef(null);
|
|
51
123
|
const [engine] = React.useState(() => new NautAvatarEngine({ shape, color, expression, seed: seedNumber(seed), idle: animated }));
|
|
124
|
+
const [entry] = React.useState(() => ({
|
|
125
|
+
engine,
|
|
126
|
+
svg: null,
|
|
127
|
+
visible: true,
|
|
128
|
+
follow: false,
|
|
129
|
+
rect: null,
|
|
130
|
+
rectAt: -1,
|
|
131
|
+
rectTime: 0,
|
|
132
|
+
lastUpdate: performance.now(),
|
|
133
|
+
}));
|
|
52
134
|
React.useImperativeHandle(forwardedRef, () => ({ play: (gesture) => engine.play(gesture) }), [
|
|
53
135
|
engine,
|
|
54
136
|
]);
|
|
@@ -58,7 +140,8 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
58
140
|
engine.setExpression(expression);
|
|
59
141
|
engine.idle = animated;
|
|
60
142
|
engine.gazeTarget = gaze;
|
|
61
|
-
|
|
143
|
+
entry.follow = followPointer && !gaze && animated;
|
|
144
|
+
}, [engine, entry, shape, color, expression, animated, gaze, followPointer]);
|
|
62
145
|
React.useLayoutEffect(() => {
|
|
63
146
|
if (!rootRef.current ||
|
|
64
147
|
!bodyRef.current ||
|
|
@@ -77,14 +160,17 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
77
160
|
engine.update(0);
|
|
78
161
|
}, [engine]);
|
|
79
162
|
React.useEffect(() => {
|
|
80
|
-
if (
|
|
163
|
+
if (!animated) {
|
|
81
164
|
engine.settle();
|
|
82
165
|
engine.update(0);
|
|
83
166
|
return;
|
|
84
167
|
}
|
|
85
|
-
const entry = { engine, visible: true };
|
|
86
|
-
ticker.add(entry);
|
|
87
168
|
const svg = svgRef.current;
|
|
169
|
+
entry.svg = svg;
|
|
170
|
+
entry.rect = null;
|
|
171
|
+
entry.rectAt = -1;
|
|
172
|
+
entry.lastUpdate = performance.now();
|
|
173
|
+
ticker.add(entry);
|
|
88
174
|
const observer = svg && typeof IntersectionObserver !== 'undefined'
|
|
89
175
|
? new IntersectionObserver((records) => {
|
|
90
176
|
for (const record of records)
|
|
@@ -96,13 +182,14 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
96
182
|
return () => {
|
|
97
183
|
observer?.disconnect();
|
|
98
184
|
ticker.remove(entry);
|
|
185
|
+
entry.svg = null;
|
|
99
186
|
};
|
|
100
|
-
}, [engine,
|
|
187
|
+
}, [animated, engine, entry]);
|
|
101
188
|
React.useEffect(() => {
|
|
102
|
-
if (
|
|
189
|
+
if (animated)
|
|
103
190
|
return;
|
|
104
191
|
engine.settle();
|
|
105
192
|
engine.update(0);
|
|
106
|
-
}, [
|
|
107
|
-
return (_jsx("svg", { ref: svgRef, viewBox: NAUT_VIEWBOX, "data-slot": "naut-avatar", "data-shape": shape, "data-color": color, "data-expression": expression, className: cn('block size-full overflow-visible select-none', className), style: style, "aria-hidden": props['aria-label'] ? undefined : true, ...props, children: _jsxs("g", { ref: rootRef, children: [_jsx("path", { ref: bodyRef }), _jsxs("g", { ref: eyesRef, children: [_jsx("path", { ref: eyeLRef, fill: "var(--naut-avatar-eye, var(--background))" }), _jsx("path", { ref: eyeRRef, fill: "var(--naut-avatar-eye, var(--background))" })] })] }) }));
|
|
193
|
+
}, [animated, engine, shape, color, expression]);
|
|
194
|
+
return (_jsx("svg", { ref: svgRef, viewBox: NAUT_VIEWBOX, "data-slot": "naut-avatar", "data-shape": shape, "data-color": color, "data-expression": expression, "data-atmos-motion": animated ? 'running' : undefined, "data-atmos-motion-property": animated ? 'svg-attributes' : undefined, className: cn('block size-full overflow-visible select-none', className), style: style, "aria-hidden": props['aria-label'] ? undefined : true, ...props, children: _jsxs("g", { ref: rootRef, children: [_jsx("path", { ref: bodyRef }), _jsxs("g", { ref: eyesRef, children: [_jsx("path", { ref: eyeLRef, fill: "var(--naut-avatar-eye, var(--background))" }), _jsx("path", { ref: eyeRRef, fill: "var(--naut-avatar-eye, var(--background))" })] })] }) }));
|
|
108
195
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface FaceBox {
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Where a face should look to notice a mouse at (x, y), in the same [-1, 1] gaze the
|
|
9
|
+
* avatar takes elsewhere. Null once the pointer is out of reach, which hands the eyes
|
|
10
|
+
* back to their idle wander. Reach scales with the face, so a 16px Naut in a list only
|
|
11
|
+
* looks up for a cursor beside it while a hero-sized one watches across a column.
|
|
12
|
+
*/
|
|
13
|
+
export declare function pointerGaze(box: FaceBox, x: number, y: number): {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
} | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** How far from a face a mouse still counts as near, in face radii. */
|
|
2
|
+
const REACH = 6;
|
|
3
|
+
/** Inside this share of the reach the face gives the pointer its full attention. */
|
|
4
|
+
const FULL = 0.45;
|
|
5
|
+
/** Pointer offset, in face radii, that deflects the eyes all the way. */
|
|
6
|
+
const SPAN = 3;
|
|
7
|
+
/** A glance, not a lock: the eyes stop short of their extreme. */
|
|
8
|
+
const WEIGHT = 0.85;
|
|
9
|
+
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
10
|
+
const smoothstep = (v) => v * v * (3 - 2 * v);
|
|
11
|
+
const deflect = (offset, span) => Math.max(-1, Math.min(1, offset / span));
|
|
12
|
+
/**
|
|
13
|
+
* Where a face should look to notice a mouse at (x, y), in the same [-1, 1] gaze the
|
|
14
|
+
* avatar takes elsewhere. Null once the pointer is out of reach, which hands the eyes
|
|
15
|
+
* back to their idle wander. Reach scales with the face, so a 16px Naut in a list only
|
|
16
|
+
* looks up for a cursor beside it while a hero-sized one watches across a column.
|
|
17
|
+
*/
|
|
18
|
+
export function pointerGaze(box, x, y) {
|
|
19
|
+
if (!box.width || !box.height)
|
|
20
|
+
return null;
|
|
21
|
+
const radius = Math.max(box.width, box.height) / 2;
|
|
22
|
+
const dx = x - (box.left + box.width / 2);
|
|
23
|
+
const dy = y - (box.top + box.height / 2);
|
|
24
|
+
const reach = radius * REACH;
|
|
25
|
+
const distance = Math.hypot(dx, dy);
|
|
26
|
+
if (distance > reach)
|
|
27
|
+
return null;
|
|
28
|
+
const attention = WEIGHT * smoothstep(clamp01((reach - distance) / (reach * (1 - FULL))));
|
|
29
|
+
const span = radius * SPAN;
|
|
30
|
+
return { x: deflect(dx, span) * attention, y: deflect(dy, span) * attention };
|
|
31
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { NautColor, NautExpression, NautGesture, NautShape } from './naut-avatar-engine.js';
|
|
3
|
+
export interface NautSceneFace {
|
|
4
|
+
shape: NautShape;
|
|
5
|
+
color: NautColor;
|
|
6
|
+
expression: NautExpression;
|
|
7
|
+
/** Share of the scene's height the face stands as; smaller reads as further off. */
|
|
8
|
+
size: number;
|
|
9
|
+
/** Where the face floats, as a share of the scene box from its top left. */
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
/** Seconds one drift takes; slower reads as further off. */
|
|
13
|
+
drift: number;
|
|
14
|
+
}
|
|
15
|
+
export interface NautSceneDef {
|
|
16
|
+
faces: readonly NautSceneFace[];
|
|
17
|
+
/** Run across the whole scene on a loop, one face after the next. */
|
|
18
|
+
gesture: NautGesture;
|
|
19
|
+
/** Seconds between one run of the gesture and the next. */
|
|
20
|
+
interval: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The illustrations a page reaches for, drawn from the same faces the product
|
|
24
|
+
* uses everywhere else — so an empty state, a 404 and a chat header are all
|
|
25
|
+
* recognisably the same Nauts rather than a separate cast of stock art.
|
|
26
|
+
*/
|
|
27
|
+
export declare const NAUT_SCENES: {
|
|
28
|
+
readonly adrift: {
|
|
29
|
+
readonly faces: readonly [{
|
|
30
|
+
readonly shape: "circle";
|
|
31
|
+
readonly color: "sun";
|
|
32
|
+
readonly expression: "confused";
|
|
33
|
+
readonly size: 0.62;
|
|
34
|
+
readonly x: 0.4;
|
|
35
|
+
readonly y: 0.4;
|
|
36
|
+
readonly drift: 7;
|
|
37
|
+
}, {
|
|
38
|
+
readonly shape: "triangle";
|
|
39
|
+
readonly color: "mint";
|
|
40
|
+
readonly expression: "surprised";
|
|
41
|
+
readonly size: 0.3;
|
|
42
|
+
readonly x: 0.76;
|
|
43
|
+
readonly y: 0.68;
|
|
44
|
+
readonly drift: 9;
|
|
45
|
+
}, {
|
|
46
|
+
readonly shape: "diamond";
|
|
47
|
+
readonly color: "lavender";
|
|
48
|
+
readonly expression: "sleepy";
|
|
49
|
+
readonly size: 0.2;
|
|
50
|
+
readonly x: 0.105;
|
|
51
|
+
readonly y: 0.72;
|
|
52
|
+
readonly drift: 11;
|
|
53
|
+
}];
|
|
54
|
+
readonly gesture: "lookAround";
|
|
55
|
+
readonly interval: 6;
|
|
56
|
+
};
|
|
57
|
+
readonly greeting: {
|
|
58
|
+
readonly faces: readonly [{
|
|
59
|
+
readonly shape: "circle";
|
|
60
|
+
readonly color: "sun";
|
|
61
|
+
readonly expression: "happy";
|
|
62
|
+
readonly size: 0.56;
|
|
63
|
+
readonly x: 0.4;
|
|
64
|
+
readonly y: 0.42;
|
|
65
|
+
readonly drift: 6;
|
|
66
|
+
}, {
|
|
67
|
+
readonly shape: "hexagon";
|
|
68
|
+
readonly color: "sky";
|
|
69
|
+
readonly expression: "excited";
|
|
70
|
+
readonly size: 0.34;
|
|
71
|
+
readonly x: 0.735;
|
|
72
|
+
readonly y: 0.56;
|
|
73
|
+
readonly drift: 7.5;
|
|
74
|
+
}, {
|
|
75
|
+
readonly shape: "squircle";
|
|
76
|
+
readonly color: "coral";
|
|
77
|
+
readonly expression: "happy";
|
|
78
|
+
readonly size: 0.22;
|
|
79
|
+
readonly x: 0.1;
|
|
80
|
+
readonly y: 0.62;
|
|
81
|
+
readonly drift: 9;
|
|
82
|
+
}];
|
|
83
|
+
readonly gesture: "bounce";
|
|
84
|
+
readonly interval: 5;
|
|
85
|
+
};
|
|
86
|
+
readonly crew: {
|
|
87
|
+
readonly faces: readonly [{
|
|
88
|
+
readonly shape: "squircle";
|
|
89
|
+
readonly color: "coral";
|
|
90
|
+
readonly expression: "happy";
|
|
91
|
+
readonly size: 0.38;
|
|
92
|
+
readonly x: 0.17;
|
|
93
|
+
readonly y: 0.56;
|
|
94
|
+
readonly drift: 8;
|
|
95
|
+
}, {
|
|
96
|
+
readonly shape: "circle";
|
|
97
|
+
readonly color: "sun";
|
|
98
|
+
readonly expression: "focused";
|
|
99
|
+
readonly size: 0.52;
|
|
100
|
+
readonly x: 0.5;
|
|
101
|
+
readonly y: 0.46;
|
|
102
|
+
readonly drift: 6.5;
|
|
103
|
+
}, {
|
|
104
|
+
readonly shape: "hexagon";
|
|
105
|
+
readonly color: "sky";
|
|
106
|
+
readonly expression: "thinking";
|
|
107
|
+
readonly size: 0.38;
|
|
108
|
+
readonly x: 0.83;
|
|
109
|
+
readonly y: 0.56;
|
|
110
|
+
readonly drift: 9.5;
|
|
111
|
+
}];
|
|
112
|
+
readonly gesture: "nod";
|
|
113
|
+
readonly interval: 6;
|
|
114
|
+
};
|
|
115
|
+
readonly alone: {
|
|
116
|
+
readonly faces: readonly [{
|
|
117
|
+
readonly shape: "circle";
|
|
118
|
+
readonly color: "sun";
|
|
119
|
+
readonly expression: "neutral";
|
|
120
|
+
readonly size: 0.86;
|
|
121
|
+
readonly x: 0.5;
|
|
122
|
+
readonly y: 0.5;
|
|
123
|
+
readonly drift: 7;
|
|
124
|
+
}];
|
|
125
|
+
readonly gesture: "blink";
|
|
126
|
+
readonly interval: 7;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
export type NautSceneName = keyof typeof NAUT_SCENES;
|
|
130
|
+
export interface NautSceneProps {
|
|
131
|
+
scene: NautSceneName;
|
|
132
|
+
/** Enables ambient drift and gestures. */
|
|
133
|
+
animated?: boolean;
|
|
134
|
+
/** Sizes the scene; the faces lay themselves out inside whatever box this gives. */
|
|
135
|
+
className?: string;
|
|
136
|
+
}
|
|
137
|
+
/** A small animated illustration built from Naut faces. Decorative — it carries no label. */
|
|
138
|
+
export declare function NautScene({ scene, animated, className, }: NautSceneProps): React.ReactElement;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { motion, useReducedMotion } from 'motion/react';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
import { NautAvatar } from './naut-avatar.js';
|
|
7
|
+
/**
|
|
8
|
+
* The illustrations a page reaches for, drawn from the same faces the product
|
|
9
|
+
* uses everywhere else — so an empty state, a 404 and a chat header are all
|
|
10
|
+
* recognisably the same Nauts rather than a separate cast of stock art.
|
|
11
|
+
*/
|
|
12
|
+
export const NAUT_SCENES = {
|
|
13
|
+
adrift: {
|
|
14
|
+
faces: [
|
|
15
|
+
{
|
|
16
|
+
shape: 'circle',
|
|
17
|
+
color: 'sun',
|
|
18
|
+
expression: 'confused',
|
|
19
|
+
size: 0.62,
|
|
20
|
+
x: 0.4,
|
|
21
|
+
y: 0.4,
|
|
22
|
+
drift: 7,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
shape: 'triangle',
|
|
26
|
+
color: 'mint',
|
|
27
|
+
expression: 'surprised',
|
|
28
|
+
size: 0.3,
|
|
29
|
+
x: 0.76,
|
|
30
|
+
y: 0.68,
|
|
31
|
+
drift: 9,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
shape: 'diamond',
|
|
35
|
+
color: 'lavender',
|
|
36
|
+
expression: 'sleepy',
|
|
37
|
+
size: 0.2,
|
|
38
|
+
x: 0.105,
|
|
39
|
+
y: 0.72,
|
|
40
|
+
drift: 11,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
gesture: 'lookAround',
|
|
44
|
+
interval: 6,
|
|
45
|
+
},
|
|
46
|
+
greeting: {
|
|
47
|
+
faces: [
|
|
48
|
+
{ shape: 'circle', color: 'sun', expression: 'happy', size: 0.56, x: 0.4, y: 0.42, drift: 6 },
|
|
49
|
+
{
|
|
50
|
+
shape: 'hexagon',
|
|
51
|
+
color: 'sky',
|
|
52
|
+
expression: 'excited',
|
|
53
|
+
size: 0.34,
|
|
54
|
+
x: 0.735,
|
|
55
|
+
y: 0.56,
|
|
56
|
+
drift: 7.5,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
shape: 'squircle',
|
|
60
|
+
color: 'coral',
|
|
61
|
+
expression: 'happy',
|
|
62
|
+
size: 0.22,
|
|
63
|
+
x: 0.1,
|
|
64
|
+
y: 0.62,
|
|
65
|
+
drift: 9,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
gesture: 'bounce',
|
|
69
|
+
interval: 5,
|
|
70
|
+
},
|
|
71
|
+
crew: {
|
|
72
|
+
faces: [
|
|
73
|
+
{
|
|
74
|
+
shape: 'squircle',
|
|
75
|
+
color: 'coral',
|
|
76
|
+
expression: 'happy',
|
|
77
|
+
size: 0.38,
|
|
78
|
+
x: 0.17,
|
|
79
|
+
y: 0.56,
|
|
80
|
+
drift: 8,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
shape: 'circle',
|
|
84
|
+
color: 'sun',
|
|
85
|
+
expression: 'focused',
|
|
86
|
+
size: 0.52,
|
|
87
|
+
x: 0.5,
|
|
88
|
+
y: 0.46,
|
|
89
|
+
drift: 6.5,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
shape: 'hexagon',
|
|
93
|
+
color: 'sky',
|
|
94
|
+
expression: 'thinking',
|
|
95
|
+
size: 0.38,
|
|
96
|
+
x: 0.83,
|
|
97
|
+
y: 0.56,
|
|
98
|
+
drift: 9.5,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
gesture: 'nod',
|
|
102
|
+
interval: 6,
|
|
103
|
+
},
|
|
104
|
+
alone: {
|
|
105
|
+
faces: [
|
|
106
|
+
{
|
|
107
|
+
shape: 'circle',
|
|
108
|
+
color: 'sun',
|
|
109
|
+
expression: 'neutral',
|
|
110
|
+
size: 0.86,
|
|
111
|
+
x: 0.5,
|
|
112
|
+
y: 0.5,
|
|
113
|
+
drift: 7,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
gesture: 'blink',
|
|
117
|
+
interval: 7,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
/** Milliseconds between one face taking up the gesture and the next. */
|
|
121
|
+
const GESTURE_STAGGER_MS = 180;
|
|
122
|
+
/** Share of its own height a face wanders up and down. */
|
|
123
|
+
const DRIFT_TRAVEL = 9;
|
|
124
|
+
/** Degrees a face tips as it drifts. */
|
|
125
|
+
const DRIFT_TILT = 4;
|
|
126
|
+
/** Seconds between the starts of neighbouring drifts, so nothing moves in unison. */
|
|
127
|
+
const DRIFT_OFFSET = 0.7;
|
|
128
|
+
/** A small animated illustration built from Naut faces. Decorative — it carries no label. */
|
|
129
|
+
export function NautScene({ scene, animated = true, className, }) {
|
|
130
|
+
const reducedMotion = useReducedMotion();
|
|
131
|
+
const active = animated && !reducedMotion;
|
|
132
|
+
const { faces, gesture, interval } = NAUT_SCENES[scene];
|
|
133
|
+
const handles = React.useRef([]);
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
if (!active)
|
|
136
|
+
return;
|
|
137
|
+
const pending = [];
|
|
138
|
+
const run = () => {
|
|
139
|
+
handles.current.forEach((handle, index) => {
|
|
140
|
+
pending.push(window.setTimeout(() => handle?.play(gesture), index * GESTURE_STAGGER_MS));
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const id = window.setInterval(run, interval * 1000);
|
|
144
|
+
return () => {
|
|
145
|
+
window.clearInterval(id);
|
|
146
|
+
for (const timeout of pending)
|
|
147
|
+
window.clearTimeout(timeout);
|
|
148
|
+
};
|
|
149
|
+
}, [active, gesture, interval]);
|
|
150
|
+
return (_jsx("div", { "aria-hidden": true, className: cn('flex items-center justify-center', className), children: _jsx("div", { className: "relative aspect-3/2 h-full", children: faces.map((face, index) => (_jsx("span", { className: "absolute block aspect-square", style: {
|
|
151
|
+
left: `${face.x * 100}%`,
|
|
152
|
+
top: `${face.y * 100}%`,
|
|
153
|
+
height: `${face.size * 100}%`,
|
|
154
|
+
transform: 'translate(-50%, -50%)',
|
|
155
|
+
}, children: _jsx(motion.span, { className: "block size-full", animate: !active
|
|
156
|
+
? undefined
|
|
157
|
+
: {
|
|
158
|
+
y: [`-${DRIFT_TRAVEL}%`, `${DRIFT_TRAVEL}%`],
|
|
159
|
+
rotate: [-DRIFT_TILT, DRIFT_TILT],
|
|
160
|
+
}, transition: {
|
|
161
|
+
duration: face.drift,
|
|
162
|
+
delay: index * DRIFT_OFFSET,
|
|
163
|
+
repeat: Infinity,
|
|
164
|
+
repeatType: 'mirror',
|
|
165
|
+
ease: 'easeInOut',
|
|
166
|
+
}, children: _jsx(NautAvatar, { ref: (handle) => {
|
|
167
|
+
handles.current[index] = handle;
|
|
168
|
+
}, shape: face.shape, color: face.color, expression: face.expression, idle: active, seed: `${scene}-${index}` }) }) }, `${face.shape}-${face.color}-${index}`))) }) }));
|
|
169
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface ShimmerTextProps extends Omit<HTMLMotionProps<'span'>, 'children'> {
|
|
2
|
+
export interface ShimmerTextProps extends Omit<React.ComponentPropsWithoutRef<'span'>, 'children'> {
|
|
4
3
|
children: string | number;
|
|
4
|
+
animate?: boolean;
|
|
5
5
|
duration?: number;
|
|
6
6
|
spread?: number;
|
|
7
7
|
}
|
|
8
|
-
export declare const ShimmerText: React.NamedExoticComponent<
|
|
8
|
+
export declare const ShimmerText: React.NamedExoticComponent<ShimmerTextProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -1,23 +1,59 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useMediaQuery } from '../hooks/use-media-query.js';
|
|
5
5
|
import { cn } from '../lib/cn.js';
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const SHIMMER_FRAME_MS = 1000 / 8;
|
|
7
|
+
const shimmerEntries = new Set();
|
|
8
|
+
let shimmerTimer = 0;
|
|
9
|
+
function paintShimmers() {
|
|
10
|
+
const now = performance.now();
|
|
11
|
+
for (const entry of shimmerEntries) {
|
|
12
|
+
const progress = ((now - entry.startedAt) % entry.durationMs) / entry.durationMs;
|
|
13
|
+
entry.element.style.backgroundPosition = `${100 - progress * 100}% center, 0 center`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function startShimmer(entry) {
|
|
17
|
+
shimmerEntries.add(entry);
|
|
18
|
+
paintShimmers();
|
|
19
|
+
if (!shimmerTimer)
|
|
20
|
+
shimmerTimer = window.setInterval(paintShimmers, SHIMMER_FRAME_MS);
|
|
21
|
+
}
|
|
22
|
+
function stopShimmer(entry) {
|
|
23
|
+
shimmerEntries.delete(entry);
|
|
24
|
+
if (shimmerEntries.size || !shimmerTimer)
|
|
25
|
+
return;
|
|
26
|
+
window.clearInterval(shimmerTimer);
|
|
27
|
+
shimmerTimer = 0;
|
|
28
|
+
}
|
|
29
|
+
const ShimmerTextComponent = React.forwardRef(function ShimmerText({ children, className, duration = 2, spread = 2, style, animate = true, ...props }, forwardedRef) {
|
|
30
|
+
const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
31
|
+
const active = animate !== false && !reducedMotion;
|
|
32
|
+
const localRef = React.useRef(null);
|
|
33
|
+
const setRef = React.useCallback((node) => {
|
|
34
|
+
localRef.current = node;
|
|
35
|
+
if (typeof forwardedRef === 'function')
|
|
36
|
+
forwardedRef(node);
|
|
37
|
+
else if (forwardedRef)
|
|
38
|
+
forwardedRef.current = node;
|
|
39
|
+
}, [forwardedRef]);
|
|
8
40
|
const dynamicSpread = React.useMemo(() => `${String(children).length * spread}px`, [children, spread]);
|
|
9
|
-
const backgroundImage =
|
|
10
|
-
? 'linear-gradient(var(--base-color), var(--base-color))'
|
|
11
|
-
: '
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
duration,
|
|
19
|
-
|
|
20
|
-
}
|
|
41
|
+
const backgroundImage = active
|
|
42
|
+
? 'var(--bg), linear-gradient(var(--base-color), var(--base-color))'
|
|
43
|
+
: 'linear-gradient(var(--base-color), var(--base-color))';
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
const element = localRef.current;
|
|
46
|
+
if (!active || !element)
|
|
47
|
+
return;
|
|
48
|
+
const entry = {
|
|
49
|
+
element,
|
|
50
|
+
durationMs: Math.max(duration * 1000, SHIMMER_FRAME_MS),
|
|
51
|
+
startedAt: performance.now(),
|
|
52
|
+
};
|
|
53
|
+
startShimmer(entry);
|
|
54
|
+
return () => stopShimmer(entry);
|
|
55
|
+
}, [active, duration, children]);
|
|
56
|
+
return (_jsx("span", { ref: setRef, "data-slot": "shimmer-text", "data-atmos-motion": active ? 'running' : undefined, "data-atmos-motion-property": active ? 'background-position' : undefined, className: cn('relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent contain-paint', '[--base-color:color-mix(in_oklch,var(--foreground)_30%,transparent)] [--base-gradient-color:var(--foreground)]', '[background-repeat:no-repeat,padding-box]', '[--bg:linear-gradient(90deg,transparent_calc(50%-var(--spread)),var(--base-gradient-color),transparent_calc(50%+var(--spread)))]', className), style: {
|
|
21
57
|
'--spread': dynamicSpread,
|
|
22
58
|
backgroundImage,
|
|
23
59
|
...style,
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { TemplateChipInput, type TemplateChipInputProps, type TemplateChipInputH
|
|
|
24
24
|
export { RunStatusBadge, type RunStatus, type RunStatusBadgeProps, } from './components/run-status-badge.js';
|
|
25
25
|
export { ConfidenceMeter, confidenceLevel, type ConfidenceLevel, type ConfidenceMeterProps, } from './components/confidence-meter.js';
|
|
26
26
|
export { DateTile, type DateTileProps } from './components/date-tile.js';
|
|
27
|
+
export { DiffStatLabel, type DiffStatLabelProps } from './components/diff-stat-label.js';
|
|
28
|
+
export { compactFormatter } from './components/diff-stat-format.js';
|
|
27
29
|
export { MetricBreakdown, MetricGrid, MetricPanel, MetricSectionHeading, MetricTile, type MetricBreakdownItem, type MetricBreakdownProps, type MetricGridProps, type MetricPanelProps, type MetricSectionHeadingProps, type MetricTileProps, type MetricTone, } from './components/metric-grid.js';
|
|
28
30
|
export { resolveDateTimeFormat, type FormatDateTime } from './lib/date-time-format.js';
|
|
29
31
|
export { ProvenanceChip, type ProvenanceChipProps, type ProvenanceMethod, } from './components/provenance-chip.js';
|
|
@@ -67,6 +69,7 @@ export { SortableList, SortableItem, SortableRow, DragHandle, DragPreview, useSo
|
|
|
67
69
|
export { VoiceLine, type VoiceLineProps, type VoiceLineState } from './components/voice-line.js';
|
|
68
70
|
export { ShimmerText, type ShimmerTextProps } from './components/shimmer-text.js';
|
|
69
71
|
export { NautAvatar, NAUT_COLORS, NAUT_COLOR_KEYS, NAUT_EXPRESSIONS, NAUT_GESTURES, NAUT_SHAPES, isNautColor, isNautShape, nautAppearanceForSeed, type NautAppearance, type NautAvatarHandle, type NautAvatarProps, type NautColor, type NautExpression, type NautGesture, type NautShape, } from './components/naut-avatar.js';
|
|
72
|
+
export { NautScene, NAUT_SCENES, type NautSceneDef, type NautSceneFace, type NautSceneName, type NautSceneProps, } from './components/naut-scene.js';
|
|
70
73
|
export { TranscriptionOverlay, type TranscriptionOverlayLabels, type TranscriptionOverlayProps, type TranscriptionOverlaySelector, type TranscriptionOverlayState, } from './components/transcription-overlay.js';
|
|
71
74
|
export { TimestampedTranscript, type TimestampedTranscriptLabels, type TimestampedTranscriptProps, type TimestampedTranscriptUtterance, type TimestampedTranscriptWord, } from './components/timestamped-transcript.js';
|
|
72
75
|
export { MicrophonePriority, type MicrophonePriorityItem, type MicrophonePriorityLabels, type MicrophonePriorityProps, } from './components/microphone-priority.js';
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,8 @@ export { TemplateChipInput, } from './components/template-chip-input.js';
|
|
|
24
24
|
export { RunStatusBadge, } from './components/run-status-badge.js';
|
|
25
25
|
export { ConfidenceMeter, confidenceLevel, } from './components/confidence-meter.js';
|
|
26
26
|
export { DateTile } from './components/date-tile.js';
|
|
27
|
+
export { DiffStatLabel } from './components/diff-stat-label.js';
|
|
28
|
+
export { compactFormatter } from './components/diff-stat-format.js';
|
|
27
29
|
export { MetricBreakdown, MetricGrid, MetricPanel, MetricSectionHeading, MetricTile, } from './components/metric-grid.js';
|
|
28
30
|
export { resolveDateTimeFormat } from './lib/date-time-format.js';
|
|
29
31
|
export { ProvenanceChip, } from './components/provenance-chip.js';
|
|
@@ -67,6 +69,7 @@ export { SortableList, SortableItem, SortableRow, DragHandle, DragPreview, useSo
|
|
|
67
69
|
export { VoiceLine } from './components/voice-line.js';
|
|
68
70
|
export { ShimmerText } from './components/shimmer-text.js';
|
|
69
71
|
export { NautAvatar, NAUT_COLORS, NAUT_COLOR_KEYS, NAUT_EXPRESSIONS, NAUT_GESTURES, NAUT_SHAPES, isNautColor, isNautShape, nautAppearanceForSeed, } from './components/naut-avatar.js';
|
|
72
|
+
export { NautScene, NAUT_SCENES, } from './components/naut-scene.js';
|
|
70
73
|
export { TranscriptionOverlay, } from './components/transcription-overlay.js';
|
|
71
74
|
export { TimestampedTranscript, } from './components/timestamped-transcript.js';
|
|
72
75
|
export { MicrophonePriority, } from './components/microphone-priority.js';
|
package/dist/mcp-app/vite.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { type UserConfig } from 'vite';
|
|
1
|
+
import { type Plugin, type UserConfig } from 'vite';
|
|
2
2
|
export interface AtmosMcpAppViteOptions {
|
|
3
3
|
root: string;
|
|
4
4
|
entry?: string;
|
|
5
5
|
outDir?: string;
|
|
6
6
|
}
|
|
7
7
|
export declare function atmosMcpAppViteConfig({ root, entry, outDir, }: AtmosMcpAppViteOptions): UserConfig;
|
|
8
|
+
export declare function atmosMcpAppSourceMetadata(root: string): Plugin;
|
|
9
|
+
export declare function annotateMcpAppReactComponents(code: string, filename: string, sourcePath: string): string;
|
package/dist/mcp-app/vite.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import tailwindcss from '@tailwindcss/postcss';
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import ts from 'typescript';
|
|
4
5
|
import { defineConfig } from 'vite';
|
|
6
|
+
const MCP_APP_SOURCE_METADATA_PROPERTY = '__atmosMcpSource';
|
|
7
|
+
const REACT_COMPONENT_FILE = /\.[jt]sx$/i;
|
|
8
|
+
const NON_PRODUCTION_COMPONENT_FILE = /\.(?:stories|test|spec)\.[cm]?[jt]sx?$/i;
|
|
5
9
|
export function atmosMcpAppViteConfig({ root, entry = 'src/main.tsx', outDir = '../dist', }) {
|
|
6
10
|
return defineConfig({
|
|
7
11
|
root,
|
|
8
12
|
publicDir: false,
|
|
9
|
-
plugins: [react()],
|
|
13
|
+
plugins: [atmosMcpAppSourceMetadata(root), react()],
|
|
10
14
|
define: { 'process.env.NODE_ENV': JSON.stringify('production') },
|
|
11
15
|
css: { postcss: { plugins: [tailwindcss()] } },
|
|
12
16
|
resolve: { dedupe: ['react', 'react-dom', 'react/jsx-runtime'] },
|
|
@@ -28,3 +32,104 @@ export function atmosMcpAppViteConfig({ root, entry = 'src/main.tsx', outDir = '
|
|
|
28
32
|
},
|
|
29
33
|
});
|
|
30
34
|
}
|
|
35
|
+
export function atmosMcpAppSourceMetadata(root) {
|
|
36
|
+
const appRoot = normalizePath(path.resolve(root));
|
|
37
|
+
return {
|
|
38
|
+
name: 'atmos-mcp-app-source-metadata',
|
|
39
|
+
enforce: 'pre',
|
|
40
|
+
transform(code, id) {
|
|
41
|
+
const file = normalizePath(id.split('?')[0] ?? id);
|
|
42
|
+
if (!shouldAnnotateComponentFile(file, appRoot))
|
|
43
|
+
return null;
|
|
44
|
+
const sourcePath = normalizePath(path.relative(appRoot, file));
|
|
45
|
+
if (!isSafeSourcePath(sourcePath))
|
|
46
|
+
return null;
|
|
47
|
+
const transformed = annotateMcpAppReactComponents(code, file, sourcePath);
|
|
48
|
+
return transformed === code ? null : { code: transformed, map: null };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function annotateMcpAppReactComponents(code, filename, sourcePath) {
|
|
53
|
+
const sourceFile = ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true, filename.toLowerCase().endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.JSX);
|
|
54
|
+
const componentNames = topLevelComponentNames(sourceFile);
|
|
55
|
+
if (componentNames.length === 0)
|
|
56
|
+
return code;
|
|
57
|
+
const assignments = componentNames.map((componentName) => sourceMetadataAssignment(componentName, sourcePath));
|
|
58
|
+
return `${code.trimEnd()}\n\n${assignments.join('\n')}\n`;
|
|
59
|
+
}
|
|
60
|
+
function topLevelComponentNames(sourceFile) {
|
|
61
|
+
const names = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const statement of sourceFile.statements) {
|
|
64
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
65
|
+
addComponentName(names, seen, statement.name.text);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!ts.isVariableStatement(statement))
|
|
69
|
+
continue;
|
|
70
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
71
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
72
|
+
declaration.initializer &&
|
|
73
|
+
isReactComponentInitializer(declaration.initializer)) {
|
|
74
|
+
addComponentName(names, seen, declaration.name.text);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return names;
|
|
79
|
+
}
|
|
80
|
+
function addComponentName(names, seen, name) {
|
|
81
|
+
if (!/^[A-Z][A-Za-z0-9_]*$/.test(name) || seen.has(name))
|
|
82
|
+
return;
|
|
83
|
+
seen.add(name);
|
|
84
|
+
names.push(name);
|
|
85
|
+
}
|
|
86
|
+
function isReactComponentInitializer(expression) {
|
|
87
|
+
const value = unwrapExpression(expression);
|
|
88
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value))
|
|
89
|
+
return true;
|
|
90
|
+
if (!ts.isCallExpression(value))
|
|
91
|
+
return false;
|
|
92
|
+
const callee = dottedName(value.expression);
|
|
93
|
+
return (callee === 'memo' ||
|
|
94
|
+
callee === 'React.memo' ||
|
|
95
|
+
callee === 'forwardRef' ||
|
|
96
|
+
callee === 'React.forwardRef');
|
|
97
|
+
}
|
|
98
|
+
function unwrapExpression(expression) {
|
|
99
|
+
let value = expression;
|
|
100
|
+
while (ts.isParenthesizedExpression(value) ||
|
|
101
|
+
ts.isAsExpression(value) ||
|
|
102
|
+
ts.isTypeAssertionExpression(value) ||
|
|
103
|
+
ts.isSatisfiesExpression(value) ||
|
|
104
|
+
ts.isNonNullExpression(value)) {
|
|
105
|
+
value = value.expression;
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function dottedName(expression) {
|
|
110
|
+
if (ts.isIdentifier(expression))
|
|
111
|
+
return expression.text;
|
|
112
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
113
|
+
return undefined;
|
|
114
|
+
const parent = dottedName(expression.expression);
|
|
115
|
+
return parent ? `${parent}.${expression.name.text}` : expression.name.text;
|
|
116
|
+
}
|
|
117
|
+
function sourceMetadataAssignment(componentName, sourcePath) {
|
|
118
|
+
const metadata = JSON.stringify({ sourcePath, componentName });
|
|
119
|
+
return `Object.defineProperty(${componentName}, ${JSON.stringify(MCP_APP_SOURCE_METADATA_PROPERTY)}, { value: ${metadata} });`;
|
|
120
|
+
}
|
|
121
|
+
function shouldAnnotateComponentFile(file, appRoot) {
|
|
122
|
+
return (file.startsWith(`${appRoot}/`) &&
|
|
123
|
+
REACT_COMPONENT_FILE.test(file) &&
|
|
124
|
+
!NON_PRODUCTION_COMPONENT_FILE.test(file) &&
|
|
125
|
+
!file.includes('/node_modules/'));
|
|
126
|
+
}
|
|
127
|
+
function isSafeSourcePath(value) {
|
|
128
|
+
return (Boolean(value) &&
|
|
129
|
+
value.length <= 512 &&
|
|
130
|
+
!value.startsWith('/') &&
|
|
131
|
+
!value.split('/').includes('..'));
|
|
132
|
+
}
|
|
133
|
+
function normalizePath(value) {
|
|
134
|
+
return value.replace(/\\/g, '/');
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atmos.build/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "The complete atmOS React component system and MCP App authoring helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"types": "./dist/lib/cn.d.ts",
|
|
33
33
|
"import": "./dist/lib/cn.js"
|
|
34
34
|
},
|
|
35
|
+
"./naut-avatar-engine": {
|
|
36
|
+
"types": "./dist/components/naut-avatar-engine.d.ts",
|
|
37
|
+
"import": "./dist/components/naut-avatar-engine.js"
|
|
38
|
+
},
|
|
35
39
|
"./view-query": {
|
|
36
40
|
"types": "./dist/lib/view-query/index.d.ts",
|
|
37
41
|
"import": "./dist/lib/view-query/index.js"
|
|
@@ -83,6 +87,7 @@
|
|
|
83
87
|
"tailwind-merge": "^3.6.0",
|
|
84
88
|
"tailwindcss": "^4.3.2",
|
|
85
89
|
"tw-animate-css": "^1.4.0",
|
|
90
|
+
"typescript": "^5.7.0",
|
|
86
91
|
"use-intl": "^4.13.2",
|
|
87
92
|
"vite": "8.1.4",
|
|
88
93
|
"yjs": "^13.6.31"
|