@brunosouzas/design-system 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -234,6 +234,19 @@ A spec with `"format": "banner"` (the default is `"cover"`) builds a home banner
234
234
 
235
235
  The banner is one image that scales down as a whole. Words in it become small on a phone (a 19px label is about 6px at 375px wide), so keep them few or none. `test/fixtures/covers/home-banner.json` is the brunosouzas.com home banner redrawn from the pieces.
236
236
 
237
+ ### Signature lockup
238
+
239
+ A cover, banner, LinkedIn card or og spec takes an optional `"signature": true`: Mira (scale 0.25) and the symbol, anchored to the frame's bottom-right corner by `cover.signatureMargin` from both edges. Omit it and nothing is added — `signature` isn't used by a diagram, a favicon or an avatar.
240
+
241
+ ### Social and platform formats
242
+
243
+ Four more formats hold the symbol or a reduced cover frame, fixed size, no `height`:
244
+
245
+ - `"format": "linkedin-card"` — 1080x1080, cover frame conventions (`background`, `rings`, `pieces`, `custom`, `signature`). The "new piece" announcement card.
246
+ - `"format": "og"` — 1200x630, cover frame conventions. The site's generic social-preview image (not an article's own cover, which already serves as its og:image).
247
+ - `"format": "favicon"` — 32x32, no `pieces`: the symbol drawn twice, a light-theme group and a dark-theme group switched by a `prefers-color-scheme` media query. This command writes the SVG only; a favicon PNG set (16/32/48/180/192/512) is the consuming site's own build step.
248
+ - `"format": "avatar"` — 500x500, no `background`, no `pieces`: the symbol filling the frame on an accent-coloured ground. One static image (GitHub profile, etc.), not theme-switching.
249
+
237
250
  ### Body diagrams and icons from a spec
238
251
 
239
252
  The same command draws the blog's body diagrams and icons, so a consumer needs no script and no local install:
@@ -273,7 +286,7 @@ Project-wide decisions are recorded as ADRs in [`docs/adr/`](docs/adr/README.md)
273
286
  - **Animations are code, made with Remotion.** A scene is React components and data, so a correction is a re-render and every format and language comes from one composition. AI video generators were rejected because their outputs are not consistent between clips. [ADR 0001](docs/adr/0001-remotion-for-motion.md)
274
287
  - **One token source.** Colours, type, outline, shadow and motion timings live in one file; the site, the animations and the SVG scripts read generated exports instead of keeping their own copies. [ADR 0002](docs/adr/0002-one-token-source.md)
275
288
  - **One visual language, fixed colour meanings.** Green is always healthy, orange always blocked, red always the failure path, so a reader learns the code once. Rotated text is allowed since the 2026-10-23 amendment. [ADR 0003](docs/adr/0003-visual-language-and-colour-meaning.md)
276
- - **Scenes illustrate published articles.** Every string on screen comes from the article, so an animation never says more than the reviewed text. [ADR 0004](docs/adr/0004-scenes-illustrate-published-articles.md)
289
+ - **Scenes illustrate an approved source.** Every string on screen comes from a published article, or from a post or page the owner approved, so a scene or piece never says more than the reviewed text. Catalogue scenes still come from published articles. [ADR 0004](docs/adr/0004-scenes-illustrate-published-articles.md)
277
290
  - **Neutral videos, told by a fixed cast.** One video for both languages, no words on screen, a caption only when the motion cannot explain itself, and every gesture tied to an article line. [ADR 0005](docs/adr/0005-neutral-videos-and-the-cast.md)
278
291
 
279
292
  ## How this project is built
@@ -0,0 +1,12 @@
1
+ import { type SurfaceName } from "./ink.ts";
2
+ export declare const BANK_OBJECT_NAMES: readonly ["courier", "lume", "carga", "anel", "modulo", "hub"];
3
+ export type BankObjectName = (typeof BANK_OBJECT_NAMES)[number];
4
+ export type BankObjectParams = {
5
+ name: BankObjectName;
6
+ x: number;
7
+ y: number;
8
+ surface?: SurfaceName;
9
+ scale?: number;
10
+ };
11
+ /** One of the six prop-bank objects for covers (I10): Correio, Lume, Carga, Anel, Módulo, Hub — screen off. */
12
+ export declare const bankObject: import("./piece.ts").Piece<BankObjectParams>;
@@ -0,0 +1,44 @@
1
+ // The bank of objects for covers (identity studio I10): six objects, screen
2
+ // off, that appear on track covers instead of a cast character with a role.
3
+ // Geometry ported unchanged from the approved phase-3 source
4
+ // (`~/.hermes/cache/scratch/ab267/casting3.py`: `courier`/`lume`/`carga`/`anel`/`modulo`/`hub`).
5
+ //
6
+ // Why screen off: a cast character (Mira, Visor, Drone, Kiu, Sinal) has a
7
+ // story role; a bank object is scenery. If a future piece needs the same
8
+ // object with a face in two or more places, it becomes a character with its
9
+ // own sheet (I10) — not a change to this file.
10
+ import { drawn, el, g } from "./node.js";
11
+ import { surfaceParam } from "./ink.js";
12
+ import { pos, num } from "./params.js";
13
+ import { definePiece } from "./piece.js";
14
+ import { outlined, skin } from "./skin.js";
15
+ export const BANK_OBJECT_NAMES = ["courier", "lume", "carga", "anel", "modulo", "hub"];
16
+ /** The screen, off: just the visor-coloured panel, no eyes (I10). */
17
+ const screenOff = (x, y, w, h, s, rx) => el("rect", { x, y, width: w, height: h, rx: rx ?? h / 2.4, fill: s.visor });
18
+ const courier = (s) => g({}, el("path", { d: "M-42 -100 L-60 -126 H-34 L-18 -100 Z", fill: s.outline }), el("path", { d: "M-42 -40 L-60 -14 H-34 L-18 -40 Z", fill: s.outline }), el("rect", { x: -72, y: -84, width: 14, height: 28, rx: 4, fill: s.outline }), el("path", { d: "M-50 -102 H18 C58 -102 80 -84 80 -70 C80 -56 58 -38 18 -38 H-50 A32 32 0 0 1 -50 -102 Z", ...outlined(s) }), el("rect", { x: -40, y: -82, width: 24, height: 24, rx: 5, fill: s.accent }), screenOff(-2, -90, 56, 38, s));
19
+ const lume = (s) => {
20
+ const rays = [0, 1, 2].map((i) => el("path", { d: `M${50 + i * 4} ${-104 + i * 22} L${78 + i * 8} ${-112 + i * 26}`, stroke: s.accent, "stroke-width": 7, "stroke-linecap": "round" }));
21
+ return g({}, el("path", { d: "M-20 -138 A20 20 0 0 1 20 -138", stroke: s.outline, "stroke-width": 6, fill: "none" }), el("path", { d: "M-30 -138 H30 L38 -124 H-38 Z", fill: s.outline }), el("rect", { x: -36, y: -126, width: 72, height: 80, rx: 14, ...outlined(s) }), screenOff(-25, -114, 50, 44, s), el("path", { d: "M-38 -46 H38 L30 -34 H-30 Z", fill: s.outline }), el("path", { d: "M-10 -34 H10 L0 -20 Z", fill: s.outline }), ...rays);
22
+ };
23
+ const carga = (s) => g({}, el("rect", { x: -44, y: -152, width: 60, height: 28, rx: 4, fill: s.accent }), el("path", { d: "M-30 -152 V-124 M-14 -152 V-124 M2 -152 V-124", stroke: s.body, "stroke-width": 3 }), el("path", { d: "M-24 -60 L-38 -32 L-24 -6 M24 -60 L12 -32 L26 -6", stroke: s.outline, "stroke-width": 10, "stroke-linecap": "round", "stroke-linejoin": "round", fill: "none" }), el("rect", { x: -40, y: -8, width: 30, height: 9, rx: 4.5, fill: s.outline }), el("rect", { x: 12, y: -8, width: 30, height: 9, rx: 4.5, fill: s.outline }), el("rect", { x: -50, y: -126, width: 100, height: 70, rx: 16, ...outlined(s) }), screenOff(-30, -114, 62, 36, s), el("path", { d: "M50 -96 H66 V-74", stroke: s.outline, "stroke-width": 7, "stroke-linecap": "round", fill: "none" }), el("path", { d: "M58 -74 H74", stroke: s.outline, "stroke-width": 7, "stroke-linecap": "round" }));
24
+ const anel = (s) => g({}, el("path", { d: "M-82 -78 A84 20 -12 0 1 82 -98", stroke: s.accent, "stroke-width": 9, fill: "none", "stroke-linecap": "round" }), el("circle", { cx: 0, cy: -88, r: 46, ...outlined(s) }), screenOff(-31, -112, 62, 32, s), el("path", { d: "M82 -98 A84 20 -12 0 1 -82 -78", stroke: s.accent, "stroke-width": 9, fill: "none", "stroke-linecap": "round" }), el("rect", { x: -66, y: -74, width: 14, height: 14, rx: 3, fill: s.outline }), el("rect", { x: 58, y: -92, width: 14, height: 14, rx: 3, fill: s.outline }));
25
+ const modulo = (s) => g({}, el("path", { d: "M-22 -40 L-32 -2 M22 -40 L32 -2", stroke: s.outline, "stroke-width": 6, "stroke-linecap": "round" }), el("path", { d: "M-44 -50 L-74 -2 M44 -50 L74 -2", stroke: s.outline, "stroke-width": 7, "stroke-linecap": "round" }), el("rect", { x: -88, y: -6, width: 28, height: 7, rx: 3.5, fill: s.outline }), el("rect", { x: 60, y: -6, width: 28, height: 7, rx: 3.5, fill: s.outline }), el("rect", { x: -46, y: -62, width: 92, height: 24, rx: 5, fill: s.accent }), el("path", { d: "M22 -134 L34 -154", stroke: s.outline, "stroke-width": 5, "stroke-linecap": "round" }), el("path", { d: "M26 -160 A10 10 0 0 0 44 -150 Z", fill: s.outline }), el("path", { d: "M-36 -136 H36 L52 -114 V-80 L36 -62 H-36 L-52 -80 V-114 Z", ...outlined(s) }), screenOff(-34, -122, 68, 42, s));
26
+ const hub = (s) => {
27
+ const arms = [];
28
+ const spokes = [
29
+ [-30, -70, -70, -60, -84, -14], [-14, -64, -30, -30, -40, 0], [14, -64, 30, -30, 40, 0], [30, -70, 70, -60, 84, -14],
30
+ ];
31
+ for (const [x1, y1, cx, cy, x2, y2] of spokes) {
32
+ arms.push(el("path", { d: `M${x1} ${y1} Q${cx} ${cy} ${x2} ${y2}`, stroke: s.outline, "stroke-width": 7, fill: "none", "stroke-linecap": "round" }));
33
+ arms.push(el("rect", { x: x2 - 8, y: y2 - 4, width: 16, height: 12, rx: 3, fill: s.accent }));
34
+ }
35
+ return g({}, ...arms, el("path", { d: "M-50 -76 V-104 A50 50 0 0 1 50 -104 V-76 Q0 -60 -50 -76 Z", ...outlined(s) }), screenOff(-36, -130, 72, 40, s));
36
+ };
37
+ const DRAW = { courier, lume, carga, anel, modulo, hub };
38
+ const drawBankObject = (p) => {
39
+ const s = skin(p.surface ?? "light");
40
+ const body = DRAW[p.name](s);
41
+ return drawn([g({ "data-piece": `bank-${p.name}`, transform: `translate(${p.x} ${p.y}) scale(${p.scale ?? 1})`, stroke: "none" }, body)]);
42
+ };
43
+ /** One of the six prop-bank objects for covers (I10): Correio, Lume, Carga, Anel, Módulo, Hub — screen off. */
44
+ export const bankObject = definePiece("bankObject", { name: { kind: "enum", required: true, values: BANK_OBJECT_NAMES }, x: pos(), y: pos(), surface: surfaceParam, scale: num({ min: 0.25, max: 4 }) }, drawBankObject);
@@ -0,0 +1,72 @@
1
+ import { type SvgNode } from "./node.ts";
2
+ import { type SurfaceName } from "./ink.ts";
3
+ import { type Skin } from "./skin.ts";
4
+ export declare const CAST_EXPRESSIONS: readonly ["idle", "done", "waiting", "failed"];
5
+ export type CastExpression = (typeof CAST_EXPRESSIONS)[number];
6
+ export declare const CAST_VIEWS: readonly ["front", "back"];
7
+ export type CastView = (typeof CAST_VIEWS)[number];
8
+ /**
9
+ * The screen face, the message state (identity studio I2: 4 expressions = parked, done, waiting, failed).
10
+ * Ported from `casting3.py`'s `face(x, y, w, h, S, mode)`.
11
+ */
12
+ export declare const faceAt: (x: number, y: number, w: number, h: number, s: Skin, expression: CastExpression) => SvgNode;
13
+ /** The hood tip, dipping over the forehead: the parent-brand family device (ADR 0007, I4). Absent on Sinal. */
14
+ export declare const peak: (s: Skin, cx: number, top: number, w?: number, dip?: number) => SvgNode;
15
+ /** The hood tip drawn as a cap sitting astride a circle's rim (Visor, Drone, Kiu use this form; `final.py`'s `tipcap`). */
16
+ export declare const tipCap: (s: Skin, cx: number, cy: number, r: number, depth: number) => SvgNode;
17
+ export type MiraParams = {
18
+ x: number;
19
+ y: number;
20
+ surface?: SurfaceName;
21
+ scale?: number;
22
+ expression?: CastExpression;
23
+ view?: CastView;
24
+ /** The needle's angle in degrees (the sheets' `a`); the needle acts first (motion principle, identity sheet). */
25
+ needleAngle?: number;
26
+ /** A message riding the needle's tip. */
27
+ carries?: boolean;
28
+ /** A walking stride offset on the legs. */
29
+ stride?: number;
30
+ };
31
+ /** Mira, the protagonist: an astrolabe whose needle points the route each message takes. */
32
+ export declare const mira: import("./piece.ts").Piece<MiraParams>;
33
+ export type VisorParams = {
34
+ x: number;
35
+ y: number;
36
+ surface?: SurfaceName;
37
+ scale?: number;
38
+ expression?: CastExpression;
39
+ view?: CastView;
40
+ holds?: boolean;
41
+ points?: boolean;
42
+ };
43
+ export declare const visor: import("./piece.ts").Piece<VisorParams>;
44
+ export type DroneParams = {
45
+ x: number;
46
+ y: number;
47
+ surface?: SurfaceName;
48
+ scale?: number;
49
+ expression?: CastExpression;
50
+ view?: CastView;
51
+ carries?: boolean;
52
+ };
53
+ export declare const drone: import("./piece.ts").Piece<DroneParams>;
54
+ export type KiuParams = {
55
+ x: number;
56
+ y: number;
57
+ surface?: SurfaceName;
58
+ scale?: number;
59
+ expression?: CastExpression;
60
+ view?: CastView;
61
+ steps?: number;
62
+ };
63
+ export declare const kiu: import("./piece.ts").Piece<KiuParams>;
64
+ export type SinalParams = {
65
+ x: number;
66
+ y: number;
67
+ surface?: SurfaceName;
68
+ scale?: number;
69
+ expression?: CastExpression;
70
+ view?: CastView;
71
+ };
72
+ export declare const sinal: import("./piece.ts").Piece<SinalParams>;
@@ -0,0 +1,163 @@
1
+ // Mira, protagonist of the parent brand (ADR 0007, identity studio I2). The
2
+ // geometry is the approved round-7 model sheet's, ported unchanged
3
+ // (`~/.hermes/cache/scratch/ab267/sheet7.py`, functions `mira`/`peak`/`legs`/`needle`;
4
+ // `casting3.py`'s `face`). Front and back view only (round 7b dropped the
5
+ // side view and the middle leg).
6
+ import { drawn, el, g } from "./node.js";
7
+ import { surfaceParam } from "./ink.js";
8
+ import { pos, num } from "./params.js";
9
+ import { definePiece } from "./piece.js";
10
+ import { outlined, skin } from "./skin.js";
11
+ export const CAST_EXPRESSIONS = ["idle", "done", "waiting", "failed"];
12
+ export const CAST_VIEWS = ["front", "back"];
13
+ /** The face mode each piece draws, keyed by the shared `CastExpression` (sheets' `m`). */
14
+ const FACE_MODE = { idle: "idle", done: "happy", waiting: "wait", failed: "fail" };
15
+ /**
16
+ * The screen face, the message state (identity studio I2: 4 expressions = parked, done, waiting, failed).
17
+ * Ported from `casting3.py`'s `face(x, y, w, h, S, mode)`.
18
+ */
19
+ export const faceAt = (x, y, w, h, s, expression) => {
20
+ const mode = FACE_MODE[expression];
21
+ const rx = h / 2.4;
22
+ const visor = el("rect", { x, y, width: w, height: h, rx, fill: s.visor });
23
+ const cx = x + w / 2, cy = y + h / 2, dx = w * 0.2, ew = Math.max(5, w * 0.11), eh = h * 0.42, eye = s.eye;
24
+ const eyes = [];
25
+ if (mode === "idle") {
26
+ for (const sx of [-dx, dx])
27
+ eyes.push(el("rect", { x: cx + sx - ew / 2, y: cy - eh / 2, width: ew, height: eh, rx: ew / 2, fill: eye }));
28
+ }
29
+ else if (mode === "happy") {
30
+ for (const sx of [-dx, dx])
31
+ eyes.push(el("path", { d: `M${cx + sx - ew} ${cy + eh * 0.2} Q${cx + sx} ${cy - eh * 0.6} ${cx + sx + ew} ${cy + eh * 0.2}`, fill: "none", stroke: eye, "stroke-width": ew * 0.7, "stroke-linecap": "round" }));
32
+ }
33
+ else if (mode === "wait") {
34
+ for (const sx of [-dx, 0, dx])
35
+ eyes.push(el("circle", { cx: cx + sx, cy, r: ew * 0.45, fill: eye }));
36
+ }
37
+ else {
38
+ for (const sx of [-dx, dx]) {
39
+ const a = ew * 0.8;
40
+ eyes.push(el("path", { d: `M${cx + sx - a} ${cy - a} L${cx + sx + a} ${cy + a} M${cx + sx + a} ${cy - a} L${cx + sx - a} ${cy + a}`, stroke: eye, "stroke-width": ew * 0.6, "stroke-linecap": "round" }));
41
+ }
42
+ }
43
+ return g({}, visor, ...eyes);
44
+ };
45
+ /** The hood tip, dipping over the forehead: the parent-brand family device (ADR 0007, I4). Absent on Sinal. */
46
+ export const peak = (s, cx, top, w = 30, dip = 28) => el("path", { d: `M${cx - w} ${top + 3} Q${cx} ${top - 6} ${cx + w} ${top + 3} Q${cx + w * 0.4} ${top + 8} ${cx} ${top + dip} Q${cx - w * 0.4} ${top + 8} ${cx - w} ${top + 3} Z`, fill: s.accent, stroke: s.outline, "stroke-width": 5, "stroke-linejoin": "round" });
47
+ /** The hood tip drawn as a cap sitting astride a circle's rim (Visor, Drone, Kiu use this form; `final.py`'s `tipcap`). */
48
+ export const tipCap = (s, cx, cy, r, depth) => {
49
+ const k = 0.70710678;
50
+ return el("path", {
51
+ d: `M${cx - k * r} ${cy - k * r} A${r} ${r} 0 0 1 ${cx + k * r} ${cy - k * r} Q${cx + r * 0.28} ${cy - r * 0.62} ${cx} ${cy - depth} Q${cx - r * 0.28} ${cy - r * 0.62} ${cx - k * r} ${cy - k * r} Z`,
52
+ fill: s.accent, stroke: s.outline, "stroke-width": 5, "stroke-linejoin": "round",
53
+ });
54
+ };
55
+ const miraLegs = (s, stride = 0) => [
56
+ el("path", { d: `M-16 -44 L${-34 + stride} 0 M16 -44 L${34 + stride} 0`, stroke: s.outline, "stroke-width": 8, "stroke-linecap": "round" }),
57
+ el("rect", { x: -44 + stride, y: -6, width: 20, height: 7, rx: 3.5, fill: s.outline }),
58
+ el("rect", { x: 24 + stride, y: -6, width: 20, height: 7, rx: 3.5, fill: s.outline }),
59
+ ];
60
+ const miraNeedle = (s, a, carry) => {
61
+ const msg = carry ? [el("path", { d: "M86 0 V16", stroke: s.outline, "stroke-width": 3 }), el("circle", { cx: 86, cy: 26, r: 10, fill: s.accent })] : [];
62
+ return g({ transform: `translate(0,-90) rotate(${a})` }, el("rect", { x: -74, y: -3.5, width: 74, height: 7, rx: 3.5, fill: s.outline }), el("circle", { cx: -74, cy: 0, r: 8, fill: s.outline }), el("path", { d: "M14 -7 L96 0 L14 7 Z", fill: s.accent }), ...msg);
63
+ };
64
+ const drawMira = (p) => {
65
+ const s = skin(p.surface ?? "light");
66
+ const expression = p.expression ?? "idle", view = p.view ?? "front";
67
+ const a = p.needleAngle ?? -40, carry = p.carries ?? false, stride = p.stride ?? 0;
68
+ const scale = p.scale ?? 1;
69
+ let body;
70
+ if (view === "back") {
71
+ const rete = [
72
+ el("circle", { cx: 0, cy: -90, r: 28, fill: "none", stroke: s.outline, "stroke-width": 3 }),
73
+ el("path", { d: "M-28 -90 H28 M0 -118 V-62", stroke: s.outline, "stroke-width": 3 }),
74
+ ];
75
+ body = g({}, ...miraLegs(s), el("circle", { cx: 0, cy: -90, r: 50, ...outlined(s, 8) }), ...rete, miraNeedle(s, -140, false), el("path", { d: "M-28 -137 Q0 -146 28 -137 Q0 -128 -28 -137 Z", fill: s.accent, stroke: s.outline, "stroke-width": 5, "stroke-linejoin": "round" }));
76
+ }
77
+ else {
78
+ body = g({}, ...miraLegs(s, stride), miraNeedle(s, a, carry), el("circle", { cx: 0, cy: -90, r: 50, ...outlined(s, 8) }), faceAt(-30, -104, 60, 32, s, expression), peak(s, 0, -140, 28, 26));
79
+ }
80
+ return drawn([g({ "data-piece": "mira", transform: `translate(${p.x} ${p.y}) scale(${scale})`, stroke: "none" }, body)]);
81
+ };
82
+ /** Mira, the protagonist: an astrolabe whose needle points the route each message takes. */
83
+ export const mira = definePiece("mira", {
84
+ x: pos(), y: pos(), surface: surfaceParam,
85
+ scale: num({ min: 0.25, max: 4 }),
86
+ expression: { kind: "enum", values: CAST_EXPRESSIONS },
87
+ view: { kind: "enum", values: CAST_VIEWS },
88
+ needleAngle: num({ min: -180, max: 180 }),
89
+ carries: { kind: "boolean" },
90
+ stride: num({ min: 0, max: 20 }),
91
+ }, drawMira);
92
+ const drawVisor = (p) => {
93
+ const s = skin(p.surface ?? "light");
94
+ const expression = p.expression ?? "idle", view = p.view ?? "front", back = view === "back";
95
+ const hold = p.holds ?? false, point = p.points ?? true;
96
+ const handR = point
97
+ ? g({ transform: "translate(88,-104) rotate(-35)" }, el("rect", { x: -6, y: -30, width: 12, height: 26, rx: 6, ...outlined(s, 4.5) }), el("circle", { cx: 0, cy: 0, r: 15, ...outlined(s) }))
98
+ : el("circle", { cx: 86, cy: -58, r: 15, ...outlined(s) });
99
+ const handL = hold
100
+ ? [el("rect", { x: -106, y: -96, width: 28, height: 24, rx: 5, fill: s.accent }), el("circle", { cx: -92, cy: -66, r: 15, ...outlined(s) })]
101
+ : [el("circle", { cx: -86, cy: -58, r: 15, ...outlined(s) })];
102
+ const faceG = back ? null : faceAt(-42, -128, 84, 52, s, expression);
103
+ const pk = back
104
+ ? el("path", { d: "M-34 -144 Q0 -152 34 -144 Q0 -136 -34 -144 Z", fill: s.accent, stroke: s.outline, "stroke-width": 5, "stroke-linejoin": "round" })
105
+ : el("path", { d: "M-34 -144 Q0 -152 34 -144 Q14 -138 0 -120 Q-14 -138 -34 -144 Z", fill: s.accent, stroke: s.outline, "stroke-width": 5, "stroke-linejoin": "round" });
106
+ const vent = back
107
+ ? el("path", { d: "M-24 -110 H24 M-24 -96 H24 M-24 -82 H24", stroke: s.outline, "stroke-width": 4, "stroke-linecap": "round" })
108
+ : null;
109
+ const body = g({}, el("rect", { x: -66, y: -114, width: 14, height: 32, rx: 4, fill: s.outline }), el("rect", { x: 52, y: -114, width: 14, height: 32, rx: 4, fill: s.outline }), el("rect", { x: -56, y: -142, width: 112, height: 88, rx: 30, ...outlined(s) }), faceG, vent, pk, el("path", { d: "M-16 -52 H16 L10 -40 H-10 Z", fill: s.outline }), ...handL, handR);
110
+ return drawn([g({ "data-piece": "visor", transform: `translate(${p.x} ${p.y}) scale(${p.scale ?? 1})`, stroke: "none" }, body)]);
111
+ };
112
+ export const visor = definePiece("visor", { x: pos(), y: pos(), surface: surfaceParam, scale: num({ min: 0.25, max: 4 }), expression: { kind: "enum", values: CAST_EXPRESSIONS }, view: { kind: "enum", values: CAST_VIEWS }, holds: { kind: "boolean" }, points: { kind: "boolean" } }, drawVisor);
113
+ const drawDrone = (p) => {
114
+ const s = skin(p.surface ?? "light");
115
+ const expression = p.expression ?? "idle", view = p.view ?? "front", back = view === "back";
116
+ const cargo = p.carries ?? true;
117
+ const faceG = back ? null : faceAt(-27, -98, 54, 30, s, expression);
118
+ const cap = back
119
+ ? el("path", { d: "M-27.6 -109.6 A39 39 0 0 1 27.6 -109.6 Q0 -102 -27.6 -109.6 Z", fill: s.accent, stroke: s.outline, "stroke-width": 5 })
120
+ : tipCap(s, 0, -82, 39, 14);
121
+ const box = cargo
122
+ ? [el("rect", { x: -15, y: -36, width: 30, height: 26, rx: 6, fill: s.accent }), el("path", { d: "M-15 -27 H15", stroke: s.body, "stroke-width": 3 })]
123
+ : [];
124
+ const body = g({}, el("path", { d: "M-22 -106 L-46 -126 M22 -106 L46 -126", stroke: s.outline, "stroke-width": 7, "stroke-linecap": "round" }), el("rect", { x: -74, y: -132, width: 54, height: 9, rx: 4.5, fill: s.outline }), el("rect", { x: 20, y: -132, width: 54, height: 9, rx: 4.5, fill: s.outline }), el("circle", { cx: 0, cy: -82, r: 36, ...outlined(s) }), faceG, cap, el("path", { d: "M0 -46 V-36 M-10 -40 H10", stroke: s.outline, "stroke-width": 5, "stroke-linecap": "round" }), ...box);
125
+ return drawn([g({ "data-piece": "drone", transform: `translate(${p.x} ${p.y}) scale(${p.scale ?? 1})`, stroke: "none" }, body)]);
126
+ };
127
+ export const drone = definePiece("drone", { x: pos(), y: pos(), surface: surfaceParam, scale: num({ min: 0.25, max: 4 }), expression: { kind: "enum", values: CAST_EXPRESSIONS }, view: { kind: "enum", values: CAST_VIEWS }, carries: { kind: "boolean" } }, drawDrone);
128
+ const KIU_SEGMENTS = [[-46, 19], [-84, 17], [-118, 15], [-148, 13], [-174, 11]];
129
+ const drawKiu = (p) => {
130
+ const s = skin(p.surface ?? "light");
131
+ const expression = p.expression ?? "idle", view = p.view ?? "front", back = view === "back";
132
+ const n = p.steps ?? 4;
133
+ const seg = KIU_SEGMENTS.slice(0, n);
134
+ const parts = [];
135
+ // Farthest segment first, nearest last (on top) — matches the sheet's `reversed(enumerate(seg))`.
136
+ for (let i = seg.length - 1; i >= 0; i--) {
137
+ const [dx, r] = seg[i];
138
+ const cy = -(r + 10);
139
+ parts.push(el("path", { d: `M${dx - 5} ${cy + r - 3} L${dx - 7} 0 M${dx + 5} ${cy + r - 3} L${dx + 7} 0`, stroke: s.outline, "stroke-width": 4, "stroke-linecap": "round" }));
140
+ parts.push(el("circle", { cx: dx, cy, r, fill: s.body, stroke: s.outline, "stroke-width": 4.5 }));
141
+ if (i % 2 === 0)
142
+ parts.push(el("circle", { cx: dx, cy, r: r * 0.45, fill: s.accent }));
143
+ }
144
+ const head = [
145
+ el("path", { d: "M-6 -10 L-8 0 M8 -10 L10 0", stroke: s.outline, "stroke-width": 4, "stroke-linecap": "round" }),
146
+ el("circle", { cx: 0, cy: -40, r: 31, ...outlined(s) }),
147
+ back
148
+ ? el("path", { d: "M-24 -64 A34 34 0 0 1 24 -64 Q0 -58 -24 -64 Z", fill: s.accent, stroke: s.outline, "stroke-width": 5 })
149
+ : g({}, faceAt(-21, -54, 44, 26, s, expression), tipCap(s, 0, -40, 34, 12)),
150
+ ];
151
+ const inner = g({ transform: "translate(70,0)" }, ...parts, ...head);
152
+ const body = back ? g({ transform: "scale(-1,1)" }, inner) : inner;
153
+ return drawn([g({ "data-piece": "kiu", transform: `translate(${p.x} ${p.y}) scale(${p.scale ?? 1})`, stroke: "none" }, body)]);
154
+ };
155
+ export const kiu = definePiece("kiu", { x: pos(), y: pos(), surface: surfaceParam, scale: num({ min: 0.25, max: 4 }), expression: { kind: "enum", values: CAST_EXPRESSIONS }, view: { kind: "enum", values: CAST_VIEWS }, steps: { kind: "number", integer: true, min: 2, max: 5 } }, drawKiu);
156
+ const drawSinal = (p) => {
157
+ const s = skin(p.surface ?? "light");
158
+ const expression = p.expression ?? "idle", view = p.view ?? "front";
159
+ const faceG = view === "back" ? null : faceAt(-25, -116, 50, 32, s, expression);
160
+ const body = g({}, el("rect", { x: -22, y: -12, width: 44, height: 12, rx: 6, fill: s.outline }), el("rect", { x: -6, y: -70, width: 12, height: 62, fill: s.outline }), el("circle", { cx: 0, cy: -100, r: 36, ...outlined(s) }), faceG);
161
+ return drawn([g({ "data-piece": "sinal", transform: `translate(${p.x} ${p.y}) scale(${p.scale ?? 1})`, stroke: "none" }, body)]);
162
+ };
163
+ export const sinal = definePiece("sinal", { x: pos(), y: pos(), surface: surfaceParam, scale: num({ min: 0.25, max: 4 }), expression: { kind: "enum", values: CAST_EXPRESSIONS }, view: { kind: "enum", values: CAST_VIEWS } }, drawSinal);
@@ -0,0 +1,12 @@
1
+ import { type SurfaceName } from "./ink.ts";
2
+ export declare const BRAND_NOTATION_ICON_NAMES: readonly ["message", "pointToPointChannel", "messageQueue", "publishSubscribeChannel", "contentBasedRouter", "messageFilter", "splitter", "aggregator", "messageTranslator", "contentEnricher", "correlationIdentifier", "idempotentReceiver", "deadLetterChannel", "pipelineStage", "gateToProduction", "rollback", "toolCall", "contextWindow", "guardrail"];
3
+ export type BrandNotationIconName = (typeof BRAND_NOTATION_ICON_NAMES)[number];
4
+ export type BrandNotationIconParams = {
5
+ name: BrandNotationIconName;
6
+ x: number;
7
+ y: number;
8
+ surface?: SurfaceName;
9
+ scale?: number;
10
+ };
11
+ /** One of the 19 brand notation icons (I11): 13 EIP + 3 delivery + 3 AI, a 48×48 glyph before `scale`. */
12
+ export declare const brandNotationIcon: import("./piece.ts").Piece<BrandNotationIconParams>;
@@ -0,0 +1,79 @@
1
+ // The brand notation: 19 icons (13 EIP + 3 delivery + 3 AI), one grammar,
2
+ // used on covers, home, LinkedIn and GitHub (identity studio I11). Separate
3
+ // from the 26 body-diagram icons (`eip-icons.ts`): never shared, never the
4
+ // same export (review finding S of card 268's spec round). Redraws each
5
+ // pattern's market-recognised silhouette in the new ink-outline grammar —
6
+ // never the EIP book's own artwork, per the brand strategy.
7
+ //
8
+ // Geometry ported unchanged from the approved phase-3/4 source
9
+ // (`~/.hermes/cache/scratch/ab267/final.py`'s `icons()`).
10
+ import { drawn, el, g } from "./node.js";
11
+ import { surfaceParam } from "./ink.js";
12
+ import { pos, num } from "./params.js";
13
+ import { definePiece } from "./piece.js";
14
+ import { colour } from "./params.js";
15
+ export const BRAND_NOTATION_ICON_NAMES = [
16
+ "message", "pointToPointChannel", "messageQueue", "publishSubscribeChannel", "contentBasedRouter",
17
+ "messageFilter", "splitter", "aggregator", "messageTranslator", "contentEnricher",
18
+ "correlationIdentifier", "idempotentReceiver", "deadLetterChannel",
19
+ "pipelineStage", "gateToProduction", "rollback",
20
+ "toolCall", "contextWindow", "guardrail",
21
+ ];
22
+ const ln = (d, c, w = 4, dash = false) => el("path", { d, stroke: c, "stroke-width": w, "stroke-linecap": "round", "stroke-linejoin": "round", fill: "none", ...(dash ? { "stroke-dasharray": "0.1 8" } : {}) });
23
+ const dot = (x, y, col, r = 5) => el("circle", { cx: x, cy: y, r, fill: col });
24
+ const sq = (x, y, col, s = 11) => el("rect", { x: x - s / 2, y: y - s / 2, width: s, height: s, rx: 2, fill: col });
25
+ const node = (x, y, a) => el("path", { d: `M${x} ${y - 8} L${x + 8} ${y} L${x} ${y + 8} L${x - 8} ${y} Z`, fill: a });
26
+ const DRAW = {
27
+ message: (p) => [dot(24, 24, p.c, 8)],
28
+ pointToPointChannel: (p) => [ln("M6 24 H38", p.c), el("circle", { cx: 42, cy: 24, r: 5, fill: p.bg, stroke: p.c, "stroke-width": 3.5 })],
29
+ messageQueue: (p) => [ln("M6 24 H38", p.c, 4, true), dot(14, 24, p.c), dot(24, 24, p.c), el("circle", { cx: 42, cy: 24, r: 5, fill: p.bg, stroke: p.c, "stroke-width": 3.5 })],
30
+ publishSubscribeChannel: (p) => {
31
+ const ring = (x, y) => el("circle", { cx: x, cy: y, r: 5, fill: p.bg, stroke: p.c, "stroke-width": 3.5 });
32
+ return [ln("M4 24 H19 M29 24 H40 M24 19 V8 M24 29 V40", p.c), ring(24, 24), ring(43, 24), ring(24, 6), ring(24, 42)];
33
+ },
34
+ contentBasedRouter: (p) => [ln("M4 24 H16 M32 24 H34 Q38 24 38 20 V14 Q38 10 42 10 H44 M34 24 Q38 24 38 28 V34 Q38 38 42 38 H44", p.c), node(24, 24, p.a)],
35
+ messageFilter: (p) => [ln("M4 24 H44", p.c), ln("M24 24 V36", p.c), ln("M18 40 H30", p.c), node(24, 24, p.a)],
36
+ splitter: (p) => [
37
+ ln("M4 24 H16 M30 16 H44 M30 24 H44 M30 32 H44", p.c), sq(10, 24, p.c, 12), sq(40, 16, p.c, 7), sq(40, 24, p.c, 7), sq(40, 32, p.c, 7),
38
+ el("path", { d: "M22 14 V34", stroke: p.c, "stroke-width": 4, "stroke-linecap": "round" }),
39
+ ],
40
+ aggregator: (p) => [
41
+ ln("M4 16 H18 M4 24 H18 M4 32 H18 M32 24 H44", p.c), sq(8, 16, p.c, 7), sq(8, 24, p.c, 7), sq(8, 32, p.c, 7),
42
+ el("path", { d: "M26 14 V34", stroke: p.c, "stroke-width": 4, "stroke-linecap": "round" }), sq(40, 24, p.c, 12),
43
+ ],
44
+ messageTranslator: (p) => [ln("M4 24 H44", p.c), dot(14, 24, p.c, 6), sq(36, 24, p.c, 12)],
45
+ contentEnricher: (p) => [ln("M4 24 H44", p.c), ln("M24 8 V18", p.c, 4, true), sq(16, 24, p.c, 10), el("rect", { x: 28, y: 16, width: 14, height: 16, rx: 2, fill: p.c })],
46
+ correlationIdentifier: (p) => [
47
+ sq(14, 24, p.c, 14), el("circle", { cx: 14, cy: 24, r: 3, fill: p.bg }), ln("M22 24 H28", p.c, 3), sq(36, 24, p.c, 14), el("circle", { cx: 36, cy: 24, r: 3, fill: p.bg }),
48
+ ],
49
+ idempotentReceiver: (p) => [
50
+ ln("M4 18 H34 M4 30 H34", p.c), dot(12, 18, p.c), dot(12, 30, p.c), el("rect", { x: 34, y: 12, width: 10, height: 24, rx: 3, fill: p.c }), ln("M20 30 L28 30", p.mu, 3),
51
+ ],
52
+ deadLetterChannel: (p) => [ln("M4 14 H20 Q26 14 26 20 V30", p.f, 4, true), ln("M14 36 V42 H38 V36", p.f, 3.5), dot(26, 36, p.f, 4)],
53
+ pipelineStage: (p) => [ln("M4 24 H44", p.c), ln("M18 14 V34 M32 14 V34", p.c), dot(10, 24, p.c)],
54
+ gateToProduction: (p) => [ln("M4 24 H44", p.c), el("rect", { x: 20, y: 10, width: 8, height: 28, rx: 3, fill: p.a }), dot(10, 24, p.c), el("circle", { cx: 42, cy: 24, r: 5, fill: p.bg, stroke: p.c, "stroke-width": 3.5 })],
55
+ rollback: (p) => [ln("M40 24 H14", p.c), ln("M14 24 Q6 24 6 16 Q6 8 14 8 H24", p.c, 4, true), el("circle", { cx: 42, cy: 24, r: 5, fill: p.bg, stroke: p.c, "stroke-width": 3.5 })],
56
+ toolCall: (p) => [
57
+ ln("M8 18 H32", p.c), ln("M32 32 H10", p.c, 4, true), el("circle", { cx: 6, cy: 25, r: 5, fill: p.bg, stroke: p.c, "stroke-width": 3.5 }), el("rect", { x: 34, y: 12, width: 10, height: 24, rx: 3, fill: p.c }),
58
+ ],
59
+ contextWindow: (p) => [
60
+ el("rect", { x: 6, y: 12, width: 36, height: 24, rx: 5, fill: "none", stroke: p.c, "stroke-width": 3.5 }), dot(14, 24, p.c, 3.5), dot(22, 24, p.c, 3.5), dot(30, 24, p.c, 3.5),
61
+ el("path", { d: "M36 20 V28", stroke: p.c, "stroke-width": 3 }),
62
+ ],
63
+ guardrail: (p) => [ln("M4 24 H44", p.c), el("path", { d: "M24 8 V40", stroke: p.a, "stroke-width": 4 }), dot(12, 24, p.c), el("circle", { cx: 40, cy: 24, r: 5, fill: p.bg, stroke: p.c, "stroke-width": 3.5 })],
64
+ };
65
+ const c = (path) => colour(path);
66
+ const drawBrandNotationIcon = (p) => {
67
+ const light = (p.surface ?? "light") === "light";
68
+ const palette = {
69
+ c: light ? c("brand.light.text") : c("brand.dark.text"),
70
+ a: light ? c("brand.light.accent") : c("brand.dark.accent"),
71
+ f: light ? c("brand.light.failure") : c("brand.dark.failure"),
72
+ bg: light ? c("brand.light.bg") : c("brand.dark.bg"),
73
+ mu: light ? c("brand.light.textMuted") : c("brand.dark.textMuted"),
74
+ };
75
+ const glyph = DRAW[p.name](palette);
76
+ return drawn([g({ "data-piece": `brand-icon-${p.name}`, transform: `translate(${p.x} ${p.y}) scale(${p.scale ?? 1})`, stroke: "none" }, ...glyph)]);
77
+ };
78
+ /** One of the 19 brand notation icons (I11): 13 EIP + 3 delivery + 3 AI, a 48×48 glyph before `scale`. */
79
+ export const brandNotationIcon = definePiece("brandNotationIcon", { name: { kind: "enum", required: true, values: BRAND_NOTATION_ICON_NAMES }, x: pos(), y: pos(), surface: surfaceParam, scale: num({ min: 0.25, max: 8 }) }, drawBrandNotationIcon);
@@ -0,0 +1,12 @@
1
+ import { type SurfaceName } from "./ink.ts";
2
+ export declare const CATEGORY_ICON_NAMES: readonly ["personalTools"];
3
+ export type CategoryIconName = (typeof CATEGORY_ICON_NAMES)[number];
4
+ export type CategoryIconParams = {
5
+ category: CategoryIconName;
6
+ x: number;
7
+ y: number;
8
+ surface?: SurfaceName;
9
+ scale?: number;
10
+ };
11
+ /** The site's fourth category icon ("Ferramentas pessoais" / "Personal tools"), neutral (no track colour), a 48×48 glyph before `scale`. */
12
+ export declare const categoryIcon: import("./piece.ts").Piece<CategoryIconParams>;
@@ -0,0 +1,26 @@
1
+ // The "Ferramentas pessoais" / "Personal tools" category icon (design.md
2
+ // decision 4): the only site category with no notation or diagram icon,
3
+ // requested of the design system in phase 4 (A6), glyph choice the agent's
4
+ // call (triage TRI-1). Ported from `~/.hermes/cache/scratch/ab267/phase4.py`'s
5
+ // `cat_icon()`, the "else" branch (a wrench crossing a screwdriver, a ring
6
+ // for the bolt) — the other three site categories already read from the
7
+ // brand notation / diagram grammar and are not this piece's job.
8
+ import { drawn, el, g } from "./node.js";
9
+ import { surfaceParam } from "./ink.js";
10
+ import { pos, num } from "./params.js";
11
+ import { definePiece } from "./piece.js";
12
+ import { colour } from "./params.js";
13
+ export const CATEGORY_ICON_NAMES = ["personalTools"];
14
+ const c = (path) => colour(path);
15
+ const drawCategoryIcon = (p) => {
16
+ const light = (p.surface ?? "light") === "light";
17
+ const text = light ? c("brand.light.text") : c("brand.dark.text");
18
+ const accent = light ? c("brand.light.accent") : c("brand.dark.accent");
19
+ const glyph = [
20
+ el("path", { d: "M12 36 L28 20 M24 12 L36 24", stroke: text, "stroke-width": 5, "stroke-linecap": "round", "stroke-linejoin": "round", fill: "none" }),
21
+ el("circle", { cx: 16, cy: 32, r: 6, fill: "none", stroke: accent, "stroke-width": 3.5 }),
22
+ ];
23
+ return drawn([g({ "data-piece": "category-icon-personal-tools", transform: `translate(${p.x} ${p.y}) scale(${p.scale ?? 1})`, stroke: "none" }, ...glyph)]);
24
+ };
25
+ /** The site's fourth category icon ("Ferramentas pessoais" / "Personal tools"), neutral (no track colour), a 48×48 glyph before `scale`. */
26
+ export const categoryIcon = definePiece("categoryIcon", { category: { kind: "enum", required: true, values: CATEGORY_ICON_NAMES }, x: pos(), y: pos(), surface: surfaceParam, scale: num({ min: 0.25, max: 8 }) }, drawCategoryIcon);
@@ -2,10 +2,17 @@ import { type PieceName } from "./library.ts";
2
2
  import { type SvgNode } from "./node.ts";
3
3
  import { type Problem } from "./params.ts";
4
4
  export declare const SAFE_ID: RegExp;
5
- /** Cover (800x420, the default), banner (1200x420, rounded corners), body diagram (560 laid out, 800 shown) or icon:
6
- * spec cover-cli, "Banner output", "Diagram output", "Icon output". */
7
- export declare const FORMATS: readonly ["cover", "banner", "diagram", "icon"];
5
+ /** Cover (800x420, the default), banner (1200x420, rounded corners), body diagram (560 laid out, 800 shown),
6
+ * icon, favicon (the symbol, both themes), linkedin-card (1080x1080) or avatar (500x500, the symbol only):
7
+ * spec cover-cli, "Banner output", "Diagram output", "Icon output", "New icon formats". */
8
+ export declare const FORMATS: readonly ["cover", "banner", "diagram", "icon", "favicon", "linkedin-card", "og", "avatar"];
8
9
  export type CoverFormat = (typeof FORMATS)[number];
10
+ /** Fixed frame sizes of the platform-shaped formats (not style, so not tokens — like EIP_ICON_WIDTH). */
11
+ export declare const FAVICON_SIZE = 32;
12
+ export declare const AVATAR_SIZE = 500;
13
+ export declare const LINKEDIN_CARD_SIZE = 1080;
14
+ export declare const OG_WIDTH = 1200;
15
+ export declare const OG_HEIGHT = 630;
9
16
  export type CoverPiece = {
10
17
  piece: PieceName;
11
18
  } & Record<string, unknown>;
@@ -32,10 +39,12 @@ type Framed = SpecCommon & {
32
39
  custom?: CustomNode;
33
40
  /** `custom` is drawn before `pieces[customAt]`; default after every piece. */
34
41
  customAt?: number;
42
+ /** Mira and the symbol, bottom-right corner, `cover.signatureMargin` from both edges (spec cover-cli, "Signature lockup"). */
43
+ signature?: boolean;
35
44
  };
36
45
  /** A cover spec, by format (spec cover-cli, "Cover spec format" and each format's keys). */
37
46
  export type CoverSpec = (Framed & {
38
- format?: "cover" | "banner";
47
+ format?: "cover" | "banner" | "linkedin-card" | "og";
39
48
  background: string;
40
49
  height?: never;
41
50
  }) | (Framed & {
@@ -52,6 +61,29 @@ export type CoverSpec = (Framed & {
52
61
  custom?: never;
53
62
  customAt?: never;
54
63
  height?: never;
64
+ signature?: never;
65
+ }) | (Omit<SpecCommon, "pieces"> & {
66
+ format: "favicon";
67
+ pieces?: never[];
68
+ title?: string;
69
+ description?: string;
70
+ background?: never;
71
+ rings?: never;
72
+ custom?: never;
73
+ customAt?: never;
74
+ height?: never;
75
+ signature?: never;
76
+ }) | (Omit<SpecCommon, "pieces"> & {
77
+ format: "avatar";
78
+ pieces?: never[];
79
+ title?: string;
80
+ description?: string;
81
+ background?: never;
82
+ rings?: never;
83
+ custom?: never;
84
+ customAt?: never;
85
+ height?: never;
86
+ signature?: never;
55
87
  });
56
88
  export declare const backgroundNames: () => string[];
57
89
  /** Compares JSON paths: segment by segment, array indexes as numbers, names by code point. */
@@ -72,6 +104,6 @@ export declare const diagramRings: (cx: number, cy: number, height: number, radi
72
104
  width: number;
73
105
  radius: number;
74
106
  };
75
- /** The cover's (banner's, diagram's, icon's) node tree. The spec must be valid for `lang` (validateCoverSpec returned no problem). */
107
+ /** The cover's (banner's, diagram's, icon's, favicon's, avatar's) node tree. The spec must be valid for `lang` (validateCoverSpec returned no problem). */
76
108
  export declare const coverTree: (input: CoverSpec, options?: CoverOptions) => SvgNode;
77
109
  export {};