@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.
Files changed (32) hide show
  1. package/AGENTS.md +2 -3
  2. package/dist/components/activity-orb.d.ts +1 -1
  3. package/dist/components/activity-orb.d.ts.map +1 -1
  4. package/docs/activity.md +4 -5
  5. package/docs/contract.md +15 -4
  6. package/package.json +2 -7
  7. package/dist/components/threshold/canvas-budget.d.ts +0 -10
  8. package/dist/components/threshold/canvas-budget.d.ts.map +0 -1
  9. package/dist/components/threshold/canvas-budget.js +0 -54
  10. package/dist/components/threshold/index.d.ts +0 -6
  11. package/dist/components/threshold/index.d.ts.map +0 -1
  12. package/dist/components/threshold/index.js +0 -5
  13. package/dist/components/threshold/orb.d.ts +0 -14
  14. package/dist/components/threshold/orb.d.ts.map +0 -1
  15. package/dist/components/threshold/orb.js +0 -17
  16. package/dist/components/threshold/presence.d.ts +0 -27
  17. package/dist/components/threshold/presence.d.ts.map +0 -1
  18. package/dist/components/threshold/presence.js +0 -159
  19. package/dist/components/threshold/room.d.ts +0 -33
  20. package/dist/components/threshold/room.d.ts.map +0 -1
  21. package/dist/components/threshold/room.js +0 -168
  22. package/dist/components/threshold/threshold.css +0 -228
  23. package/dist/components/threshold/typed-line.d.ts +0 -8
  24. package/dist/components/threshold/typed-line.d.ts.map +0 -1
  25. package/dist/components/threshold/typed-line.js +0 -43
  26. package/dist/components/threshold/use-reduced-motion.d.ts +0 -10
  27. package/dist/components/threshold/use-reduced-motion.d.ts.map +0 -1
  28. package/dist/components/threshold/use-reduced-motion.js +0 -32
  29. package/dist/threshold.d.ts +0 -2
  30. package/dist/threshold.d.ts.map +0 -1
  31. package/dist/threshold.js +0 -13
  32. 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
- - First run, sign-in, and repair surfaces: `docs/threshold.md`; the
13
- primitives are `@nadicodeai/ui/threshold`, with
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 at an application threshold. */
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,qEAAqE;IACrE,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"}
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 for an application threshold. The 20 preset is
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. The existing `Orb` threshold adapter
107
- keeps its public API: `working` maps to `processing`, `connecting` maps to
108
- `connecting`, and `absent` mounts nothing. Its fixed dark appearance stays at
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/threshold.css` is the one stylesheet outside it, because the surface that needs it most loads no Tailwind layer at all: the Nadia desktop shell takes that file and the design system's token artifact and nothing else. A React caller imports it beside globals.css, never instead of it.
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. The existing threshold `Orb` adapts its public states to
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. `@nadicodeai/ui/threshold` is the plan-approved package surface for the room, Nadia's presence, the status orb and the typed line used by the Nadia desktop app. The screen, its copy and its state machine stay in the consuming app, and the entry ships no screen. Its contract is [`threshold.md`](threshold.md). A component left unconsumed after its target migration ships gets removed.
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": "6.6.2",
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": "6.6.2",
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,5 +0,0 @@
1
- export { Orb } from "./orb.js";
2
- export { arrivalOpacity, NadiaPresence, } from "./presence.js";
3
- export { ThresholdRoom } from "./room.js";
4
- export { TypedLine } from "./typed-line.js";
5
- export { useReducedMotion } from "./use-reduced-motion.js";
@@ -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
- }
@@ -1,2 +0,0 @@
1
- export * from "./components/threshold/index.js";
2
- //# sourceMappingURL=threshold.d.ts.map
@@ -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.