@nadicodeai/ui 8.6.0 → 9.0.1

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 (72) hide show
  1. package/AGENTS.md +1 -1
  2. package/LICENSES/THINKING-ORBS-MIT.txt +21 -0
  3. package/NOTICE +7 -10
  4. package/dist/components/agent-orb-appearance.d.ts +54 -0
  5. package/dist/components/agent-orb-appearance.d.ts.map +1 -0
  6. package/dist/components/agent-orb-appearance.js +87 -0
  7. package/dist/components/agent-orb.d.ts +5 -3
  8. package/dist/components/agent-orb.d.ts.map +1 -1
  9. package/dist/index.d.ts +0 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +0 -2
  12. package/dist/internal/agent-orb-motion.d.ts +3 -3
  13. package/dist/internal/agent-orb-motion.d.ts.map +1 -1
  14. package/dist/internal/agent-orb-motion.js +1 -0
  15. package/dist/internal/agent-orb.d.ts +11 -1
  16. package/dist/internal/agent-orb.d.ts.map +1 -1
  17. package/dist/internal/agent-orb.js +80 -5
  18. package/docs/agent-orb.md +47 -14
  19. package/docs/contract.md +8 -29
  20. package/docs/migration/9.0.0.md +35 -0
  21. package/package.json +2 -3
  22. package/LICENSES/PAPER-SHADERS-APACHE-2.0.txt +0 -186
  23. package/dist/components/agent-visual.d.ts +0 -17
  24. package/dist/components/agent-visual.d.ts.map +0 -1
  25. package/dist/components/agent-visual.js +0 -63
  26. package/dist/components/forma-appearance.d.ts +0 -13
  27. package/dist/components/forma-appearance.d.ts.map +0 -1
  28. package/dist/components/forma-appearance.js +0 -11
  29. package/dist/components/forma-avatar.d.ts +0 -23
  30. package/dist/components/forma-avatar.d.ts.map +0 -1
  31. package/dist/components/forma-avatar.js +0 -95
  32. package/dist/internal/agent-visual/color.d.ts +0 -4
  33. package/dist/internal/agent-visual/color.d.ts.map +0 -1
  34. package/dist/internal/agent-visual/color.js +0 -78
  35. package/dist/internal/agent-visual/poster.d.ts +0 -6
  36. package/dist/internal/agent-visual/poster.d.ts.map +0 -1
  37. package/dist/internal/agent-visual/poster.js +0 -35
  38. package/dist/internal/agent-visual/recipes.d.ts +0 -15
  39. package/dist/internal/agent-visual/recipes.d.ts.map +0 -1
  40. package/dist/internal/agent-visual/recipes.js +0 -72
  41. package/dist/internal/agent-visual/renderer.d.ts +0 -10
  42. package/dist/internal/agent-visual/renderer.d.ts.map +0 -1
  43. package/dist/internal/agent-visual/renderer.js +0 -112
  44. package/dist/internal/agent-visual/shader.d.ts +0 -2
  45. package/dist/internal/agent-visual/shader.d.ts.map +0 -1
  46. package/dist/internal/agent-visual/shader.js +0 -158
  47. package/dist/internal/forma/frame.d.ts +0 -10
  48. package/dist/internal/forma/frame.d.ts.map +0 -1
  49. package/dist/internal/forma/frame.js +0 -211
  50. package/dist/internal/forma/geometry.d.ts +0 -41
  51. package/dist/internal/forma/geometry.d.ts.map +0 -1
  52. package/dist/internal/forma/geometry.js +0 -235
  53. package/dist/internal/forma/hallmark.d.ts +0 -5
  54. package/dist/internal/forma/hallmark.d.ts.map +0 -1
  55. package/dist/internal/forma/hallmark.js +0 -4
  56. package/dist/internal/forma/model.d.ts +0 -53
  57. package/dist/internal/forma/model.d.ts.map +0 -1
  58. package/dist/internal/forma/model.js +0 -70
  59. package/dist/internal/forma/motion.d.ts +0 -42
  60. package/dist/internal/forma/motion.d.ts.map +0 -1
  61. package/dist/internal/forma/motion.js +0 -194
  62. package/dist/internal/forma/rig.d.ts +0 -10
  63. package/dist/internal/forma/rig.d.ts.map +0 -1
  64. package/dist/internal/forma/rig.js +0 -211
  65. package/dist/internal/forma/still.d.ts +0 -4
  66. package/dist/internal/forma/still.d.ts.map +0 -1
  67. package/dist/internal/forma/still.js +0 -8
  68. package/dist/internal/forma/stored-appearance.d.ts +0 -29
  69. package/dist/internal/forma/stored-appearance.d.ts.map +0 -1
  70. package/dist/internal/forma/stored-appearance.js +0 -53
  71. package/docs/agent-visual.md +0 -114
  72. package/docs/forma-avatar.md +0 -110
@@ -1,10 +0,0 @@
1
- import type { ResolvedFormaOptions } from "./model.js";
2
- export interface FormaHandle {
3
- update(options: ResolvedFormaOptions): void;
4
- dispose(): void;
5
- }
6
- export interface FormaMountCallbacks {
7
- onModeChange?: (mode: "live" | "still") => void;
8
- }
9
- export declare function mountForma(svg: SVGSVGElement, options: ResolvedFormaOptions, callbacks?: FormaMountCallbacks): FormaHandle;
10
- //# sourceMappingURL=rig.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rig.d.ts","sourceRoot":"","sources":["../../../src/internal/forma/rig.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAUpD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,IAAI,CAAC;CACjB;AACD,MAAM,WAAW,mBAAmB;IAClC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,KAAK,IAAI,CAAC;CACjD;AAyLD,wBAAgB,UAAU,CACxB,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,oBAAoB,EAC7B,SAAS,GAAE,mBAAwB,GAClC,WAAW,CAwCb"}
@@ -1,211 +0,0 @@
1
- import { computeFrame } from "./frame.js";
2
- import { createMotion, needsMotion, settled, stepPose, updateMotion, } from "./motion.js";
3
- const attributeName = (prop) => prop.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
4
- function write(element, attributes) {
5
- for (const prop in attributes) {
6
- const value = attributes[prop] ?? "";
7
- if (prop === "display") {
8
- element.style.display = value;
9
- continue;
10
- }
11
- const name = attributeName(prop);
12
- if (element.getAttribute(name) !== value)
13
- element.setAttribute(name, value);
14
- }
15
- }
16
- const pools = new WeakMap();
17
- class Pool {
18
- document;
19
- entries = new Map();
20
- observer;
21
- reduced = typeof matchMedia === "function"
22
- ? matchMedia("(prefers-reduced-motion: reduce)")
23
- : { matches: false, addEventListener() { }, removeEventListener() { } };
24
- pointer = { x: NaN, y: NaN };
25
- frame;
26
- lastFrame = -Infinity;
27
- constructor(document) {
28
- this.document = document;
29
- this.observer =
30
- typeof IntersectionObserver === "undefined"
31
- ? undefined
32
- : new IntersectionObserver((records) => {
33
- for (const record of records) {
34
- const entry = this.entries.get(record.target);
35
- if (!entry)
36
- continue;
37
- if (record.isIntersecting && !entry.visible)
38
- entry.motion.snap = true;
39
- entry.visible = record.isIntersecting;
40
- entry.dirty = true;
41
- }
42
- this.request();
43
- }, { rootMargin: "80px" });
44
- document.addEventListener("visibilitychange", this.onWake);
45
- document.addEventListener("pointermove", this.onPointer, { passive: true });
46
- document.addEventListener("pointerleave", this.onPointerLeave);
47
- this.reduced.addEventListener("change", this.onWake);
48
- window.addEventListener("resize", this.onWake);
49
- }
50
- onWake = () => {
51
- for (const entry of this.entries.values()) {
52
- entry.dirty = true;
53
- entry.motion.snap = true;
54
- entry.rect = null;
55
- }
56
- if (this.document.hidden && this.frame !== undefined) {
57
- cancelAnimationFrame(this.frame);
58
- this.frame = undefined;
59
- }
60
- this.request();
61
- };
62
- onPointer = (event) => {
63
- if (event.pointerType === "touch")
64
- return;
65
- this.pointer.x = event.clientX;
66
- this.pointer.y = event.clientY;
67
- this.request();
68
- };
69
- onPointerLeave = () => {
70
- this.pointer.x = NaN;
71
- this.pointer.y = NaN;
72
- this.request();
73
- };
74
- request() {
75
- if (this.frame === undefined &&
76
- !this.document.hidden &&
77
- this.entries.size > 0 &&
78
- typeof requestAnimationFrame === "function")
79
- this.frame = requestAnimationFrame(this.tick);
80
- }
81
- aim(entry, now) {
82
- const { motion } = entry;
83
- if (motion.still || motion.view !== "front" || Number.isNaN(this.pointer.x)) {
84
- motion.pointer.x = 0;
85
- motion.pointer.y = 0;
86
- return;
87
- }
88
- if (!entry.rect || now - entry.rectAt > 500) {
89
- entry.rect = entry.svg.getBoundingClientRect();
90
- entry.rectAt = now;
91
- }
92
- const rect = entry.rect;
93
- if (!rect.width)
94
- return;
95
- const cx = rect.left + rect.width / 2;
96
- const cy = rect.top + rect.height / 2;
97
- const reach = Math.max(rect.width, 32) * 2.5;
98
- const dx = this.pointer.x - cx;
99
- const dy = this.pointer.y - cy;
100
- const distance = Math.hypot(dx, dy);
101
- const k = Math.min(1, distance / reach) / (distance || 1);
102
- motion.pointer.x = Math.max(-1, Math.min(1, dx * k));
103
- motion.pointer.y = Math.max(-1, Math.min(1, dy * k));
104
- }
105
- tick = (now) => {
106
- this.frame = undefined;
107
- if (this.document.hidden)
108
- return;
109
- if (now - this.lastFrame < 1000 / 30) {
110
- this.request();
111
- return;
112
- }
113
- const dt = Math.min(0.05, Math.max(0, (now - this.lastFrame) / 1000));
114
- this.lastFrame = now;
115
- let again = false;
116
- for (const entry of this.entries.values()) {
117
- if (!entry.visible)
118
- continue;
119
- const { motion } = entry;
120
- motion.still = motion.paused || this.reduced.matches;
121
- this.aim(entry, now);
122
- const moving = needsMotion(motion, now);
123
- if (entry.dirty || moving || entry.terminalFrameDue) {
124
- const pose = stepPose(motion, now, entry.dirty ? 0 : dt);
125
- this.paint(entry, pose, now);
126
- this.setMode(entry, motion.still ? "still" : "live");
127
- entry.dirty = false;
128
- entry.terminalFrameDue = moving;
129
- }
130
- again ||= needsMotion(motion, now) && !settled(motion, now);
131
- if (Number.isFinite(this.pointer.x) && motion.state === "idle" && !motion.still)
132
- again = true;
133
- }
134
- if (again)
135
- this.request();
136
- };
137
- paint(entry, pose, now) {
138
- const frame = computeFrame(entry.motion, pose, now);
139
- for (const part in frame) {
140
- const element = entry.parts.get(part);
141
- if (element)
142
- write(element, frame[part]);
143
- }
144
- entry.svg.dataset.state = entry.motion.state;
145
- }
146
- setMode(entry, mode) {
147
- if (entry.svg.dataset.renderMode === mode)
148
- return;
149
- entry.svg.dataset.renderMode = mode;
150
- entry.callbacks.onModeChange?.(mode);
151
- }
152
- release(svg) {
153
- this.observer?.unobserve(svg);
154
- this.entries.delete(svg);
155
- if (this.entries.size !== 0)
156
- return;
157
- if (this.frame !== undefined)
158
- cancelAnimationFrame(this.frame);
159
- this.observer?.disconnect();
160
- this.document.removeEventListener("visibilitychange", this.onWake);
161
- this.document.removeEventListener("pointermove", this.onPointer);
162
- this.document.removeEventListener("pointerleave", this.onPointerLeave);
163
- this.reduced.removeEventListener("change", this.onWake);
164
- window.removeEventListener("resize", this.onWake);
165
- pools.delete(this.document);
166
- }
167
- }
168
- export function mountForma(svg, options, callbacks = {}) {
169
- const doc = svg.ownerDocument;
170
- let pool = pools.get(doc);
171
- if (!pool) {
172
- pool = new Pool(doc);
173
- pools.set(doc, pool);
174
- }
175
- if (pool.entries.has(svg))
176
- throw new Error("This element already has a Forma avatar");
177
- const parts = new Map();
178
- for (const element of svg.querySelectorAll("[data-part]"))
179
- parts.set(element.getAttribute("data-part"), element);
180
- const entry = {
181
- svg,
182
- parts,
183
- motion: createMotion(options, performance.now()),
184
- visible: !pool.observer,
185
- dirty: true,
186
- terminalFrameDue: false,
187
- callbacks,
188
- rect: null,
189
- rectAt: -Infinity,
190
- };
191
- const activePool = pool;
192
- pool.entries.set(svg, entry);
193
- pool.observer?.observe(svg);
194
- pool.request();
195
- let disposed = false;
196
- return {
197
- update(next) {
198
- if (disposed)
199
- return;
200
- updateMotion(entry.motion, next, performance.now());
201
- entry.dirty = true;
202
- activePool.request();
203
- },
204
- dispose() {
205
- if (disposed)
206
- return;
207
- disposed = true;
208
- activePool.release(svg);
209
- },
210
- };
211
- }
@@ -1,4 +0,0 @@
1
- import { type Frame } from "./frame.js";
2
- import type { ResolvedFormaOptions } from "./model.js";
3
- export declare function stillFrame(options: ResolvedFormaOptions): Frame;
4
- //# sourceMappingURL=still.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"still.d.ts","sourceRoot":"","sources":["../../../src/internal/forma/still.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAgB,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAGpD,wBAAgB,UAAU,CAAC,OAAO,EAAE,oBAAoB,GAAG,KAAK,CAK/D"}
@@ -1,8 +0,0 @@
1
- import { computeFrame } from "./frame.js";
2
- import { createMotion, stepPose } from "./motion.js";
3
- export function stillFrame(options) {
4
- const motion = createMotion({ ...options, paused: true }, 0);
5
- motion.still = true;
6
- const pose = stepPose(motion, 0, 0);
7
- return computeFrame(motion, pose, 0);
8
- }
@@ -1,29 +0,0 @@
1
- import { type FormaChoice, type FormaColor } from "./model.js";
2
- /** What Hermes stores for an Agent's look. Both may be absent or unknown. */
3
- export interface StoredAppearance {
4
- shape?: string | null;
5
- color?: string | null;
6
- }
7
- /** The `shape` value a chosen form is stored as. */
8
- export declare const shapeFor: (form: FormaChoice) => string;
9
- /** The stored shape of the form an Agent wears until a customer chooses one. */
10
- export declare const defaultShape: string;
11
- /**
12
- * The hex each palette colour is stored as. Hermes keeps an Agent's colour as
13
- * a CSS colour for its own surfaces, so the stored spelling is a value rather
14
- * than a token reference; `stored-appearance.test.ts` holds this table to the
15
- * shipped design-system tokens, and the ESLint colour-literal exemption in the
16
- * root config names this file.
17
- */
18
- export declare const formaColorValues: Record<FormaColor, string>;
19
- /**
20
- * Resolve the stored strings to what `FormaAvatar` draws. An unknown shape is
21
- * the default form and an unknown colour is undefined, which leaves the form
22
- * its own pigment. The root Agent is the caller's business: a caller that owns
23
- * one passes `form="nadia"` instead of asking here.
24
- */
25
- export declare function resolveAppearance(stored: StoredAppearance): {
26
- form: FormaChoice;
27
- color: FormaColor | undefined;
28
- };
29
- //# sourceMappingURL=stored-appearance.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"stored-appearance.d.ts","sourceRoot":"","sources":["../../../src/internal/forma/stored-appearance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAA0C,MAAM,SAAS,CAAC;AAEpG,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,oDAAoD;AACpD,eAAO,MAAM,QAAQ,GAAI,MAAM,WAAW,KAAG,MAAyB,CAAC;AAEvE,gFAAgF;AAChF,eAAO,MAAM,YAAY,EAAE,MAA8B,CAAC;AAc1D;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAOvD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG;IAC3D,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC;CAC/B,CAEA"}
@@ -1,53 +0,0 @@
1
- import { defaultForm, formaChoices, formaColors } from "./model.js";
2
- /** The `shape` value a chosen form is stored as. */
3
- export const shapeFor = (form) => `forma:${form}`;
4
- /** The stored shape of the form an Agent wears until a customer chooses one. */
5
- export const defaultShape = shapeFor(defaultForm);
6
- /**
7
- * The five role names the launch Agents were stored under, each kept at the
8
- * form it wore. Forms are shapes, never roles: nothing new is stored this way.
9
- */
10
- const legacyShapes = new Map([
11
- ["forma:research", "uovo"],
12
- ["forma:build", "goccia"],
13
- ["forma:design", "nuvola"],
14
- ["forma:operate", "quadrato"],
15
- ["forma:review", "esagono"],
16
- ]);
17
- /**
18
- * The hex each palette colour is stored as. Hermes keeps an Agent's colour as
19
- * a CSS colour for its own surfaces, so the stored spelling is a value rather
20
- * than a token reference; `stored-appearance.test.ts` holds this table to the
21
- * shipped design-system tokens, and the ESLint colour-literal exemption in the
22
- * root config names this file.
23
- */
24
- export const formaColorValues = {
25
- verde: "#00a85a",
26
- cobalto: "#1e3cff",
27
- rosso: "#d3302f",
28
- arancio: "#ff5a1f",
29
- giallo: "#ffc220",
30
- avorio: "#e9e7db",
31
- };
32
- /**
33
- * Resolve the stored strings to what `FormaAvatar` draws. An unknown shape is
34
- * the default form and an unknown colour is undefined, which leaves the form
35
- * its own pigment. The root Agent is the caller's business: a caller that owns
36
- * one passes `form="nadia"` instead of asking here.
37
- */
38
- export function resolveAppearance(stored) {
39
- return { form: formFor(stored.shape), color: colorFor(stored.color) };
40
- }
41
- function formFor(shape) {
42
- if (!shape)
43
- return defaultForm;
44
- // A Map, not an object: the shape is a stored string from a machine, and an
45
- // object lookup would hand back `toString` or `constructor` for those spellings.
46
- return (formaChoices.find((form) => shape === shapeFor(form)) ?? legacyShapes.get(shape) ?? defaultForm);
47
- }
48
- function colorFor(color) {
49
- if (!color)
50
- return undefined;
51
- const stored = color.toLowerCase();
52
- return formaColors.find((name) => stored === formaColorValues[name] || color === name);
53
- }
@@ -1,114 +0,0 @@
1
- # Agent Visual System
2
-
3
- Legacy API for existing consumers. New Agent workspace surfaces use
4
- [AgentOrb](agent-orb.md) and the shared
5
- [product language](../../design-system/PRODUCT_LANGUAGE.md#agent-section-contract).
6
- The multicolour recipes below do not govern new Portal agent representations.
7
-
8
- The Agent visual is the decorative identity for a persistent Agent in Nadia.
9
- It is a living, multicolour metaball form whose recipe is assigned by Agent
10
- type. It does not represent status, capability, runtime activity, or work
11
- output. Those facts stay in text, status components, assistant-ui, and the
12
- product surfaces that own them.
13
-
14
- This system is distinct from Nadia's portrait. Nadia keeps the one canonical
15
- identity in `brand/AGENTS.md` "Nadia's identity"; Agent visuals identify the other named,
16
- role-specific Agents that a company operates inside Nadia.
17
-
18
- ## Approved form
19
-
20
- - One connected metaball entity sits on a true-black field.
21
- - The entity is always multicolour and draws its five hues from the Agent
22
- identity palette: Cobalto, Verde, Arancio, Giallo, and Rosso. That palette
23
- is owned by `packages/design-system/DESIGN.md`, which holds the token names
24
- and values; this document holds the recipes that arrange them.
25
- - The entity itself carries the surface. Texture never paints the background.
26
- - The four approved surfaces are solid, ordered points, borderless triangular
27
- wire, and irregular living particles.
28
- - Ordered points form a regular, readable field. Living particles vary in
29
- position, size, and brightness so the two surfaces remain visibly distinct.
30
- - Triangular wire has no outer stroke. The clipped triangular network alone
31
- defines the body.
32
- - The visual has no initials, letters, face, avatar frame, status tint, or
33
- caller-selected colour.
34
-
35
- ## Closed recipes
36
-
37
- The five recipes below are the complete public set. Their names, palettes,
38
- surface assignments, ball counts, sizes, and deterministic frames are fixed.
39
- Applications assign one recipe explicitly. They never hash an id, inspect a
40
- free-form role label, randomize a recipe, or expose these parameters to a
41
- caller.
42
-
43
- | Recipe | Surface | Palette order | Count | Size | Still frame |
44
- | ------------- | ---------------- | -------------------------------------- | ----: | ---: | ----------: |
45
- | General | Solid | Cobalto, Verde, Arancio, Giallo, Rosso | 10 | 0.83 | 0 |
46
- | Reasoning | Ordered points | Cobalto, Verde, Giallo | 10 | 0.83 | 0 |
47
- | Coordination | Triangular wire | Verde, Cobalto, Arancio | 8 | 0.76 | 2700 |
48
- | Communication | Solid | Giallo, Arancio, Rosso | 12 | 0.69 | 5400 |
49
- | Execution | Living particles | Rosso, Arancio, Cobalto | 6 | 0.92 | 8100 |
50
-
51
- `general` is the deliberate fallback when a product has not stored an Agent's
52
- recipe yet. It is not an inference from the Agent's name, id, role, status, or
53
- position in a list.
54
-
55
- ## Placement and motion
56
-
57
- The same recipe keeps the same identity wherever it appears. Each recipe owns
58
- both its approved deterministic SVG poster form and its ambient shader
59
- parameters in one internal record. The two presentations are intentionally
60
- recognizable realizations of one recipe, not pixel-identical renderers.
61
-
62
- - Registry cells, `AgentCard`, focal Agent detail, and the design-system
63
- showcase use the ambient reference motion at speed 0.24. The text name
64
- remains the primary identity, and motion never reports status or activity.
65
- - The shared component mounts the renderer only while the visual intersects
66
- the viewport and releases its WebGL context offscreen; the vendor pauses it
67
- when the document is hidden. Compact registry placement therefore keeps the
68
- same identity without retaining offscreen shader canvases.
69
- - Reduced-motion, missing WebGL2, shader initialization failure, and context
70
- loss all keep the same deterministic still. They do not substitute a new
71
- shape, colour, icon, or blank box.
72
- - The still renders before the client shader is eligible so server rendering
73
- and hydration never present an empty visual slot.
74
-
75
- The visual is normally hidden from assistive technology because the adjacent
76
- Agent name carries the identity. A picker in which the visual is itself the
77
- choice may give it an accessible name. Status and job information never enter
78
- that name.
79
-
80
- ## Implementation ownership
81
-
82
- `@nadicodeai/ui` owns the typed `AgentVisual` implementation and the
83
- presentational `AgentCard` composition. The API exposes the closed recipe and
84
- approved presentation roles, not raw colours, shader source, speed, count,
85
- frame, density, material, or arbitrary tuning.
86
-
87
- Consuming products own the stored recipe assignment, Agent data, routing,
88
- actions, permissions, and placement. An Agent registry remains a table when
89
- the job is comparison; the visual may appear inside its identity cell without
90
- turning the row into a new `AgentRow` component.
91
-
92
- ## Source and modification record
93
-
94
- The solid form mounts the published `metaballsFragmentShader` from
95
- `@paper-design/shaders` version `0.0.78` verbatim. The other three surfaces use
96
- a NadicodeAI modification of that release's published Metaballs shader. The
97
- modification preserves metaball generation, motion, colour blending, edge
98
- calculation, and background composition, then changes only the fill clipped
99
- inside the final entity shape.
100
-
101
- The package is Apache-2.0 and includes a `NOTICE` file. Any distributed
102
- modified source must keep the Apache-2.0 license, retain the upstream notices,
103
- carry a prominent modification notice, and include the upstream NOTICE text:
104
-
105
- ```text
106
- Paper Shaders
107
- Copyright 2026 Paper
108
-
109
- Powered by Paper Shaders:
110
- https://shaders.paper.design
111
- ```
112
-
113
- The identity system is one closed, package-owned system; additions go through
114
- this document, never through a new ad-hoc visual.
@@ -1,110 +0,0 @@
1
- # Forma Avatars
2
-
3
- Forma is the avatar family for the Agents a company operates inside Nadia,
4
- and for Nadia herself. It is a character: a coloured shell with a visor and
5
- two eyes, alive through the approved lifecycle gestures. `@nadicodeai/ui`
6
- owns its rendering as one SVG per avatar; consuming products own the stored
7
- appearance, the Agent data, and every fact about status or work.
8
-
9
- This system is distinct from Nadia's portrait, which stays the one canonical
10
- identity in `brand/AGENTS.md` "Nadia's identity". Nadia's avatar here is her
11
- approved Forma shell: the tricolour, the signature eyes and lash flicks, and
12
- the Italy hallmark on her right flank (Natural Earth 110m Italy from
13
- AshKyd/geojson-regions, projected once with d3-geo 3.1.1 into a 512-unit box).
14
-
15
- ## Forms and colours
16
-
17
- A customer picks a form and a colour. Forms are shapes, never roles: choosing
18
- Goccia grants no tools and changes no instructions, and the product names the
19
- job. The closed set is Nadia plus seven forms:
20
-
21
- | Form | Silhouette | Default shell | Visor | Eyes |
22
- | --- | --- | --- | --- | --- |
23
- | `nadia` | The approved shell, tricolour, fixed | verde | black | signature mint |
24
- | `goccia` | Drop | rosso | black | white |
25
- | `nuvola` | Cloud | cobalto | ivory | ink |
26
- | `triangolo` | Rounded triangle | verde | ivory | ink |
27
- | `esagono` | Hexagon | giallo | black | white |
28
- | `quadrato` | Squircle | arancio | ivory | ink |
29
- | `pillola` | Stadium | verde | black | white |
30
- | `uovo` | Egg | cobalto | black | white |
31
-
32
- Shell colours are the Agent identity palette (`identity-verde`, `-cobalto`,
33
- `-rosso`, `-arancio`, `-giallo`) plus `forma-avorio`; the visor, bezel,
34
- Nadia's eye light and the blocked sand are the `forma-*` tokens. All of them are owned by
35
- `packages/design-system/DESIGN.md` and read through their CSS custom
36
- properties, so an avatar carries no colour of its own. `uovo` is the form an
37
- Agent wears until a customer chooses; it is never inferred from a name, an
38
- id, a role label, or a position in a list.
39
-
40
- ## Construction
41
-
42
- Every form carries a core sphere inside its silhouette. The one approved
43
- visor and the form's eyes ride on that sphere, seen through the engine's
44
- camera (perspective from 5.2 radii), and the silhouette clips them: the face
45
- is the same size and curvature on every form, and turning the head moves it
46
- across the shell the same way, foreshortening at the rim.
47
-
48
- The lighting reproduces the engine's studio, number for number where a
49
- gradient can: the shell is enamel deepened by its own pigment toward the
50
- rim (a multiply layer), shaded by the hemisphere light toward the floor,
51
- lit by the wide top-left key card, a hot spot, and the right rim card; the
52
- visor is black glass with one crisp reflection at its top-left corner, a
53
- soft haze across the top, the right-edge reflection, and a thin bezel lit
54
- only where the key reaches it; the eyes carry their halo and a glint, and
55
- Nadia's Italy hallmark is the engine's ivory engraving at longitude 1.24 on
56
- her right flank, visible from the side. Blocked morphs every form into the same torus and drains
57
- its pigment 28% toward `forma-sabbia`, as the approved renders do.
58
-
59
- ## States and motion
60
-
61
- Six states: `idle`, `working`, `thinking`, `waiting`, `blocked`, `done`. The
62
- choreography is the approved engine's, number for number: the idle sway and
63
- pointer-following gaze; working's acknowledging nod, scan, beat, three pulsing
64
- ticks and two orbiting bead trails; thinking's tilt and three thought beads;
65
- waiting's pause bars; blocked's morph into the ring and bar; done's wind-up,
66
- full turn, hop, happy eyes and the three-ribbon celebration. A completion
67
- celebrates once per real transition or `motionKey` change; an initially
68
- completed avatar holds its finished pose. Waiting and blocked always face
69
- forward; `view="side"` shows the flank otherwise.
70
-
71
- One loop per document drives every mounted avatar at up to 30 frames a
72
- second. Offscreen and hidden avatars do not render, settled states stop the
73
- loop, and the pointer turns the heads nearest to it. `paused` and reduced
74
- motion hold the state's deterministic resting frame, which is also what the
75
- server renders, effects included (a working still carries its orbit beads, a
76
- thinking still its thought beads), so no surface ever shows an empty slot and
77
- no still image is shipped.
78
-
79
- ## Package shape
80
-
81
- `FormaAvatar` (`@nadicodeai/ui/components/forma-avatar`) takes `form`,
82
- `color`, `state`, `size` (12–512 CSS pixels), `paused`, `view`, and
83
- `motionKey`. It is decorative by default; a visual that is itself the
84
- choice, as in a picker, opts into `decorative={false}` with a `label`. It
85
- exposes `data-forma-avatar`, `data-state`, `data-color` on its host and
86
- `data-render-mode` (`still` or `live`) on the SVG. The model's closed lists
87
- (`formaForms`, `formaChoices`, `formaColors`, `formaStates`, `formDetails`,
88
- `defaultForm`) are exported beside it for pickers and stored-appearance
89
- mapping; the geometry, rig and frame computation are internal.
90
-
91
- The stored appearance resolves in this package too, so every product draws one
92
- Agent the same way. `StoredAppearance` is the pair of strings a product stores, `shape`
93
- and `color`, either of which may be absent or unknown. `resolveAppearance`
94
- answers the `form` and `color` the avatar takes: an unknown shape is
95
- `defaultForm`, the five retired role shapes keep the form each one wore, and an
96
- unknown colour is `undefined`, which leaves the form its own pigment. A stored
97
- colour matches a palette hex in any case or the bare colour name.
98
- The resolver lives in `@nadicodeai/ui/components/forma-appearance`, which
99
- carries no `"use client"`: `FormaAvatar` is a client component, and a pure
100
- function re-exported from a client module becomes a client reference a server
101
- cannot call, so the two entry points do not re-export each other and every
102
- consumer imports the look from one and the avatar from the other.
103
- `shapeFor(form)` writes the stored spelling of a chosen form, `defaultShape` is
104
- `shapeFor(defaultForm)`, and `formaColorValues` is the hex each palette colour
105
- is stored as, held to the shipped design-system tokens by a test beside the
106
- resolver. The root Agent is the product's decision, not the resolver's: a
107
- product that owns one passes `form="nadia"` itself.
108
-
109
- Additions to the family go through this document and the design-system
110
- token contract, never through an ad-hoc visual.