@atmos.build/ui 0.1.1 → 0.1.3
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/inline-alert.d.ts +4 -4
- package/dist/components/inline-alert.js +3 -2
- package/dist/components/metric-grid.d.ts +3 -1
- package/dist/components/metric-grid.js +3 -2
- package/dist/components/naut-avatar-engine.d.ts +17 -5
- package/dist/components/naut-avatar-engine.js +32 -8
- package/dist/components/naut-avatar.js +25 -14
- package/dist/components/naut-scene.d.ts +3 -1
- package/dist/components/naut-scene.js +6 -5
- package/dist/components/shimmer-text.d.ts +3 -3
- package/dist/components/shimmer-text.js +51 -15
- package/dist/components/spec-strip.d.ts +5 -0
- package/dist/components/spec-strip.js +3 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +5 -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
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
export type InlineAlertTone = 'error' | 'warning' | 'info';
|
|
3
|
-
export interface InlineAlertProps {
|
|
4
|
+
export interface InlineAlertProps extends Omit<React.ComponentProps<'div'>, 'children'> {
|
|
4
5
|
tone?: InlineAlertTone;
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
onDismiss?: () => void;
|
|
7
8
|
dismissLabel?: string;
|
|
8
|
-
className?: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function InlineAlert({ tone, children, onDismiss, dismissLabel, className, }: InlineAlertProps):
|
|
11
|
-
export declare function InlineNotice({ tone, children, className, }: Pick<InlineAlertProps, 'tone' | 'children' | 'className'>):
|
|
10
|
+
export declare function InlineAlert({ tone, children, onDismiss, dismissLabel, className, ...props }: InlineAlertProps): React.JSX.Element;
|
|
11
|
+
export declare function InlineNotice({ tone, children, className, }: Pick<InlineAlertProps, 'tone' | 'children' | 'className'>): React.JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
3
4
|
import { AlertTriangle, Info, X } from 'lucide-react';
|
|
4
5
|
import { cn } from '../lib/cn.js';
|
|
5
6
|
import { Button } from './button.js';
|
|
@@ -8,9 +9,9 @@ const toneClass = {
|
|
|
8
9
|
warning: 'border-warning/30 bg-warning/15 text-warning',
|
|
9
10
|
info: 'border-info/30 bg-info/15 text-info',
|
|
10
11
|
};
|
|
11
|
-
export function InlineAlert({ tone = 'error', children, onDismiss, dismissLabel, className, }) {
|
|
12
|
+
export function InlineAlert({ tone = 'error', children, onDismiss, dismissLabel, className, ...props }) {
|
|
12
13
|
const Icon = tone === 'info' ? Info : AlertTriangle;
|
|
13
|
-
return (_jsxs("div", { role: "alert", "data-slot": "inline-alert", "data-tone": tone, className: cn('flex items-start gap-2 rounded-md border px-3 py-2 text-xs', toneClass[tone], className), children: [_jsx(Icon, { className: "mt-0.5 size-3.5 shrink-0", strokeWidth: 1.5, "aria-hidden": true }), _jsx("span", { className: "flex-1 leading-relaxed", children: children }), onDismiss ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", onClick: onDismiss, "aria-label": dismissLabel, className: "-mr-1 size-4 rounded text-current opacity-80 hover:bg-transparent hover:opacity-100", children: _jsx(X, { className: "size-3", strokeWidth: 1.5 }) })) : null] }));
|
|
14
|
+
return (_jsxs("div", { role: "alert", "data-slot": "inline-alert", "data-tone": tone, className: cn('flex items-start gap-2 rounded-md border px-3 py-2 text-xs', toneClass[tone], className), ...props, children: [_jsx(Icon, { className: "mt-0.5 size-3.5 shrink-0", strokeWidth: 1.5, "aria-hidden": true }), _jsx("span", { className: "flex-1 leading-relaxed", children: children }), onDismiss ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", onClick: onDismiss, "aria-label": dismissLabel, className: "-mr-1 size-4 rounded text-current opacity-80 hover:bg-transparent hover:opacity-100", children: _jsx(X, { className: "size-3", strokeWidth: 1.5 }) })) : null] }));
|
|
14
15
|
}
|
|
15
16
|
export function InlineNotice({ tone = 'info', children, className, }) {
|
|
16
17
|
return (_jsx(InlineAlert, { tone: tone, className: className, children: children }));
|
|
@@ -21,8 +21,10 @@ export interface MetricTileProps extends Omit<React.ComponentProps<'div'>, 'titl
|
|
|
21
21
|
suffix?: React.ReactNode;
|
|
22
22
|
hint?: React.ReactNode;
|
|
23
23
|
tone?: MetricTone;
|
|
24
|
+
/** What the value is changing from, rendered as "before → after". See SpecStripItem.from. */
|
|
25
|
+
from?: React.ReactNode;
|
|
24
26
|
}
|
|
25
|
-
export declare function MetricTile({ label, value, suffix, hint, tone, className, ...props }: MetricTileProps): React.JSX.Element;
|
|
27
|
+
export declare function MetricTile({ label, value, suffix, hint, from, tone, className, ...props }: MetricTileProps): React.JSX.Element;
|
|
26
28
|
export interface MetricBreakdownItem {
|
|
27
29
|
key: string;
|
|
28
30
|
label: React.ReactNode;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import * as React from 'react';
|
|
4
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
5
|
import { cn } from '../lib/cn.js';
|
|
5
6
|
const TONE_TEXT = {
|
|
6
7
|
neutral: 'text-foreground',
|
|
@@ -18,8 +19,8 @@ const PANEL_PADDING = { none: '', sm: 'p-3', md: 'p-4' };
|
|
|
18
19
|
export function MetricPanel({ title, action, padding = 'md', className, children, ...props }) {
|
|
19
20
|
return (_jsxs("div", { "data-slot": "metric-panel", className: cn('bg-muted min-w-0', PANEL_PADDING[padding], className), ...props, children: [title || action ? (_jsxs("div", { className: cn('mb-3 flex items-center justify-between gap-2', padding === 'none' && 'px-3 pt-3'), children: [title ? _jsx("p", { className: "text-xs font-medium", children: title }) : _jsx("span", {}), action] })) : null, children] }));
|
|
20
21
|
}
|
|
21
|
-
export function MetricTile({ label, value, suffix, hint, tone = 'neutral', className, ...props }) {
|
|
22
|
-
return (_jsxs("div", { "data-slot": "metric-tile", "data-tone": tone, className: cn('bg-muted min-w-0 p-3', className), ...props, children: [label ? _jsx("p", { className: "text-muted-foreground truncate text-xs", children: label }) : null, _jsxs("p", { className: cn('text-2xl font-semibold tracking-tight tabular-nums', label ? 'mt-2' : '', TONE_TEXT[tone]), children: [value, suffix ? (_jsx("span", { className: "text-muted-foreground ml-1 text-xs font-normal", children: suffix })) : null] }), hint ? _jsx("p", { className: "text-muted-foreground mt-1 text-[11px] leading-snug", children: hint }) : null] }));
|
|
22
|
+
export function MetricTile({ label, value, suffix, hint, from, tone = 'neutral', className, ...props }) {
|
|
23
|
+
return (_jsxs("div", { "data-slot": "metric-tile", "data-tone": tone, className: cn('bg-muted min-w-0 p-3', className), ...props, children: [label ? _jsx("p", { className: "text-muted-foreground truncate text-xs", children: label }) : null, _jsxs("p", { className: cn('text-2xl font-semibold tracking-tight tabular-nums', label ? 'mt-2' : '', TONE_TEXT[tone]), children: [from !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground font-normal", children: from }), _jsx(ArrowRight, { className: "text-muted-foreground mx-1.5 inline size-4 align-[-0.05em]", strokeWidth: 1.5, "aria-hidden": true })] })) : null, value, suffix ? (_jsx("span", { className: "text-muted-foreground ml-1 text-xs font-normal", children: suffix })) : null] }), hint ? _jsx("p", { className: "text-muted-foreground mt-1 text-[11px] leading-snug", children: hint }) : null] }));
|
|
23
24
|
}
|
|
24
25
|
const TONE_BAR = {
|
|
25
26
|
neutral: 'bg-foreground/35',
|
|
@@ -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
|
|
@@ -65,8 +73,10 @@ export declare class NautAvatarEngine {
|
|
|
65
73
|
/** An expression holds one eye shape forever; idle opens the eyes between beats. */
|
|
66
74
|
private readonly eyeRest;
|
|
67
75
|
private el;
|
|
76
|
+
private readonly rendered;
|
|
68
77
|
constructor(opts?: NautAvatarOptions);
|
|
69
78
|
attach(el: NautAvatarElements): void;
|
|
79
|
+
get needsResponsiveFrameRate(): boolean;
|
|
70
80
|
setShape(shape: NautShape): void;
|
|
71
81
|
setColor(color: NautColor): void;
|
|
72
82
|
setExpression(key: NautExpression, instant?: boolean): void;
|
|
@@ -78,6 +88,8 @@ export declare class NautAvatarEngine {
|
|
|
78
88
|
/** Eases the eyes back to their neutral open shape and back into the expression. */
|
|
79
89
|
private restEyes;
|
|
80
90
|
private render;
|
|
91
|
+
private writeAttribute;
|
|
92
|
+
private writeStyle;
|
|
81
93
|
}
|
|
82
94
|
/** Stable pseudo-random pick so the same id always gets the same appearance. */
|
|
83
95
|
export declare function hashSeed(input: string): number;
|
|
@@ -646,6 +646,7 @@ export class NautAvatarEngine {
|
|
|
646
646
|
/** An expression holds one eye shape forever; idle opens the eyes between beats. */
|
|
647
647
|
eyeRest = { open: 0, cur: 0, next: HOLD_EXPRESSION_S };
|
|
648
648
|
el = null;
|
|
649
|
+
rendered = new Map();
|
|
649
650
|
constructor(opts = {}) {
|
|
650
651
|
this.rand = rng(opts.seed ?? 1);
|
|
651
652
|
this.shape = opts.shape ?? 'circle';
|
|
@@ -664,6 +665,17 @@ export class NautAvatarEngine {
|
|
|
664
665
|
}
|
|
665
666
|
attach(el) {
|
|
666
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));
|
|
667
679
|
}
|
|
668
680
|
setShape(shape) {
|
|
669
681
|
this.shape = shape;
|
|
@@ -796,10 +808,10 @@ export class NautAvatarEngine {
|
|
|
796
808
|
const el = this.el;
|
|
797
809
|
if (!el)
|
|
798
810
|
return;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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)})`);
|
|
803
815
|
const yaw = clamp(o.gazeX, -1.3, 1.3) * 0.85 + o.spin;
|
|
804
816
|
const pitch = -clamp(o.gazeY, -1.3, 1.3) * 0.7;
|
|
805
817
|
const R = this.sphere;
|
|
@@ -811,7 +823,7 @@ export class NautAvatarEngine {
|
|
|
811
823
|
const poly = eyePolygon(p, sgn);
|
|
812
824
|
const eye = side === 'L' ? el.eyeL : el.eyeR;
|
|
813
825
|
if (!poly.length) {
|
|
814
|
-
eye.
|
|
826
|
+
this.writeAttribute(`eye${side}.d`, eye, 'd', '');
|
|
815
827
|
continue;
|
|
816
828
|
}
|
|
817
829
|
const fr = eyeFrame(sgn * o.eyeSep, o.eyeLat, yaw, pitch);
|
|
@@ -823,12 +835,24 @@ export class NautAvatarEngine {
|
|
|
823
835
|
const vx = lerp(0, fr.V[0], curve);
|
|
824
836
|
const vy = lerp(1, fr.V[1], curve);
|
|
825
837
|
const d = poly
|
|
826
|
-
.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)}`)
|
|
827
839
|
.join(' ');
|
|
828
|
-
eye.
|
|
829
|
-
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');
|
|
830
842
|
}
|
|
831
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
|
+
}
|
|
832
856
|
}
|
|
833
857
|
/** Stable pseudo-random pick so the same id always gets the same appearance. */
|
|
834
858
|
export function hashSeed(input) {
|
|
@@ -8,11 +8,15 @@ import { pointerGaze } from './naut-pointer-gaze.js';
|
|
|
8
8
|
export { NAUT_COLORS, NAUT_COLOR_KEYS, NAUT_EXPRESSIONS, NAUT_GESTURES, NAUT_SHAPES, isNautColor, isNautShape, nautAppearanceForSeed, } from './naut-avatar-engine.js';
|
|
9
9
|
/** A face can be carried across the page by something other than a mouse or a scroll. */
|
|
10
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;
|
|
11
14
|
/** Where the mouse is, shared by every face so one listener serves the page. */
|
|
12
15
|
const pointer = {
|
|
13
16
|
x: 0,
|
|
14
17
|
y: 0,
|
|
15
18
|
active: false,
|
|
19
|
+
lastMovedAt: 0,
|
|
16
20
|
/** Bumps whenever cached face rects could have gone stale. */
|
|
17
21
|
moved: 0,
|
|
18
22
|
onPointer(event) {
|
|
@@ -23,6 +27,7 @@ const pointer = {
|
|
|
23
27
|
pointer.x = event.clientX;
|
|
24
28
|
pointer.y = event.clientY;
|
|
25
29
|
pointer.active = true;
|
|
30
|
+
pointer.lastMovedAt = performance.now();
|
|
26
31
|
pointer.moved++;
|
|
27
32
|
},
|
|
28
33
|
onLeave() {
|
|
@@ -64,16 +69,21 @@ function aimAtPointer(entry, now) {
|
|
|
64
69
|
const ticker = {
|
|
65
70
|
entries: new Set(),
|
|
66
71
|
frame: 0,
|
|
67
|
-
last: 0,
|
|
68
72
|
tick(now) {
|
|
69
|
-
const
|
|
70
|
-
ticker.
|
|
71
|
-
|
|
72
|
-
|
|
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)
|
|
73
84
|
aimAtPointer(entry, now);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
entry.engine.update(dt);
|
|
85
|
+
entry.engine.update(dt);
|
|
86
|
+
}
|
|
77
87
|
ticker.frame = ticker.entries.size ? requestAnimationFrame(ticker.tick) : 0;
|
|
78
88
|
},
|
|
79
89
|
add(entry) {
|
|
@@ -81,7 +91,6 @@ const ticker = {
|
|
|
81
91
|
pointer.listen();
|
|
82
92
|
ticker.entries.add(entry);
|
|
83
93
|
if (!ticker.frame) {
|
|
84
|
-
ticker.last = performance.now();
|
|
85
94
|
ticker.frame = requestAnimationFrame(ticker.tick);
|
|
86
95
|
}
|
|
87
96
|
},
|
|
@@ -120,6 +129,7 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
120
129
|
rect: null,
|
|
121
130
|
rectAt: -1,
|
|
122
131
|
rectTime: 0,
|
|
132
|
+
lastUpdate: performance.now(),
|
|
123
133
|
}));
|
|
124
134
|
React.useImperativeHandle(forwardedRef, () => ({ play: (gesture) => engine.play(gesture) }), [
|
|
125
135
|
engine,
|
|
@@ -150,7 +160,7 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
150
160
|
engine.update(0);
|
|
151
161
|
}, [engine]);
|
|
152
162
|
React.useEffect(() => {
|
|
153
|
-
if (
|
|
163
|
+
if (!animated) {
|
|
154
164
|
engine.settle();
|
|
155
165
|
engine.update(0);
|
|
156
166
|
return;
|
|
@@ -159,6 +169,7 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
159
169
|
entry.svg = svg;
|
|
160
170
|
entry.rect = null;
|
|
161
171
|
entry.rectAt = -1;
|
|
172
|
+
entry.lastUpdate = performance.now();
|
|
162
173
|
ticker.add(entry);
|
|
163
174
|
const observer = svg && typeof IntersectionObserver !== 'undefined'
|
|
164
175
|
? new IntersectionObserver((records) => {
|
|
@@ -173,12 +184,12 @@ export const NautAvatar = React.forwardRef(function NautAvatar({ shape = 'circle
|
|
|
173
184
|
ticker.remove(entry);
|
|
174
185
|
entry.svg = null;
|
|
175
186
|
};
|
|
176
|
-
}, [engine, entry
|
|
187
|
+
}, [animated, engine, entry]);
|
|
177
188
|
React.useEffect(() => {
|
|
178
|
-
if (
|
|
189
|
+
if (animated)
|
|
179
190
|
return;
|
|
180
191
|
engine.settle();
|
|
181
192
|
engine.update(0);
|
|
182
|
-
}, [
|
|
183
|
-
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))" })] })] }) }));
|
|
184
195
|
});
|
|
@@ -129,8 +129,10 @@ export declare const NAUT_SCENES: {
|
|
|
129
129
|
export type NautSceneName = keyof typeof NAUT_SCENES;
|
|
130
130
|
export interface NautSceneProps {
|
|
131
131
|
scene: NautSceneName;
|
|
132
|
+
/** Enables ambient drift and gestures. */
|
|
133
|
+
animated?: boolean;
|
|
132
134
|
/** Sizes the scene; the faces lay themselves out inside whatever box this gives. */
|
|
133
135
|
className?: string;
|
|
134
136
|
}
|
|
135
137
|
/** A small animated illustration built from Naut faces. Decorative — it carries no label. */
|
|
136
|
-
export declare function NautScene({ scene, className }: NautSceneProps): React.ReactElement;
|
|
138
|
+
export declare function NautScene({ scene, animated, className, }: NautSceneProps): React.ReactElement;
|
|
@@ -126,12 +126,13 @@ const DRIFT_TILT = 4;
|
|
|
126
126
|
/** Seconds between the starts of neighbouring drifts, so nothing moves in unison. */
|
|
127
127
|
const DRIFT_OFFSET = 0.7;
|
|
128
128
|
/** A small animated illustration built from Naut faces. Decorative — it carries no label. */
|
|
129
|
-
export function NautScene({ scene, className }) {
|
|
129
|
+
export function NautScene({ scene, animated = true, className, }) {
|
|
130
130
|
const reducedMotion = useReducedMotion();
|
|
131
|
+
const active = animated && !reducedMotion;
|
|
131
132
|
const { faces, gesture, interval } = NAUT_SCENES[scene];
|
|
132
133
|
const handles = React.useRef([]);
|
|
133
134
|
React.useEffect(() => {
|
|
134
|
-
if (
|
|
135
|
+
if (!active)
|
|
135
136
|
return;
|
|
136
137
|
const pending = [];
|
|
137
138
|
const run = () => {
|
|
@@ -145,13 +146,13 @@ export function NautScene({ scene, className }) {
|
|
|
145
146
|
for (const timeout of pending)
|
|
146
147
|
window.clearTimeout(timeout);
|
|
147
148
|
};
|
|
148
|
-
}, [gesture, interval
|
|
149
|
+
}, [active, gesture, interval]);
|
|
149
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: {
|
|
150
151
|
left: `${face.x * 100}%`,
|
|
151
152
|
top: `${face.y * 100}%`,
|
|
152
153
|
height: `${face.size * 100}%`,
|
|
153
154
|
transform: 'translate(-50%, -50%)',
|
|
154
|
-
}, children: _jsx(motion.span, { className: "block size-full", animate:
|
|
155
|
+
}, children: _jsx(motion.span, { className: "block size-full", animate: !active
|
|
155
156
|
? undefined
|
|
156
157
|
: {
|
|
157
158
|
y: [`-${DRIFT_TRAVEL}%`, `${DRIFT_TRAVEL}%`],
|
|
@@ -164,5 +165,5 @@ export function NautScene({ scene, className }) {
|
|
|
164
165
|
ease: 'easeInOut',
|
|
165
166
|
}, children: _jsx(NautAvatar, { ref: (handle) => {
|
|
166
167
|
handles.current[index] = handle;
|
|
167
|
-
}, shape: face.shape, color: face.color, expression: face.expression, seed: `${scene}-${index}` }) }) }, `${face.shape}-${face.color}-${index}`))) }) }));
|
|
168
|
+
}, shape: face.shape, color: face.color, expression: face.expression, idle: active, seed: `${scene}-${index}` }) }) }, `${face.shape}-${face.color}-${index}`))) }) }));
|
|
168
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,
|
|
@@ -4,6 +4,11 @@ export interface SpecStripItem {
|
|
|
4
4
|
label: string;
|
|
5
5
|
value: React.ReactNode;
|
|
6
6
|
suffix?: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* What the value is changing from. Present, the item reads "before → after", which is what a
|
|
9
|
+
* shape being upgraded is: the new number alone says what you get without saying what moved.
|
|
10
|
+
*/
|
|
11
|
+
from?: React.ReactNode;
|
|
7
12
|
}
|
|
8
13
|
export interface SpecStripProps extends Omit<React.ComponentProps<'div'>, 'children'> {
|
|
9
14
|
items: SpecStripItem[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from 'react';
|
|
4
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
5
|
import { cn } from '../lib/cn.js';
|
|
5
6
|
import { Separator } from './separator.js';
|
|
6
7
|
/**
|
|
@@ -11,5 +12,5 @@ import { Separator } from './separator.js';
|
|
|
11
12
|
* stops meaning anything.
|
|
12
13
|
*/
|
|
13
14
|
export function SpecStrip({ items, className, ...props }) {
|
|
14
|
-
return (_jsx("div", { "data-slot": "spec-strip", className: cn('flex items-stretch gap-3', className), ...props, children: items.map((item, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(Separator, { orientation: "vertical", className: "bg-border/60" }) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col items-center gap-1.5 py-1", children: [_jsx("span", { className: "text-muted-foreground [&_svg]:size-5", "aria-hidden": true, children: item.icon }), _jsxs("span", { className: "truncate text-[17px] font-medium tabular-nums", children: [item.value, item.suffix ? (_jsx("span", { className: "text-muted-foreground ml-1 text-[12px] font-normal", children: item.suffix })) : null] }), _jsx("span", { className: "text-muted-foreground truncate text-[11.5px]", children: item.label })] })] }, item.label))) }));
|
|
15
|
+
return (_jsx("div", { "data-slot": "spec-strip", className: cn('flex items-stretch gap-3', className), ...props, children: items.map((item, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(Separator, { orientation: "vertical", className: "bg-border/60" }) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col items-center gap-1.5 py-1", children: [_jsx("span", { className: "text-muted-foreground [&_svg]:size-5", "aria-hidden": true, children: item.icon }), _jsxs("span", { className: "truncate text-[17px] font-medium tabular-nums", children: [item.from !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground font-normal", children: item.from }), _jsx(ArrowRight, { className: "text-muted-foreground mx-1 inline size-3.5 align-[-0.1em]", strokeWidth: 1.5, "aria-hidden": true })] })) : null, item.value, item.suffix ? (_jsx("span", { className: "text-muted-foreground ml-1 text-[12px] font-normal", children: item.suffix })) : null] }), _jsx("span", { className: "text-muted-foreground truncate text-[11.5px]", children: item.label })] })] }, item.label))) }));
|
|
15
16
|
}
|
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';
|
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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atmos.build/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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"
|