@nadicodeai/ui 6.6.2 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -3
- package/dist/components/activity-orb.d.ts +1 -1
- package/dist/components/activity-orb.d.ts.map +1 -1
- package/docs/activity.md +4 -5
- package/docs/contract.md +15 -4
- package/package.json +2 -7
- package/dist/components/threshold/canvas-budget.d.ts +0 -10
- package/dist/components/threshold/canvas-budget.d.ts.map +0 -1
- package/dist/components/threshold/canvas-budget.js +0 -54
- package/dist/components/threshold/index.d.ts +0 -6
- package/dist/components/threshold/index.d.ts.map +0 -1
- package/dist/components/threshold/index.js +0 -5
- package/dist/components/threshold/orb.d.ts +0 -14
- package/dist/components/threshold/orb.d.ts.map +0 -1
- package/dist/components/threshold/orb.js +0 -17
- package/dist/components/threshold/presence.d.ts +0 -27
- package/dist/components/threshold/presence.d.ts.map +0 -1
- package/dist/components/threshold/presence.js +0 -159
- package/dist/components/threshold/room.d.ts +0 -33
- package/dist/components/threshold/room.d.ts.map +0 -1
- package/dist/components/threshold/room.js +0 -168
- package/dist/components/threshold/threshold.css +0 -228
- package/dist/components/threshold/typed-line.d.ts +0 -8
- package/dist/components/threshold/typed-line.d.ts.map +0 -1
- package/dist/components/threshold/typed-line.js +0 -43
- package/dist/components/threshold/use-reduced-motion.d.ts +0 -10
- package/dist/components/threshold/use-reduced-motion.d.ts.map +0 -1
- package/dist/components/threshold/use-reduced-motion.js +0 -32
- package/dist/threshold.d.ts +0 -2
- package/dist/threshold.d.ts.map +0 -1
- package/dist/threshold.js +0 -13
- package/docs/threshold.md +0 -221
package/AGENTS.md
CHANGED
|
@@ -9,9 +9,8 @@ exports.
|
|
|
9
9
|
- Tokens, CSS, motion, and assets: `../design-system/DESIGN.md`.
|
|
10
10
|
- Agent visuals: `docs/agent-visual.md`; the Forma avatars: `docs/forma-avatar.md`.
|
|
11
11
|
- System activity indicators: `docs/activity.md`.
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
`@nadicodeai/ui/threshold.css` beside them.
|
|
12
|
+
- What belongs in this package and what belongs in a product: `docs/contract.md`,
|
|
13
|
+
"Placement". Nadia's entry screen is the worked example that moved out.
|
|
15
14
|
- Package releases: `../../docs/agents/publish.md`.
|
|
16
15
|
|
|
17
16
|
`npm run dev -w @nadicodeai/ui` watches source and refreshes `dist/`;
|
|
@@ -7,7 +7,7 @@ export type ActivityOrbProps = {
|
|
|
7
7
|
style?: CSSProperties;
|
|
8
8
|
/** Freeze the animation for a preview or a user-controlled motion pause. */
|
|
9
9
|
paused?: boolean;
|
|
10
|
-
/** 20 inline, 64 within a region, 88
|
|
10
|
+
/** 20 inline, 64 within a region, 88 on an application's entry screen. */
|
|
11
11
|
size?: ActivityOrbSize;
|
|
12
12
|
} & ({
|
|
13
13
|
decorative: true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-orb.d.ts","sourceRoot":"","sources":["../../src/components/activity-orb.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,OAAO,CAAC;AAE9D,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAoB,MAAM,yBAAyB,CAAC;AAGpG,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE7E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,
|
|
1
|
+
{"version":3,"file":"activity-orb.d.ts","sourceRoot":"","sources":["../../src/components/activity-orb.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,OAAO,CAAC;AAE9D,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAoB,MAAM,yBAAyB,CAAC;AAGpG,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE7E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB,GAAG,CAAC;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,UAAU,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAElF,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,UAAkB,EAClB,KAAK,EACL,MAAc,EACd,IAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,gBAAgB,+BAmClB"}
|
package/docs/activity.md
CHANGED
|
@@ -61,7 +61,7 @@ movement as well as its travelling particles. The implementation owns these
|
|
|
61
61
|
values together; consumers cannot tune speed, density or particle colours.
|
|
62
62
|
|
|
63
63
|
Use 20 CSS pixels inline, including buttons and rows with room for that size;
|
|
64
|
-
64 for a contained region; 88
|
|
64
|
+
64 for a contained region; 88 on an application's entry screen. The 20 preset is
|
|
65
65
|
separately tuned, and must not be obtained by shrinking the 64 preset. The 88
|
|
66
66
|
extent uses the 64 preset. A 12–16 pixel badge slot keeps its existing compact
|
|
67
67
|
spinner until its surrounding component explicitly accommodates this family.
|
|
@@ -103,10 +103,9 @@ the operation fails or React replaces the startup surface.
|
|
|
103
103
|
|
|
104
104
|
Import `@nadicodeai/ui/globals.css` as described in the
|
|
105
105
|
[consumer guide](consuming-cross-repo.md). The renderer inherits actual CSS
|
|
106
|
-
ink and reads canonical colour tokens.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
the threshold seam, outside the shared renderer.
|
|
106
|
+
ink and reads canonical colour tokens. Nadia's entry screen wraps this renderer
|
|
107
|
+
in its own `Orb`, which fixes a dark appearance the shared renderer knows
|
|
108
|
+
nothing about; that wrapper lives in the desktop, not here.
|
|
110
109
|
|
|
111
110
|
## Accessibility and runtime
|
|
112
111
|
|
package/docs/contract.md
CHANGED
|
@@ -6,7 +6,7 @@ The normative invariants for the `@nadicodeai/ui` package: what may live here, h
|
|
|
6
6
|
|
|
7
7
|
- **Token-only, no raw literals or shadow aliases.** Components style through semantic shadcn utilities (`bg-background`, `text-foreground`, `border-input`, and the rest), or a canonical generated design-system token when no standard role is precise enough. The UI package does not mint screen-, component-, or product-specific color aliases. No raw `#hex` / `rgb()` / `hsl()` may appear in `.tsx`; a new source value goes to `packages/design-system/DESIGN.md` first and flows through generated output. A narrow protocol adapter may translate third-party classes directly to canonical tokens in `globals.css`, but it must not create another token vocabulary. Enforced by the raw-colour `no-restricted-syntax` selectors in the library ESLint config, `packages/ui/tests/guards/dark-semantic-roles.test.ts`, and `packages/ui/tests/guards/token-reference-integrity.test.ts`.
|
|
8
8
|
- **Dark-ready semantics, no component dark implementation.** Components use the same semantic role or canonical mode-aware token in every mode. They must not ship Tailwind `dark:` color utilities, component-local `.dark` blocks, OS dark-mode branching, or raw dark palette utilities such as `dark:bg-slate-950` and `dark:text-white`. An intentionally fixed-presentation technology surface, such as the dark terminal console, selects canonical fixed-mode tokens as one coherent component treatment rather than branching by app theme. Enforced by `packages/ui/tests/guards/no-dark.test.ts`, `packages/ui/tests/guards/dark-semantic-roles.test.ts`, and `packages/ui/tests/guards/token-reference-integrity.test.ts`. The generated mode and theme architecture lives only in [`packages/design-system/DESIGN.md`](../../design-system/DESIGN.md#css-architecture--token-pipeline).
|
|
9
|
-
- **Components inherit one complete React style interface.** `@nadicodeai/ui/globals.css` composes every style dependency owned by this package, including the complete design-system CSS module. Components carry no theme of their own, and React callers do not compose design-system stylesheets or partials beside it. Installation and source-scanning setup live in the [consumer setup guide](consuming-cross-repo.md). `@nadicodeai/ui/
|
|
9
|
+
- **Components inherit one complete React style interface.** `@nadicodeai/ui/globals.css` composes every style dependency owned by this package, including the complete design-system CSS module. Components carry no theme of their own, and React callers do not compose design-system stylesheets or partials beside it. Installation and source-scanning setup live in the [consumer setup guide](consuming-cross-repo.md). `@nadicodeai/ui/globals.css` is the only stylesheet this package ships; a surface that loads no Tailwind layer takes the design system's token artifact and writes its own rules against it.
|
|
10
10
|
- **Design-system geometry remains the foundation.** React components compose the kept `nc-*` page grammar and display CSS components; they do not redefine frame math, marker placement, token values, or raw CSS geometry. Use parent-paints-plus-gap for bordered cell grids and keep every visible seam single-owner (doctrine home: `packages/design-system/DESIGN.md` "## Layout").
|
|
11
11
|
|
|
12
12
|
### Persistent notices
|
|
@@ -139,8 +139,7 @@ gone, is [`packages/design-system/DESIGN.md`, "## Status Vocabulary"](../../desi
|
|
|
139
139
|
System activity uses `ActivityOrb` for loading, connecting and processing;
|
|
140
140
|
[activity.md](activity.md) owns selection, rendering and accessibility. It
|
|
141
141
|
composes with status feedback and measured progress without adding outcome
|
|
142
|
-
states to the orb.
|
|
143
|
-
this same renderer.
|
|
142
|
+
states to the orb.
|
|
144
143
|
|
|
145
144
|
- **Source-authored, built package.** Components are authored as shadcn-style source in `src/`, then emitted to `dist/` for consumption by other apps. Package exports must point at built JS, declarations, and CSS, never raw `src/*.tsx`, and `npm pack --dry-run` must include only intentional consumer artifacts.
|
|
146
145
|
- **Package surface is source-backed.** Public components stay flat under `src/components/`. Reviewed registry source under `src/internal/` is implementation, not a package namespace. Do not add top-level exports for recipes, blocks, agent, terminal, or vendor groupings unless a later package API decision adds concrete public source, built files, docs, and tests in the same change.
|
|
@@ -152,7 +151,19 @@ this same renderer.
|
|
|
152
151
|
|
|
153
152
|
## Admission Rules
|
|
154
153
|
|
|
155
|
-
Assistant conversation and runtime APIs enter through `@nadicodeai/ui/agents`; Agent identity enters through `@nadicodeai/ui/components/agent-card`, `@nadicodeai/ui/components/agent-visual`, and the Forma avatars in `@nadicodeai/ui/components/forma-avatar` (owned by [`forma-avatar.md`](forma-avatar.md)). The consuming application still constructs the runtime and owns transport, persistence, permissions, tools, approvals, and failure policy. Shared non-Agent product components (a shell, a settings surface, a generic section, a diff, or a file preview) are admitted only with a named consuming surface landing in the same change. App-bespoke UI and behavior (window chrome, PTY/xterm embeds, queues, approvals, and persistence) stay in the consuming app; they never become package abstractions.
|
|
154
|
+
Assistant conversation and runtime APIs enter through `@nadicodeai/ui/agents`; Agent identity enters through `@nadicodeai/ui/components/agent-card`, `@nadicodeai/ui/components/agent-visual`, and the Forma avatars in `@nadicodeai/ui/components/forma-avatar` (owned by [`forma-avatar.md`](forma-avatar.md)). The consuming application still constructs the runtime and owns transport, persistence, permissions, tools, approvals, and failure policy. Shared non-Agent product components (a shell, a settings surface, a generic section, a diff, or a file preview) are admitted only with a named consuming surface landing in the same change. App-bespoke UI and behavior (window chrome, PTY/xterm embeds, queues, approvals, and persistence) stay in the consuming app; they never become package abstractions. A component left unconsumed after its target migration ships gets removed.
|
|
155
|
+
|
|
156
|
+
### Placement
|
|
157
|
+
|
|
158
|
+
`@nadicodeai/ui` and `@nadicodeai/design-system` hold the Foundation and the
|
|
159
|
+
components, assets and styles at least two products use; a component, asset or
|
|
160
|
+
style one product alone needs lives in that product's own directory. Nadia's
|
|
161
|
+
entry screen is the worked example: the room, the presence, the typed line and
|
|
162
|
+
their stylesheet were a package surface no other product imported, and they now
|
|
163
|
+
live in the desktop at `apps/nadia/hermes/apps/desktop/src/plugins/nadia/entry/`,
|
|
164
|
+
where [`apps/nadia/docs/entry-screen.md`](../../../apps/nadia/docs/entry-screen.md)
|
|
165
|
+
owns their contract. `packages/ui/tests/guards/product-placement.test.ts`
|
|
166
|
+
refuses a product's name in either package.
|
|
156
167
|
|
|
157
168
|
### Agent identity presentation
|
|
158
169
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nadicodeai/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -19,11 +19,6 @@
|
|
|
19
19
|
"types": "./dist/agents.d.ts",
|
|
20
20
|
"import": "./dist/agents.js"
|
|
21
21
|
},
|
|
22
|
-
"./threshold": {
|
|
23
|
-
"types": "./dist/threshold.d.ts",
|
|
24
|
-
"import": "./dist/threshold.js"
|
|
25
|
-
},
|
|
26
|
-
"./threshold.css": "./dist/components/threshold/threshold.css",
|
|
27
22
|
"./activity": {
|
|
28
23
|
"types": "./dist/activity.d.ts",
|
|
29
24
|
"import": "./dist/activity.js"
|
|
@@ -70,7 +65,7 @@
|
|
|
70
65
|
"@assistant-ui/react": "0.15.18",
|
|
71
66
|
"@assistant-ui/react-markdown": "0.14.14",
|
|
72
67
|
"@base-ui/react": "^1.8.0",
|
|
73
|
-
"@nadicodeai/design-system": "
|
|
68
|
+
"@nadicodeai/design-system": "7.0.0",
|
|
74
69
|
"@paper-design/shaders": "0.0.80",
|
|
75
70
|
"class-variance-authority": "^0.7.1",
|
|
76
71
|
"clsx": "^2.1.1",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** True while a primitive that outranks the room owns the window's canvas. */
|
|
2
|
-
export declare function canvasHeld(): boolean;
|
|
3
|
-
/**
|
|
4
|
-
* Take the window's canvas. Returns the release, which is idempotent: a
|
|
5
|
-
* teardown that runs twice releases once.
|
|
6
|
-
*/
|
|
7
|
-
export declare function holdCanvas(): () => void;
|
|
8
|
-
/** Whether the room must yield its canvas right now. Live. */
|
|
9
|
-
export declare function useCanvasHeld(): boolean;
|
|
10
|
-
//# sourceMappingURL=canvas-budget.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-budget.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/canvas-budget.ts"],"names":[],"mappings":"AA0BA,8EAA8E;AAC9E,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,MAAM,IAAI,CAWvC;AAWD,8DAA8D;AAC9D,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useSyncExternalStore } from "react";
|
|
3
|
-
/*
|
|
4
|
-
* One shader canvas per window: the living-elements register's rule, held
|
|
5
|
-
* here so no screen can break it by composition.
|
|
6
|
-
*
|
|
7
|
-
* Two threshold primitives can paint with a shader, the room and the arriving
|
|
8
|
-
* presence, and a threshold mounts both. The presence's water outranks the
|
|
9
|
-
* room: while Nadia is arriving her water is the window's one live canvas and
|
|
10
|
-
* the room shows its still field; when she stands clear the water leaves and
|
|
11
|
-
* the room's canvas is the one that paints. The presence takes a hold for as
|
|
12
|
-
* long as its water is wanted, and the room yields while any hold is out.
|
|
13
|
-
*
|
|
14
|
-
* The budget is per window, so it is module state rather than a context: a
|
|
15
|
-
* provider would let a surface forget to wrap both primitives, and the rule
|
|
16
|
-
* would then hold only where someone remembered it.
|
|
17
|
-
*/
|
|
18
|
-
let holds = 0;
|
|
19
|
-
const listeners = new Set();
|
|
20
|
-
const notify = () => {
|
|
21
|
-
for (const listener of listeners)
|
|
22
|
-
listener();
|
|
23
|
-
};
|
|
24
|
-
/** True while a primitive that outranks the room owns the window's canvas. */
|
|
25
|
-
export function canvasHeld() {
|
|
26
|
-
return holds > 0;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Take the window's canvas. Returns the release, which is idempotent: a
|
|
30
|
-
* teardown that runs twice releases once.
|
|
31
|
-
*/
|
|
32
|
-
export function holdCanvas() {
|
|
33
|
-
holds += 1;
|
|
34
|
-
notify();
|
|
35
|
-
let released = false;
|
|
36
|
-
return () => {
|
|
37
|
-
if (released)
|
|
38
|
-
return;
|
|
39
|
-
released = true;
|
|
40
|
-
holds -= 1;
|
|
41
|
-
notify();
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function subscribe(listener) {
|
|
45
|
-
listeners.add(listener);
|
|
46
|
-
return () => {
|
|
47
|
-
listeners.delete(listener);
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
const serverSnapshot = () => false;
|
|
51
|
-
/** Whether the room must yield its canvas right now. Live. */
|
|
52
|
-
export function useCanvasHeld() {
|
|
53
|
-
return useSyncExternalStore(subscribe, canvasHeld, serverSnapshot);
|
|
54
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { Orb, type OrbProps, type OrbSize, type OrbState, type OrbTheme } from "./orb.js";
|
|
2
|
-
export { arrivalOpacity, NadiaPresence, type NadiaPresenceProps, type PresenceState, } from "./presence.js";
|
|
3
|
-
export { ThresholdRoom, type ThresholdRoomProps } from "./room.js";
|
|
4
|
-
export { TypedLine, type TypedLineProps } from "./typed-line.js";
|
|
5
|
-
export { useReducedMotion } from "./use-reduced-motion.js";
|
|
6
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAC;AACvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type ActivityOrbSize } from "../activity-orb.js";
|
|
2
|
-
export type OrbState = "working" | "connecting" | "absent";
|
|
3
|
-
export type OrbTheme = "auto" | "dark" | "light";
|
|
4
|
-
export type OrbSize = ActivityOrbSize;
|
|
5
|
-
export type OrbProps = {
|
|
6
|
-
className?: string;
|
|
7
|
-
label: string;
|
|
8
|
-
size?: OrbSize;
|
|
9
|
-
state: OrbState;
|
|
10
|
-
theme?: OrbTheme;
|
|
11
|
-
};
|
|
12
|
-
/** Preserve the threshold API while using the one shared activity renderer. */
|
|
13
|
-
export declare function Orb({ className, label, size, state, theme }: OrbProps): import("react").JSX.Element | null;
|
|
14
|
-
//# sourceMappingURL=orb.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"orb.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/orb.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG9E,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;AAC3D,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AACjD,MAAM,MAAM,OAAO,GAAG,eAAe,CAAC;AAEtC,MAAM,MAAM,QAAQ,GAAG;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB,CAAC;AAUF,+EAA+E;AAC/E,wBAAgB,GAAG,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,IAAS,EAAE,KAAK,EAAE,KAAc,EAAE,EAAE,QAAQ,sCAYnF"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { ActivityOrb } from "../activity-orb.js";
|
|
4
|
-
import { cn } from "../../lib/utils.js";
|
|
5
|
-
// A threshold deliberately fixes its appearance. Normal product surfaces
|
|
6
|
-
// inherit their ink; the shared renderer needs no knowledge of app themes.
|
|
7
|
-
const THEME_INK = {
|
|
8
|
-
auto: "inherit",
|
|
9
|
-
dark: "var(--nc-dark-ink)",
|
|
10
|
-
light: "var(--nc-ink)",
|
|
11
|
-
};
|
|
12
|
-
/** Preserve the threshold API while using the one shared activity renderer. */
|
|
13
|
-
export function Orb({ className, label, size = 88, state, theme = "dark" }) {
|
|
14
|
-
if (state === "absent")
|
|
15
|
-
return null;
|
|
16
|
-
return (_jsx(ActivityOrb, { activity: state === "working" ? "processing" : "connecting", className: cn("nc-threshold-orb", className), "data-state": state, label: label, size: size, style: { color: THEME_INK[theme] } }));
|
|
17
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export type PresenceState = "arriving" | "present" | "ready" | "error";
|
|
2
|
-
export type NadiaPresenceProps = {
|
|
3
|
-
className?: string;
|
|
4
|
-
/** The accessible name of the arrival, read by the hidden progress bar. */
|
|
5
|
-
label: string;
|
|
6
|
-
/** 0 while she is behind the field, 1 when she stands clear. */
|
|
7
|
-
reveal: number;
|
|
8
|
-
/**
|
|
9
|
-
* The portrait master. The package imports no binary asset of its own, so
|
|
10
|
-
* the consuming surface resolves `@nadicodeai/design-system/assets/nadia-solid-surface`
|
|
11
|
-
* with its own bundler and passes the URL here.
|
|
12
|
-
*/
|
|
13
|
-
src: string;
|
|
14
|
-
state: PresenceState;
|
|
15
|
-
};
|
|
16
|
-
/** The arrival curve: a trace at the start, not a face already half-built. */
|
|
17
|
-
export declare function arrivalOpacity(reveal: number): number;
|
|
18
|
-
/**
|
|
19
|
-
* Nadia at a threshold.
|
|
20
|
-
*
|
|
21
|
-
* `reveal` drives one opacity, so nothing re-renders per frame and the value
|
|
22
|
-
* a Customer sees is the value assistive technology is told. The lower edge
|
|
23
|
-
* dissolves into the room instead of ending on a rectangle. At `state="ready"`
|
|
24
|
-
* she takes one short verde vivo beat, her presence accent, and settles.
|
|
25
|
-
*/
|
|
26
|
-
export declare function NadiaPresence({ className, label, reveal, src, state }: NadiaPresenceProps): import("react").JSX.Element;
|
|
27
|
-
//# sourceMappingURL=presence.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/presence.tsx"],"names":[],"mappings":"AA+CA,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,aAAa,CAAC;CACtB,CAAC;AAIF,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD;AAgBD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,kBAAkB,+BAsIzF"}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { defaultObjectSizing, ShaderFitOptions, ShaderMount, waterFragmentShader, } from "@paper-design/shaders";
|
|
4
|
-
import { useEffect, useRef, useState } from "react";
|
|
5
|
-
import { resolveIdentityColor } from "../../internal/agent-visual/color.js";
|
|
6
|
-
import { cn } from "../../lib/utils.js";
|
|
7
|
-
import { holdCanvas } from "./canvas-budget.js";
|
|
8
|
-
import { useReducedMotion } from "./use-reduced-motion.js";
|
|
9
|
-
/*
|
|
10
|
-
* Nadia's presence: the portrait master itself, arriving through the room. No
|
|
11
|
-
* medallion, no ring, no bar. How much of her you can see is the progress.
|
|
12
|
-
*
|
|
13
|
-
* While she is arriving the portrait carries Paper Shaders' Water at a
|
|
14
|
-
* restrained intensity, so the field she is coming through moves across her
|
|
15
|
-
* without taking her geometry apart. Every amplitude is scaled by how much of
|
|
16
|
-
* the arrival is left, so the water is gone by the time she stands clear. At
|
|
17
|
-
* reveal 1, and in every other state, this is the plain portrait.
|
|
18
|
-
*
|
|
19
|
-
* The water is the window's one shader canvas while it is wanted. The presence
|
|
20
|
-
* holds the canvas budget from the moment the water is asked for until it is
|
|
21
|
-
* torn down, and the room keeps its still field for exactly that long
|
|
22
|
-
* (`canvas-budget.ts`).
|
|
23
|
-
*/
|
|
24
|
-
const ARRIVAL_FLOOR = 0.025;
|
|
25
|
-
const ARRIVAL_CURVE = 1.25;
|
|
26
|
-
const WATER_CAUSTIC = 0.35;
|
|
27
|
-
const WATER_EDGES = 0.4;
|
|
28
|
-
const WATER_WAVES = 0.3;
|
|
29
|
-
const WATER_LAYERING = 0.4;
|
|
30
|
-
const WATER_HIGHLIGHTS = 0.12;
|
|
31
|
-
const WATER_SIZE = 1;
|
|
32
|
-
const WATER_SPEED = 1;
|
|
33
|
-
const WATER_FRAME = 0;
|
|
34
|
-
const WATER_MIN_PIXEL_RATIO = 1;
|
|
35
|
-
const WATER_MAX_PIXEL_COUNT = 1_024_000;
|
|
36
|
-
const PRESENCE_FIELD_TOKEN = "--nc-identity-verde";
|
|
37
|
-
const TRANSPARENT = [0, 0, 0, 0];
|
|
38
|
-
const clampUnit = (value) => Math.min(Math.max(value, 0), 1);
|
|
39
|
-
/** The arrival curve: a trace at the start, not a face already half-built. */
|
|
40
|
-
export function arrivalOpacity(reveal) {
|
|
41
|
-
return ARRIVAL_FLOOR + (1 - ARRIVAL_FLOOR) * clampUnit(reveal) ** ARRIVAL_CURVE;
|
|
42
|
-
}
|
|
43
|
-
function waterUniforms(reveal, field) {
|
|
44
|
-
const remaining = 1 - clampUnit(reveal);
|
|
45
|
-
return {
|
|
46
|
-
u_caustic: WATER_CAUSTIC * remaining,
|
|
47
|
-
u_colorBack: TRANSPARENT,
|
|
48
|
-
u_colorHighlight: field,
|
|
49
|
-
u_edges: WATER_EDGES * remaining,
|
|
50
|
-
u_highlights: WATER_HIGHLIGHTS,
|
|
51
|
-
u_layering: WATER_LAYERING,
|
|
52
|
-
u_size: WATER_SIZE,
|
|
53
|
-
u_waves: WATER_WAVES * remaining,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Nadia at a threshold.
|
|
58
|
-
*
|
|
59
|
-
* `reveal` drives one opacity, so nothing re-renders per frame and the value
|
|
60
|
-
* a Customer sees is the value assistive technology is told. The lower edge
|
|
61
|
-
* dissolves into the room instead of ending on a rectangle. At `state="ready"`
|
|
62
|
-
* she takes one short verde vivo beat, her presence accent, and settles.
|
|
63
|
-
*/
|
|
64
|
-
export function NadiaPresence({ className, label, reveal, src, state }) {
|
|
65
|
-
const still = useReducedMotion();
|
|
66
|
-
const hostRef = useRef(null);
|
|
67
|
-
const mountRef = useRef(null);
|
|
68
|
-
const [surface, setSurface] = useState("portrait");
|
|
69
|
-
const value = clampUnit(reveal);
|
|
70
|
-
const arriving = state === "arriving";
|
|
71
|
-
const wet = arriving && value < 1 && !still;
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const host = hostRef.current;
|
|
74
|
-
if (!host || !wet)
|
|
75
|
-
return;
|
|
76
|
-
const field = resolveIdentityColor(host, PRESENCE_FIELD_TOKEN);
|
|
77
|
-
if (!field)
|
|
78
|
-
return;
|
|
79
|
-
let cancelled = false;
|
|
80
|
-
let canvas = null;
|
|
81
|
-
// Taken before the image decodes: the room stays still while her water is
|
|
82
|
-
// on its way, not only once it paints.
|
|
83
|
-
const releaseCanvas = holdCanvas();
|
|
84
|
-
const clearCanvases = () => {
|
|
85
|
-
for (const element of host.querySelectorAll("canvas"))
|
|
86
|
-
element.remove();
|
|
87
|
-
host.removeAttribute("data-paper-shader");
|
|
88
|
-
};
|
|
89
|
-
const teardown = () => {
|
|
90
|
-
canvas?.removeEventListener("webglcontextlost", handleContextLost);
|
|
91
|
-
canvas = null;
|
|
92
|
-
try {
|
|
93
|
-
mountRef.current?.dispose();
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// The plain portrait is what stays; vendor cleanup failing is not
|
|
97
|
-
// allowed to remove Nadia from the screen.
|
|
98
|
-
}
|
|
99
|
-
mountRef.current = null;
|
|
100
|
-
clearCanvases();
|
|
101
|
-
// After disposal, so the room's canvas is never alive beside this one.
|
|
102
|
-
releaseCanvas();
|
|
103
|
-
};
|
|
104
|
-
function handleContextLost(event) {
|
|
105
|
-
event.preventDefault();
|
|
106
|
-
teardown();
|
|
107
|
-
setSurface("portrait");
|
|
108
|
-
}
|
|
109
|
-
const image = new Image();
|
|
110
|
-
image.decoding = "async";
|
|
111
|
-
image.src = src;
|
|
112
|
-
const mount = async () => {
|
|
113
|
-
try {
|
|
114
|
-
await image.decode();
|
|
115
|
-
if (cancelled)
|
|
116
|
-
return;
|
|
117
|
-
mountRef.current = new ShaderMount(host, waterFragmentShader, {
|
|
118
|
-
...waterUniforms(value, field),
|
|
119
|
-
u_fit: ShaderFitOptions.contain,
|
|
120
|
-
u_image: image,
|
|
121
|
-
u_offsetX: defaultObjectSizing.offsetX,
|
|
122
|
-
u_offsetY: defaultObjectSizing.offsetY,
|
|
123
|
-
u_originX: defaultObjectSizing.originX,
|
|
124
|
-
u_originY: defaultObjectSizing.originY,
|
|
125
|
-
u_rotation: defaultObjectSizing.rotation,
|
|
126
|
-
u_scale: defaultObjectSizing.scale,
|
|
127
|
-
u_worldHeight: defaultObjectSizing.worldHeight,
|
|
128
|
-
u_worldWidth: defaultObjectSizing.worldWidth,
|
|
129
|
-
}, undefined, WATER_SPEED, WATER_FRAME, WATER_MIN_PIXEL_RATIO, WATER_MAX_PIXEL_COUNT, ["u_image"]);
|
|
130
|
-
canvas = mountRef.current.canvasElement;
|
|
131
|
-
canvas.addEventListener("webglcontextlost", handleContextLost);
|
|
132
|
-
setSurface("water");
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// No WebGL, no palette, or an image that will not decode: she arrives
|
|
136
|
-
// as the plain portrait, which is the whole design at reveal 1 anyway.
|
|
137
|
-
teardown();
|
|
138
|
-
setSurface("portrait");
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
void mount();
|
|
142
|
-
return () => {
|
|
143
|
-
cancelled = true;
|
|
144
|
-
teardown();
|
|
145
|
-
setSurface("portrait");
|
|
146
|
-
};
|
|
147
|
-
}, [src, wet]);
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
const host = hostRef.current;
|
|
150
|
-
const mount = mountRef.current;
|
|
151
|
-
if (!host || !mount)
|
|
152
|
-
return;
|
|
153
|
-
const field = resolveIdentityColor(host, PRESENCE_FIELD_TOKEN);
|
|
154
|
-
if (!field)
|
|
155
|
-
return;
|
|
156
|
-
mount.setUniforms(waterUniforms(value, field));
|
|
157
|
-
}, [value]);
|
|
158
|
-
return (_jsxs("div", { className: cn("nc-threshold-presence", className), "data-state": state, "data-still": still || undefined, style: { "--threshold-portrait": `url("${src}")` }, children: [_jsx("div", { "aria-hidden": "true", className: "nc-threshold-presence-figure", "data-surface": surface, ref: hostRef, style: { opacity: arriving ? arrivalOpacity(value) : 1 }, children: _jsx("img", { alt: "", className: "nc-threshold-presence-portrait", draggable: false, src: src }) }), _jsx("div", { "aria-label": label, "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": Math.floor(value * 100), className: "nc-threshold-sr", role: "progressbar" })] }));
|
|
159
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export type ThresholdRoomProps = {
|
|
2
|
-
className?: string;
|
|
3
|
-
/** Repair and error states quiet the room instead of hiding it. */
|
|
4
|
-
dimmed?: boolean;
|
|
5
|
-
/**
|
|
6
|
-
* How much of the room's pace is running, 0 to 1. The captured recipe is
|
|
7
|
-
* energy 1; the first-run screen holds it there while Nadia is still
|
|
8
|
-
* arriving and drops it to 0 when she has, which stills the field. Sign-in,
|
|
9
|
-
* success and every repair state pass 0.
|
|
10
|
-
*/
|
|
11
|
-
energy?: number;
|
|
12
|
-
/** Success: the field's colour becomes verde vivo, Nadia's presence accent. */
|
|
13
|
-
ready?: boolean;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* The threshold's background field.
|
|
17
|
-
*
|
|
18
|
-
* It paints absolutely inside its own relative, isolated parent and never
|
|
19
|
-
* against the viewport, so a Portal panel can hold one without covering the
|
|
20
|
-
* page. It takes no pointer events and carries no words.
|
|
21
|
-
*
|
|
22
|
-
* When WebGL is unavailable, when the design-system tokens are not in the
|
|
23
|
-
* cascade above it, or when the context is lost after mounting, the element
|
|
24
|
-
* keeps its still field: a token-built radial gradient that needs no GPU. The
|
|
25
|
-
* room is never blank.
|
|
26
|
-
*
|
|
27
|
-
* The window carries one shader canvas. While Nadia's water is painting her
|
|
28
|
-
* arrival, the room holds this same still field and mounts nothing; the
|
|
29
|
-
* moment she stands clear the water leaves and the room's canvas is the one
|
|
30
|
-
* that paints (`canvas-budget.ts`).
|
|
31
|
-
*/
|
|
32
|
-
export declare function ThresholdRoom({ className, dimmed, energy, ready, }: ThresholdRoomProps): import("react").JSX.Element;
|
|
33
|
-
//# sourceMappingURL=room.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"room.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/room.tsx"],"names":[],"mappings":"AAwCA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAIF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,MAAc,EACd,MAAU,EACV,KAAa,GACd,EAAE,kBAAkB,+BA+IpB"}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { defaultObjectSizing, meshGradientFragmentShader, ShaderFitOptions, ShaderMount, } from "@paper-design/shaders";
|
|
4
|
-
import { useEffect, useRef, useState } from "react";
|
|
5
|
-
import { resolveIdentityColor } from "../../internal/agent-visual/color.js";
|
|
6
|
-
import { cn } from "../../lib/utils.js";
|
|
7
|
-
import { useCanvasHeld } from "./canvas-budget.js";
|
|
8
|
-
import { useReducedMotion } from "./use-reduced-motion.js";
|
|
9
|
-
/*
|
|
10
|
-
* The room is the place a threshold happens in: one field of verde ink on true
|
|
11
|
-
* black, behind Nadia and behind the words. It is Paper Shaders' published
|
|
12
|
-
* Mesh Gradient "Ink" preset, adapted to two colours and slowed, mounted
|
|
13
|
-
* through the core `ShaderMount` the package already depends on. There is no
|
|
14
|
-
* React wrapper and no second shader engine.
|
|
15
|
-
*
|
|
16
|
-
* Every number below was selected from rendered captures (the study is
|
|
17
|
-
* `apps/nadia/build/design/threshold/`), so this module is the one place they
|
|
18
|
-
* are written.
|
|
19
|
-
*/
|
|
20
|
-
const ROOM_SPEED = 0.22;
|
|
21
|
-
const ROOM_FRAME = 12_000;
|
|
22
|
-
const ROOM_MIN_PIXEL_RATIO = 1;
|
|
23
|
-
const ROOM_MAX_PIXEL_COUNT = 1_024_000;
|
|
24
|
-
const ROOM_DISTORTION = 1;
|
|
25
|
-
const ROOM_SWIRL = 0.2;
|
|
26
|
-
const ROOM_ROTATION = 90;
|
|
27
|
-
/** The field's ink, and the two colours it carries. Uniforms, never CSS. */
|
|
28
|
-
const ROOM_INK_TOKEN = "--nc-identity-ink";
|
|
29
|
-
const ROOM_FIELD_TOKEN = "--nc-identity-verde";
|
|
30
|
-
const ROOM_READY_TOKEN = "--nc-dark-verde-vivo";
|
|
31
|
-
const clampUnit = (value) => Math.min(Math.max(value, 0), 1);
|
|
32
|
-
/**
|
|
33
|
-
* The threshold's background field.
|
|
34
|
-
*
|
|
35
|
-
* It paints absolutely inside its own relative, isolated parent and never
|
|
36
|
-
* against the viewport, so a Portal panel can hold one without covering the
|
|
37
|
-
* page. It takes no pointer events and carries no words.
|
|
38
|
-
*
|
|
39
|
-
* When WebGL is unavailable, when the design-system tokens are not in the
|
|
40
|
-
* cascade above it, or when the context is lost after mounting, the element
|
|
41
|
-
* keeps its still field: a token-built radial gradient that needs no GPU. The
|
|
42
|
-
* room is never blank.
|
|
43
|
-
*
|
|
44
|
-
* The window carries one shader canvas. While Nadia's water is painting her
|
|
45
|
-
* arrival, the room holds this same still field and mounts nothing; the
|
|
46
|
-
* moment she stands clear the water leaves and the room's canvas is the one
|
|
47
|
-
* that paints (`canvas-budget.ts`).
|
|
48
|
-
*/
|
|
49
|
-
export function ThresholdRoom({ className, dimmed = false, energy = 0, ready = false, }) {
|
|
50
|
-
const still = useReducedMotion();
|
|
51
|
-
const hostRef = useRef(null);
|
|
52
|
-
const mountRef = useRef(null);
|
|
53
|
-
const [surface, setSurface] = useState("still");
|
|
54
|
-
const held = useCanvasHeld();
|
|
55
|
-
const speed = still ? 0 : ROOM_SPEED * clampUnit(energy);
|
|
56
|
-
// The deferred mount reads the values current when it runs, not the ones of
|
|
57
|
-
// the render that scheduled it.
|
|
58
|
-
const speedRef = useRef(speed);
|
|
59
|
-
const readyRef = useRef(ready);
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
speedRef.current = speed;
|
|
62
|
-
readyRef.current = ready;
|
|
63
|
-
});
|
|
64
|
-
// The window's one canvas. While Nadia's water holds it the room keeps its
|
|
65
|
-
// still field; when the hold is released the room mounts. Every other prop
|
|
66
|
-
// change is carried into the live mount by the two effects below, so a prop
|
|
67
|
-
// change never rebuilds a WebGL context.
|
|
68
|
-
//
|
|
69
|
-
// The mount is deferred by one microtask. Effects run in tree order, so a
|
|
70
|
-
// presence mounted after the room takes its hold after this effect has run;
|
|
71
|
-
// deferring lets that hold cancel a mount that has not happened yet, and
|
|
72
|
-
// the window never carries two live contexts, not even for one tick.
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
const host = hostRef.current;
|
|
75
|
-
if (!host || held)
|
|
76
|
-
return;
|
|
77
|
-
let cancelled = false;
|
|
78
|
-
let canvas = null;
|
|
79
|
-
const clearCanvases = () => {
|
|
80
|
-
for (const element of host.querySelectorAll("canvas"))
|
|
81
|
-
element.remove();
|
|
82
|
-
host.removeAttribute("data-paper-shader");
|
|
83
|
-
};
|
|
84
|
-
const handleContextLost = (event) => {
|
|
85
|
-
event.preventDefault();
|
|
86
|
-
canvas?.removeEventListener("webglcontextlost", handleContextLost);
|
|
87
|
-
canvas = null;
|
|
88
|
-
try {
|
|
89
|
-
mountRef.current?.dispose();
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// The still field is already behind the canvas; vendor cleanup failing
|
|
93
|
-
// must not take the room down with it.
|
|
94
|
-
}
|
|
95
|
-
mountRef.current = null;
|
|
96
|
-
clearCanvases();
|
|
97
|
-
setSurface("still");
|
|
98
|
-
};
|
|
99
|
-
const mount = () => {
|
|
100
|
-
if (cancelled)
|
|
101
|
-
return;
|
|
102
|
-
const ink = resolveIdentityColor(host, ROOM_INK_TOKEN);
|
|
103
|
-
const field = resolveIdentityColor(host, readyRef.current ? ROOM_READY_TOKEN : ROOM_FIELD_TOKEN);
|
|
104
|
-
// Without the design-system cascade there is no palette to paint with, and
|
|
105
|
-
// a guessed colour would be a second source for a token value.
|
|
106
|
-
if (!ink || !field)
|
|
107
|
-
return;
|
|
108
|
-
try {
|
|
109
|
-
mountRef.current = new ShaderMount(host, meshGradientFragmentShader, {
|
|
110
|
-
u_colors: [field, ink],
|
|
111
|
-
u_colorsCount: 2,
|
|
112
|
-
u_distortion: ROOM_DISTORTION,
|
|
113
|
-
u_fit: ShaderFitOptions.contain,
|
|
114
|
-
u_grainMixer: 0,
|
|
115
|
-
u_grainOverlay: 0,
|
|
116
|
-
u_offsetX: defaultObjectSizing.offsetX,
|
|
117
|
-
u_offsetY: defaultObjectSizing.offsetY,
|
|
118
|
-
u_originX: defaultObjectSizing.originX,
|
|
119
|
-
u_originY: defaultObjectSizing.originY,
|
|
120
|
-
u_rotation: ROOM_ROTATION,
|
|
121
|
-
u_scale: defaultObjectSizing.scale,
|
|
122
|
-
u_swirl: ROOM_SWIRL,
|
|
123
|
-
u_worldHeight: defaultObjectSizing.worldHeight,
|
|
124
|
-
u_worldWidth: defaultObjectSizing.worldWidth,
|
|
125
|
-
}, undefined, speedRef.current, ROOM_FRAME, ROOM_MIN_PIXEL_RATIO, ROOM_MAX_PIXEL_COUNT);
|
|
126
|
-
canvas = mountRef.current.canvasElement;
|
|
127
|
-
canvas.addEventListener("webglcontextlost", handleContextLost);
|
|
128
|
-
setSurface("shader");
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// The constructor prepends its canvas before it reaches the context, so
|
|
132
|
-
// a failed mount leaves one behind that would cover the still field.
|
|
133
|
-
mountRef.current = null;
|
|
134
|
-
clearCanvases();
|
|
135
|
-
setSurface("still");
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
queueMicrotask(mount);
|
|
139
|
-
return () => {
|
|
140
|
-
cancelled = true;
|
|
141
|
-
canvas?.removeEventListener("webglcontextlost", handleContextLost);
|
|
142
|
-
try {
|
|
143
|
-
mountRef.current?.dispose();
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Same reason as above: disposal is best effort.
|
|
147
|
-
}
|
|
148
|
-
mountRef.current = null;
|
|
149
|
-
clearCanvases();
|
|
150
|
-
setSurface("still");
|
|
151
|
-
};
|
|
152
|
-
}, [held]);
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
mountRef.current?.setSpeed(speed);
|
|
155
|
-
}, [speed]);
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
const host = hostRef.current;
|
|
158
|
-
const mount = mountRef.current;
|
|
159
|
-
if (!host || !mount)
|
|
160
|
-
return;
|
|
161
|
-
const ink = resolveIdentityColor(host, ROOM_INK_TOKEN);
|
|
162
|
-
const field = resolveIdentityColor(host, ready ? ROOM_READY_TOKEN : ROOM_FIELD_TOKEN);
|
|
163
|
-
if (!ink || !field)
|
|
164
|
-
return;
|
|
165
|
-
mount.setUniforms({ u_colors: [field, ink] });
|
|
166
|
-
}, [ready]);
|
|
167
|
-
return (_jsx("div", { "aria-hidden": "true", className: cn("nc-threshold-room", className), "data-dimmed": dimmed || undefined, "data-ready": ready || undefined, "data-surface": surface, ref: hostRef }));
|
|
168
|
-
}
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @nadicodeai/ui/threshold.css
|
|
3
|
-
*
|
|
4
|
-
* The threshold primitives' stylesheet. It is a separate import rather than a
|
|
5
|
-
* part of globals.css because the surface that needs it most, the Nadia
|
|
6
|
-
* desktop app, runs inside a shell that does not load Tailwind: it takes this
|
|
7
|
-
* file and the design-system token layer, and nothing else from this package.
|
|
8
|
-
*
|
|
9
|
-
* A threshold is a fixed dark surface. It is true black in every theme, so the
|
|
10
|
-
* ladders below read the dark-mode tokens directly instead of branching on the
|
|
11
|
-
* app's mode; that is the fixed-presentation escape the package contract
|
|
12
|
-
* names, taken once, coherently, for the whole surface.
|
|
13
|
-
*
|
|
14
|
-
* The room's two colours are shader uniforms, not CSS, and live in room.tsx.
|
|
15
|
-
* The only colour written here is the still field the room falls back to.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/* The room ---------------------------------------------------------------- */
|
|
19
|
-
|
|
20
|
-
/*
|
|
21
|
-
* Absolute inside its own relative, isolated parent, never fixed against the
|
|
22
|
-
* viewport: a Portal panel holds a room without covering the page. Its still
|
|
23
|
-
* field is a token-built radial that needs no GPU, and the canvas covers it
|
|
24
|
-
* once the shader is painting.
|
|
25
|
-
*/
|
|
26
|
-
.nc-threshold-room {
|
|
27
|
-
position: absolute;
|
|
28
|
-
inset: 0;
|
|
29
|
-
z-index: 0;
|
|
30
|
-
overflow: hidden;
|
|
31
|
-
pointer-events: none;
|
|
32
|
-
background: radial-gradient(
|
|
33
|
-
ellipse 55% 46% at 50% 36%,
|
|
34
|
-
color-mix(in oklab, var(--nc-identity-verde) 30%, var(--nc-identity-ink)),
|
|
35
|
-
transparent 90%
|
|
36
|
-
);
|
|
37
|
-
mask-image: radial-gradient(ellipse 65% 58% at 50% 35%, black 10%, transparent 95%);
|
|
38
|
-
transition: opacity var(--nc-duration-open) var(--nc-ease-emphasized);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.nc-threshold-room canvas {
|
|
42
|
-
opacity: 0.65;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* Repair and error states quiet the room; they never remove it. */
|
|
46
|
-
.nc-threshold-room[data-dimmed] {
|
|
47
|
-
opacity: 0.22;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* The presence ------------------------------------------------------------ */
|
|
51
|
-
|
|
52
|
-
.nc-threshold-presence {
|
|
53
|
-
position: relative;
|
|
54
|
-
flex: none;
|
|
55
|
-
width: clamp(180px, 38vh, 320px);
|
|
56
|
-
aspect-ratio: 462 / 520;
|
|
57
|
-
transition: opacity var(--nc-duration-confirm) var(--nc-ease-emphasized);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/* Her lower edge dissolves into the room rather than ending on a rectangle. */
|
|
61
|
-
.nc-threshold-presence-figure {
|
|
62
|
-
position: relative;
|
|
63
|
-
width: 100%;
|
|
64
|
-
height: 100%;
|
|
65
|
-
mask-image: linear-gradient(black 55%, transparent 98%);
|
|
66
|
-
transition: opacity var(--nc-duration-confirm) var(--nc-ease-emphasized);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.nc-threshold-presence-portrait {
|
|
70
|
-
display: block;
|
|
71
|
-
width: 100%;
|
|
72
|
-
height: 100%;
|
|
73
|
-
object-fit: contain;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/* While the water is painting her, the plain portrait beneath it is redundant. */
|
|
77
|
-
.nc-threshold-presence-figure[data-surface="water"] .nc-threshold-presence-portrait {
|
|
78
|
-
visibility: hidden;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/*
|
|
82
|
-
* The water canvas is cut to her own silhouette. Water's highlight term is
|
|
83
|
-
* added to the whole frame, not only to the sampled image, so an unmasked
|
|
84
|
-
* canvas paints a faintly lit rectangle over the room: the visible tile the
|
|
85
|
-
* rendered comparisons rejected. The mask matches the shader's own `contain`
|
|
86
|
-
* fit and centred origin, so the two agree pixel for pixel.
|
|
87
|
-
*/
|
|
88
|
-
.nc-threshold-presence-figure canvas {
|
|
89
|
-
position: absolute;
|
|
90
|
-
inset: 0;
|
|
91
|
-
mask-image: var(--threshold-portrait);
|
|
92
|
-
mask-position: center;
|
|
93
|
-
mask-repeat: no-repeat;
|
|
94
|
-
mask-size: contain;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.nc-threshold-presence[data-state="error"] {
|
|
98
|
-
opacity: 0.62;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/*
|
|
102
|
-
* Success: one short verde vivo beat over her own silhouette, then nothing.
|
|
103
|
-
* Verde vivo is Nadia's presence accent and this is the only place a threshold
|
|
104
|
-
* spends it, which is why the buttons beside it stay neutral.
|
|
105
|
-
*/
|
|
106
|
-
.nc-threshold-presence::after {
|
|
107
|
-
content: "";
|
|
108
|
-
position: absolute;
|
|
109
|
-
inset: 0;
|
|
110
|
-
background: var(--nc-dark-verde-vivo);
|
|
111
|
-
mask-image: var(--threshold-portrait);
|
|
112
|
-
mask-position: center;
|
|
113
|
-
mask-repeat: no-repeat;
|
|
114
|
-
mask-size: contain;
|
|
115
|
-
opacity: 0;
|
|
116
|
-
pointer-events: none;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.nc-threshold-presence[data-state="ready"]::after {
|
|
120
|
-
animation: nc-threshold-arrival 360ms var(--nc-ease-out-strong) 1;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@keyframes nc-threshold-arrival {
|
|
124
|
-
0% {
|
|
125
|
-
opacity: 0;
|
|
126
|
-
}
|
|
127
|
-
35% {
|
|
128
|
-
opacity: 0.22;
|
|
129
|
-
}
|
|
130
|
-
100% {
|
|
131
|
-
opacity: 0;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/* The orb ----------------------------------------------------------------- */
|
|
136
|
-
|
|
137
|
-
.nc-threshold-orb {
|
|
138
|
-
display: block;
|
|
139
|
-
flex: none;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* The line ---------------------------------------------------------------- */
|
|
143
|
-
|
|
144
|
-
/*
|
|
145
|
-
* A hidden copy of the whole sentence holds the width while the visible one
|
|
146
|
-
* fills in, so the orb beside it does not travel with each character.
|
|
147
|
-
*/
|
|
148
|
-
.nc-threshold-line {
|
|
149
|
-
display: grid;
|
|
150
|
-
position: relative;
|
|
151
|
-
margin: 0;
|
|
152
|
-
font-family: var(--nc-type-display-md-font-family);
|
|
153
|
-
font-size: var(--nc-type-display-md-font-size);
|
|
154
|
-
font-weight: var(--nc-type-display-md-font-weight);
|
|
155
|
-
line-height: var(--nc-type-display-md-line-height);
|
|
156
|
-
letter-spacing: -0.01em;
|
|
157
|
-
text-align: center;
|
|
158
|
-
text-wrap: balance;
|
|
159
|
-
color: var(--nc-dark-ink);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.nc-threshold-line-measure {
|
|
163
|
-
grid-area: 1 / 1;
|
|
164
|
-
visibility: hidden;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.nc-threshold-line-spoken {
|
|
168
|
-
grid-area: 1 / 1;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.nc-threshold-caret {
|
|
172
|
-
display: inline-block;
|
|
173
|
-
width: 2px;
|
|
174
|
-
height: 1em;
|
|
175
|
-
margin-left: 2px;
|
|
176
|
-
vertical-align: -0.15em;
|
|
177
|
-
background: currentcolor;
|
|
178
|
-
visibility: hidden;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.nc-threshold-line[data-typing] .nc-threshold-caret {
|
|
182
|
-
visibility: visible;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/* Shared ------------------------------------------------------------------ */
|
|
186
|
-
|
|
187
|
-
/*
|
|
188
|
-
* The threshold ships its own visually-hidden helper: the desktop shell does
|
|
189
|
-
* not load this package's Tailwind layer, and a progress value nobody can read
|
|
190
|
-
* is not an accessible progress value.
|
|
191
|
-
*/
|
|
192
|
-
.nc-threshold-sr {
|
|
193
|
-
position: absolute;
|
|
194
|
-
width: 1px;
|
|
195
|
-
height: 1px;
|
|
196
|
-
overflow: hidden;
|
|
197
|
-
clip-path: inset(50%);
|
|
198
|
-
white-space: nowrap;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
@media (prefers-reduced-motion: reduce) {
|
|
202
|
-
.nc-threshold-room,
|
|
203
|
-
.nc-threshold-presence,
|
|
204
|
-
.nc-threshold-presence-figure {
|
|
205
|
-
transition: none;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
.nc-threshold-presence[data-state="ready"]::after {
|
|
209
|
-
animation: none;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.nc-threshold-caret {
|
|
213
|
-
visibility: hidden;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/*
|
|
218
|
-
* The components also carry the preference as `data-still`, which is what a
|
|
219
|
-
* shell that toggles reduced motion without the media query (an in-app
|
|
220
|
-
* setting) sets. Same result, one rule.
|
|
221
|
-
*/
|
|
222
|
-
.nc-threshold-presence[data-still] {
|
|
223
|
-
transition: none;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.nc-threshold-presence[data-still][data-state="ready"]::after {
|
|
227
|
-
animation: none;
|
|
228
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export type TypedLineProps = {
|
|
2
|
-
/** Milliseconds per character. The threshold's cadence is 44. */
|
|
3
|
-
cadenceMs?: number;
|
|
4
|
-
className?: string;
|
|
5
|
-
text: string;
|
|
6
|
-
};
|
|
7
|
-
export declare function TypedLine({ cadenceMs, className, text }: TypedLineProps): import("react").JSX.Element;
|
|
8
|
-
//# sourceMappingURL=typed-line.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"typed-line.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/typed-line.tsx"],"names":[],"mappings":"AAwBA,MAAM,MAAM,cAAc,GAAG;IAC3B,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,wBAAgB,SAAS,CAAC,EAAE,SAAsB,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,cAAc,+BAwCpF"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { cn } from "../../lib/utils.js";
|
|
5
|
-
import { useReducedMotion } from "./use-reduced-motion.js";
|
|
6
|
-
/*
|
|
7
|
-
* The line Nadia says while a threshold is open. It types itself, because a
|
|
8
|
-
* first run is the one moment in the product where waiting is the content.
|
|
9
|
-
*
|
|
10
|
-
* Three decisions the captures settled:
|
|
11
|
-
*
|
|
12
|
-
* - the count comes from elapsed time, not from how many callbacks arrived, so
|
|
13
|
-
* a software renderer or a busy main thread cannot stretch a one second
|
|
14
|
-
* sentence into five;
|
|
15
|
-
* - a hidden copy of the whole sentence reserves its width, so nothing beside
|
|
16
|
-
* the line travels sideways with each character;
|
|
17
|
-
* - the live region receives the complete sentence at once, so a screen reader
|
|
18
|
-
* is never read one letter at a time.
|
|
19
|
-
*/
|
|
20
|
-
const CADENCE_MS = 44;
|
|
21
|
-
export function TypedLine({ cadenceMs = CADENCE_MS, className, text }) {
|
|
22
|
-
const still = useReducedMotion();
|
|
23
|
-
const [shown, setShown] = useState("");
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (still) {
|
|
26
|
-
setShown(text);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
setShown("");
|
|
30
|
-
const startedAt = performance.now();
|
|
31
|
-
let timer = 0;
|
|
32
|
-
const tick = () => {
|
|
33
|
-
const count = Math.min(text.length, Math.floor((performance.now() - startedAt) / cadenceMs));
|
|
34
|
-
setShown(text.slice(0, count));
|
|
35
|
-
if (count < text.length)
|
|
36
|
-
timer = window.setTimeout(tick, cadenceMs);
|
|
37
|
-
};
|
|
38
|
-
timer = window.setTimeout(tick, cadenceMs);
|
|
39
|
-
return () => window.clearTimeout(timer);
|
|
40
|
-
}, [cadenceMs, still, text]);
|
|
41
|
-
const typing = shown.length < text.length;
|
|
42
|
-
return (_jsxs("p", { className: cn("nc-threshold-line", className), "data-typing": typing || undefined, children: [_jsx("span", { "aria-hidden": "true", className: "nc-threshold-line-measure", children: text }), _jsxs("span", { "aria-hidden": "true", className: "nc-threshold-line-spoken", children: [_jsx("span", { children: shown }), _jsx("span", { className: "nc-threshold-caret" })] }), _jsx("span", { "aria-live": "polite", className: "nc-threshold-sr", children: text })] }));
|
|
43
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live `prefers-reduced-motion`, the one reader the threshold primitives share.
|
|
3
|
-
*
|
|
4
|
-
* The value is read from the media query on every render rather than cached in
|
|
5
|
-
* state, so a preference changed while a threshold is open reaches the room,
|
|
6
|
-
* the presence, the orb and the typed line in the same commit. Server renders
|
|
7
|
-
* report false and the first client render corrects it.
|
|
8
|
-
*/
|
|
9
|
-
export declare function useReducedMotion(): boolean;
|
|
10
|
-
//# sourceMappingURL=use-reduced-motion.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-reduced-motion.d.ts","sourceRoot":"","sources":["../../../src/components/threshold/use-reduced-motion.ts"],"names":[],"mappings":"AA2BA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C"}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useSyncExternalStore } from "react";
|
|
3
|
-
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
4
|
-
function query() {
|
|
5
|
-
return typeof window === "undefined" || typeof window.matchMedia !== "function"
|
|
6
|
-
? undefined
|
|
7
|
-
: window.matchMedia(REDUCED_MOTION_QUERY);
|
|
8
|
-
}
|
|
9
|
-
function subscribe(onChange) {
|
|
10
|
-
const list = query();
|
|
11
|
-
if (!list)
|
|
12
|
-
return () => { };
|
|
13
|
-
list.addEventListener("change", onChange);
|
|
14
|
-
return () => list.removeEventListener("change", onChange);
|
|
15
|
-
}
|
|
16
|
-
function getSnapshot() {
|
|
17
|
-
return query()?.matches ?? false;
|
|
18
|
-
}
|
|
19
|
-
function getServerSnapshot() {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Live `prefers-reduced-motion`, the one reader the threshold primitives share.
|
|
24
|
-
*
|
|
25
|
-
* The value is read from the media query on every render rather than cached in
|
|
26
|
-
* state, so a preference changed while a threshold is open reaches the room,
|
|
27
|
-
* the presence, the orb and the typed line in the same commit. Server renders
|
|
28
|
-
* report false and the first client render corrects it.
|
|
29
|
-
*/
|
|
30
|
-
export function useReducedMotion() {
|
|
31
|
-
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
32
|
-
}
|
package/dist/threshold.d.ts
DELETED
package/dist/threshold.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"threshold.d.ts","sourceRoot":"","sources":["../src/threshold.ts"],"names":[],"mappings":"AAYA,cAAc,8BAA8B,CAAC"}
|
package/dist/threshold.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* `@nadicodeai/ui/threshold`: the primitives every threshold is drawn from.
|
|
3
|
-
*
|
|
4
|
-
* A threshold is the moment before the product is usable: a first run, a
|
|
5
|
-
* sign-in, a repair. The Nadia desktop app draws its room, presence, status
|
|
6
|
-
* orb and typed line from here, so the brand logic lives in one place.
|
|
7
|
-
*
|
|
8
|
-
* The screens themselves, and the state machines behind them, stay in the
|
|
9
|
-
* consuming app. This entry ships no screen.
|
|
10
|
-
*
|
|
11
|
-
* The stylesheet is a separate import: `@nadicodeai/ui/threshold.css`.
|
|
12
|
-
*/
|
|
13
|
-
export * from "./components/threshold/index.js";
|
package/docs/threshold.md
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# The threshold primitives
|
|
2
|
-
|
|
3
|
-
`@nadicodeai/ui/threshold` holds the four primitives a threshold is drawn from. A
|
|
4
|
-
threshold is the moment before the product is usable: a first run, a sign-in, a
|
|
5
|
-
repair. The Nadia desktop app draws its room, presence, status orb and typed line
|
|
6
|
-
from here, so the brand logic has one home.
|
|
7
|
-
|
|
8
|
-
The entry ships no screen and no state machine. Which view is on, what the
|
|
9
|
-
words are, and when a threshold ends belong to the consuming app.
|
|
10
|
-
|
|
11
|
-
```tsx
|
|
12
|
-
import { NadiaPresence, Orb, ThresholdRoom, TypedLine } from "@nadicodeai/ui/threshold";
|
|
13
|
-
import "@nadicodeai/ui/threshold.css";
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## What each part is for
|
|
17
|
-
|
|
18
|
-
| Part | Its one job | Never |
|
|
19
|
-
| --- | --- | --- |
|
|
20
|
-
| `ThresholdRoom` | the place: a verde field on true black | inside the working app |
|
|
21
|
-
| `NadiaPresence` | the host: her arrival is the progress | a loader, a ring, a bar |
|
|
22
|
-
| `Orb` | system work underway, through the shared activity renderer | reporting a failed operation or waiting on a person as ongoing work |
|
|
23
|
-
| `TypedLine` | what Nadia is saying while you wait | a control or state machine |
|
|
24
|
-
|
|
25
|
-
The logo is not on this list. It belongs to window chrome, the tray and the
|
|
26
|
-
About row, never to a threshold.
|
|
27
|
-
|
|
28
|
-
## The stylesheet
|
|
29
|
-
|
|
30
|
-
`@nadicodeai/ui/threshold.css` is a separate import rather than a part of
|
|
31
|
-
`globals.css`, because the surface that needs it most does not load Tailwind:
|
|
32
|
-
the Nadia desktop shell takes this file and the design-system token layer and
|
|
33
|
-
nothing else from this package. A Tailwind consumer imports it beside
|
|
34
|
-
`@nadicodeai/ui/globals.css`.
|
|
35
|
-
|
|
36
|
-
Both the stylesheet and the room need the design-system tokens above them, from
|
|
37
|
-
`@nadicodeai/ui/globals.css` or `@nadicodeai/design-system/css`. Without that
|
|
38
|
-
cascade there is no palette, and the room paints its still field rather than
|
|
39
|
-
guessing a colour.
|
|
40
|
-
|
|
41
|
-
## `ThresholdRoom`
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
<div className="relative isolate overflow-hidden">
|
|
45
|
-
<ThresholdRoom energy={arriving ? 1 : 0} dimmed={repairing} ready={connected} />
|
|
46
|
-
{/* the presence and the words, above it */}
|
|
47
|
-
</div>
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
| Prop | Type | Default | Meaning |
|
|
51
|
-
| --- | --- | --- | --- |
|
|
52
|
-
| `dimmed` | `boolean` | `false` | a repair or an error quiets the room to 22 percent |
|
|
53
|
-
| `energy` | `number` | `0` | how much of the room's pace runs, 0 to 1 |
|
|
54
|
-
| `ready` | `boolean` | `false` | success: the field takes verde vivo |
|
|
55
|
-
| `className` | `string` | — | added to the root |
|
|
56
|
-
|
|
57
|
-
The room paints absolutely inside its own relative, isolated parent and never
|
|
58
|
-
against the viewport, so a bounded surface can hold one without covering the
|
|
59
|
-
page. It is `aria-hidden`, takes no pointer events, and carries no words.
|
|
60
|
-
|
|
61
|
-
**The recipe.** Paper Shaders 0.0.78 `meshGradientFragmentShader`, its published
|
|
62
|
-
Ink preset adapted to two colours and slowed: `u_colors` are
|
|
63
|
-
`--nc-identity-verde` and `--nc-identity-ink`, `u_colorsCount` 2, `u_distortion`
|
|
64
|
-
1, `u_swirl` 0.2, `u_rotation` 90, `u_grainMixer` and `u_grainOverlay` 0, fit
|
|
65
|
-
contain, scale 1. Mount speed is 0.22 at energy 1, starting frame 12000, minimum
|
|
66
|
-
pixel ratio 1, maximum pixel count 1,024,000, canvas opacity 0.65. The room is
|
|
67
|
-
masked with `radial-gradient(ellipse 65% 58% at 50% 35%, black 10%, transparent
|
|
68
|
-
95%)`. The two colours are shader uniforms, never CSS.
|
|
69
|
-
|
|
70
|
-
`energy` is a scalar so a screen can decide. The captured recipe is energy 1;
|
|
71
|
-
Nadia's first run holds it there while she is still arriving and drops it to 0
|
|
72
|
-
when she has, which stills the field. Sign-in, success and every repair state
|
|
73
|
-
pass 0.
|
|
74
|
-
|
|
75
|
-
**One canvas per window.** While `NadiaPresence` is painting her arrival with
|
|
76
|
-
water, the room mounts nothing and shows this same still field; the moment the
|
|
77
|
-
water leaves, at reveal 1 or when it cannot paint, the room's canvas mounts.
|
|
78
|
-
The budget lives in the package, so a surface that composes both, as first run
|
|
79
|
-
does, keeps the register's one shader canvas without doing anything.
|
|
80
|
-
|
|
81
|
-
**No GPU, no tokens, or a lost context.** The room keeps its still field: a
|
|
82
|
-
token-built radial gradient that needs no WebGL. It carries `data-surface`,
|
|
83
|
-
`"shader"` while the canvas paints and `"still"` otherwise, and a mount that
|
|
84
|
-
fails leaves no dead canvas covering the fallback. A `webglcontextlost` event
|
|
85
|
-
returns it to the still field rather than to a black rectangle.
|
|
86
|
-
|
|
87
|
-
**Reduced motion.** Speed 0: the shader paints frame 12000 once and stops. The
|
|
88
|
-
room still has its colour and its shape; it just holds one frame.
|
|
89
|
-
|
|
90
|
-
## `NadiaPresence`
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
<NadiaPresence label={t("install.progress")} reveal={fraction} src={portrait} state="arriving" />
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
| Prop | Type | Meaning |
|
|
97
|
-
| --- | --- | --- |
|
|
98
|
-
| `label` | `string` | the accessible name of the arrival |
|
|
99
|
-
| `reveal` | `number` | 0 behind the field, 1 standing clear |
|
|
100
|
-
| `src` | `string` | the portrait master's URL |
|
|
101
|
-
| `state` | `"arriving" \| "present" \| "ready" \| "error"` | which threshold she is in |
|
|
102
|
-
|
|
103
|
-
`src` is a prop rather than an import because this package imports no binary
|
|
104
|
-
asset of its own; the consuming surface resolves
|
|
105
|
-
`@nadicodeai/design-system/assets/nadia-solid-surface` with its own bundler and
|
|
106
|
-
passes the URL. Keeping it out of the library is what lets a Node or a
|
|
107
|
-
bundler-free consumer load the module at all.
|
|
108
|
-
|
|
109
|
-
**The arrival.** Opacity is `0.025 + 0.975 × reveal^1.25`, exported as
|
|
110
|
-
`arrivalOpacity` so a screen can read the same curve. At the start she is a
|
|
111
|
-
trace, which gives the arrival somewhere to go. Her lower edge dissolves into
|
|
112
|
-
the room with `linear-gradient(black 55%, transparent 98%)` instead of ending on
|
|
113
|
-
a rectangle. The `error` state multiplies her by 0.62.
|
|
114
|
-
|
|
115
|
-
**The water.** While `state` is `arriving`, `reveal` is below 1 and motion is
|
|
116
|
-
allowed, the portrait is painted by Paper's `waterFragmentShader` at a
|
|
117
|
-
restrained intensity: `caustic`, `edges` and `waves` are `0.35`, `0.4` and `0.3`
|
|
118
|
-
scaled by how much of the arrival is left, with `layering` 0.4 and `highlights`
|
|
119
|
-
0.12, `colorBack` fully transparent and `colorHighlight` on
|
|
120
|
-
`--nc-identity-verde`. Every amplitude reaches zero as she arrives, so her
|
|
121
|
-
geometry holds and the water is gone by the time she stands clear. At reveal 1,
|
|
122
|
-
and in every other state, this is the plain portrait.
|
|
123
|
-
|
|
124
|
-
While the water is wanted it is the window's one shader canvas: the presence
|
|
125
|
-
holds the canvas budget from the moment it asks for the water until the water
|
|
126
|
-
is torn down, and the room keeps its still field for exactly that long.
|
|
127
|
-
|
|
128
|
-
The water canvas is masked to her own silhouette. Water adds its highlight term
|
|
129
|
-
to the whole frame rather than only to the sampled image, so an unmasked canvas
|
|
130
|
-
paints a faintly lit rectangle over the room; the mask matches the shader's own
|
|
131
|
-
contain fit and centred origin.
|
|
132
|
-
|
|
133
|
-
**Reduced motion, no GPU, or an image that will not decode.** The plain
|
|
134
|
-
portrait, at the same reveal. The figure carries `data-surface`, `"water"` or
|
|
135
|
-
`"portrait"`.
|
|
136
|
-
|
|
137
|
-
**Success.** `state="ready"` runs one 360 ms verde vivo beat over her silhouette
|
|
138
|
-
on `--nc-ease-out-strong`, opacity 0 to 0.22 at 35 percent and back to 0, once.
|
|
139
|
-
Verde vivo is Nadia's presence accent and this is the only place a threshold
|
|
140
|
-
spends it, which is why the buttons beside it stay neutral. Reduced motion
|
|
141
|
-
suppresses the beat.
|
|
142
|
-
|
|
143
|
-
**Assistive technology.** A hidden `progressbar` carries `label` and the floored
|
|
144
|
-
percentage, so it reaches 100 only when the arrival is actually finished. The
|
|
145
|
-
portrait itself is decorative; the words are the typed line's.
|
|
146
|
-
|
|
147
|
-
## `Orb`
|
|
148
|
-
|
|
149
|
-
```tsx
|
|
150
|
-
<Orb label={t("install.working")} state="working" />
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
| Prop | Type | Default | Meaning |
|
|
154
|
-
| --- | --- | --- | --- |
|
|
155
|
-
| `label` | `string` | — | what the orb means, in the Customer's language |
|
|
156
|
-
| `size` | `88 \| 64 \| 20` | `88` | CSS pixels: threshold, region, or inline operation; see `activity.md` |
|
|
157
|
-
| `state` | `"working" \| "connecting" \| "absent"` | — | see below |
|
|
158
|
-
|
|
159
|
-
The threshold adapter preserves its public states: `working` selects the
|
|
160
|
-
shared `processing` activity, `connecting` selects `connecting`, and `absent`
|
|
161
|
-
renders nothing. The threshold defaults to fixed dark ink; `theme="auto"`
|
|
162
|
-
inherits the surrounding product's ink and `theme="light"` fixes light-surface
|
|
163
|
-
ink. There is one renderer, `ActivityOrb`, rather than a second threshold orb.
|
|
164
|
-
|
|
165
|
-
[System activity](activity.md) owns the motion recipes, colour treatment,
|
|
166
|
-
optical sizes, reduced-motion behaviour and connection-versus-person-waiting
|
|
167
|
-
rule. Applications retain their operation state machines and copy.
|
|
168
|
-
|
|
169
|
-
## `TypedLine`
|
|
170
|
-
|
|
171
|
-
```tsx
|
|
172
|
-
<TypedLine text={t("install.step.1")} />
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
| Prop | Type | Default |
|
|
176
|
-
| --- | --- | --- |
|
|
177
|
-
| `cadenceMs` | `number` | `44` |
|
|
178
|
-
| `text` | `string` | — |
|
|
179
|
-
| `className` | `string` | — |
|
|
180
|
-
|
|
181
|
-
The line types itself at 44 ms per character, the cadence the rendered
|
|
182
|
-
comparisons selected for a first-run pause. Three things it does that a naive
|
|
183
|
-
typewriter does not:
|
|
184
|
-
|
|
185
|
-
- the count comes from elapsed time, not from how many callbacks arrived, so a
|
|
186
|
-
software renderer or a busy main thread cannot stretch a one second sentence
|
|
187
|
-
into five;
|
|
188
|
-
- a hidden copy of the whole sentence reserves its width, so the orb beside it
|
|
189
|
-
does not travel sideways with each character;
|
|
190
|
-
- the live region receives the complete sentence at once, so a screen reader is
|
|
191
|
-
never read one letter at a time.
|
|
192
|
-
|
|
193
|
-
A new `text` retypes from a cleared line. The caret is steady while typing and
|
|
194
|
-
hidden afterwards.
|
|
195
|
-
|
|
196
|
-
**Reduced motion.** The whole line at once, and no caret.
|
|
197
|
-
|
|
198
|
-
## Tokens, and the one fixed presentation
|
|
199
|
-
|
|
200
|
-
Everything visual comes from design-system tokens. A threshold is a fixed dark
|
|
201
|
-
surface: it is true black in every theme, so the text ladder reads
|
|
202
|
-
`--nc-dark-ink`, `--nc-dark-body` and `--nc-dark-muted` directly instead of
|
|
203
|
-
branching on the app's mode. That is the fixed-presentation escape
|
|
204
|
-
[`contract.md`](contract.md) names, taken once and coherently for the whole
|
|
205
|
-
surface.
|
|
206
|
-
|
|
207
|
-
The room's two colours are the exception that proves it: they are shader
|
|
208
|
-
uniforms read off `--nc-identity-verde` and `--nc-identity-ink` at mount, not
|
|
209
|
-
CSS values.
|
|
210
|
-
|
|
211
|
-
## Motion budget
|
|
212
|
-
|
|
213
|
-
The foundation's: `--nc-duration-open` for a room fading in or out,
|
|
214
|
-
`--nc-duration-confirm` for the presence, `--nc-ease-emphasized` and
|
|
215
|
-
`--nc-ease-out-strong`. The success beat is the one
|
|
216
|
-
duration written here rather than taken from a token, 360 ms, because it is an
|
|
217
|
-
envelope rather than a state change.
|
|
218
|
-
|
|
219
|
-
Under `prefers-reduced-motion: reduce` the room holds one frame, the water is
|
|
220
|
-
not mounted, the presence jumps to its value, the beat does not run, the orb
|
|
221
|
-
draws once and the line appears whole.
|