@brunosouzas/design-system 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -25,7 +25,7 @@ The visual identity of [brunosouzas.com](https://brunosouzas.com): design tokens
25
25
 
26
26
  ## Status
27
27
 
28
- Early. The first change of the [roadmap](docs/roadmap.md) is implemented: the token source and its CSS, TypeScript and JSON exports, the motion components, and the circuit-breaker state machine as the first catalogue scene (16:9 and 4:5, Portuguese and English), with commands to render its stills and videos. The first variety scene, `publish-subscribe`, is implemented with independent message copies, Publish acceptance, two ACKs and a local invoice NACK. Its owner stills/video approval is pending; implementation is not visual approval. The illustration pieces (roadmap item 3) and the metro and diagram pieces (item 6) are implemented: the static drawing library that covers, banners, diagrams and scenes share, with a command that builds a blog cover from a JSON spec. The published covers are not ported to it yet (item 5).
28
+ Early. The first change of the [roadmap](docs/roadmap.md) is implemented: the token source and its CSS, TypeScript and JSON exports, the motion components, and the circuit-breaker state machine as the first catalogue scene (16:9 and 4:5, Portuguese and English), with commands to render its stills and videos. The first variety scene, `publish-subscribe`, is implemented with independent message copies, Publish acceptance, two ACKs and a local invoice NACK. Its owner stills/video approval is pending; implementation is not visual approval. The illustration pieces (roadmap item 3), the metro and diagram pieces (item 6) and stackledger's map markers, zones and icons (AB#167, 0.3.0) are implemented: the static drawing library that covers, banners, diagrams and scenes share, with a command that builds a blog cover from a JSON spec. The published covers are not ported to it yet (item 5).
29
29
 
30
30
  ## What it is
31
31
 
@@ -130,17 +130,21 @@ The components take every colour and size from the tokens, and what is visible a
130
130
 
131
131
  - `toSvg(node)` writes a tree as SVG text; `toReact(node)` turns the same tree into React elements. Both describe the same elements, attributes and text, which the tests check.
132
132
  - `pose(node, { x, y, tilt, squash, hop, scale, shake, opacity })` animates any piece around its base point; a scene recomputes the numbers every frame.
133
- - Every colour parameter is a token path (`"color.healthy"`, `"illustration.accent"`), never a hex value, except the two line fills of the map pieces below. Each piece checks its parameters before drawing and names the first wrong one. Positions and points take any finite number, because a map has no edge; sizes stay within 10000. A drawing that would hold a non-finite number throws `<piece>: the drawing overflows`.
134
- - The 35 pieces, with their anchors, parameters and defaults, are listed in [design §4](openspec/changes/archive/2026-10-23-illustration-pieces/design.md) (the 26 cover pieces) and in [design §3](openspec/changes/archive/2026-10-23-metro-and-diagram-pieces/design.md) of `metro-and-diagram-pieces` (the nine below); the rules are in [`openspec/specs/illustration-pieces/`](openspec/specs/illustration-pieces/spec.md). `npm run pieces:sheet -- --out <file.svg>` draws them all.
133
+ - Every colour parameter is a token path (`"color.healthy"`, `"illustration.accent"`), never a hex value, except the three data fills of the map pieces below. Each piece checks its parameters before drawing and names the first wrong one. Positions and points take any finite number, because a map has no edge; sizes stay within 10000. A drawing that would hold a non-finite number throws `<piece>: the drawing overflows`.
134
+ - The 41 pieces, with their anchors, parameters and defaults, are listed in [design §4](openspec/changes/archive/2026-10-23-illustration-pieces/design.md) (the 26 cover pieces), in [design §3](openspec/changes/archive/2026-10-23-metro-and-diagram-pieces/design.md) of `metro-and-diagram-pieces` (the nine map and diagram pieces) and in [design §3, §5 and §6](openspec/changes/archive/2026-10-23-stackledger-map-and-link-pieces/design.md) of `stackledger-map-and-link-pieces` (the six markers, zones and icons); the rules are in [`openspec/specs/illustration-pieces/`](openspec/specs/illustration-pieces/spec.md). `npm run pieces:sheet -- --out <file.svg>` draws them all.
135
135
 
136
136
  ### Maps and diagrams
137
137
 
138
- Nine pieces draw a metro map and its page diagrams, for [stackledger](https://github.com/brunosouzas/stackledger) and any other map in this style: `lineBadge`, `stationMarker` (stop, interchange as a capsule, deprecated), `pipe`, `countBadge`, `nodeBlock`, `pillNode`, `hexNode` (normal, focus, placeholder), `messageArrow` (curved or straight, with a head and an optional label) and `ledgerMark` (stackledger's mark). They paint what the caller lays out: positions, text measurement and truncation stay with the map engine.
138
+ Thirteen pieces draw a metro map and its page diagrams, for [stackledger](https://github.com/brunosouzas/stackledger) and any other map in this style: `lineBadge`, `stationMarker` (stop, interchange as a capsule, deprecated), `pipe`, `countBadge`, `nodeBlock`, `pillNode`, `hexNode` (normal, focus, placeholder), `messageArrow` (curved or straight, with a head and an optional label) `ledgerMark` (stackledger's mark; `surface: "dark"` lines it with a light rim), `renewalRing` (orange bands round a station), `suggestedHalo` (a dashed muted circle), `licenceMark` (a 10-unit disc, green when held, white to buy) and `zone` (the outline the caller gives, widened by a round 32-unit stroke at the surface's zone opacity, with an optional title). They paint what the caller lays out: positions, hulls, text measurement and truncation stay with the map engine.
139
139
 
140
- - `surface: "light" | "dark"` picks the background they sit on (`surface.*` tokens); every map and diagram piece takes it except `pipe` and `ledgerMark`. On dark the hard shadow takes `surface.dark.shadow` and the outline gets a light rim ([ADR 0003](docs/adr/0003-visual-language-and-colour-meaning.md), amendment of AB#157).
140
+ - A `placeholder` node may have a `subtitle` (drawn like a normal node's, all muted), and `lineBadge` may have no `text` (the pill alone, 30 wide by default).
141
+ - The licence's disc is small, so it has its own room rule: `size` at least three outlines (9 at ink scale 1).
142
+
143
+ - `surface: "light" | "dark"` picks the background they sit on (`surface.*` tokens); every map and diagram piece takes it except `pipe`; `ledgerMark` takes only `surface`. On dark the hard shadow takes `surface.dark.shadow` and the outline gets a light rim ([ADR 0003](docs/adr/0003-visual-language-and-colour-meaning.md), amendment of AB#157).
141
144
  - `inkScale` (0.25 to 8) thickens the outline, rim and shadow without changing the geometry, on `lineBadge`, `stationMarker`, `countBadge` and the three nodes; the map uses 1, the page diagrams 1.6. A shape's height (a marker's diameter) must be at least four outlines; a placeholder node, which draws no ink, is exempt.
142
- - A line's colour comes from the map's data, so `pipe.fill` and `lineBadge.fill` also accept `#RRGGBB`. Every other colour is a token path, and a cover spec rejects any hex.
143
- - The `map.*` tokens hold the measures (outline 3, shadow 3.5, line 12, casing 6, badge 20, node 46…), and `site.*` the page palette, fonts and type scale of brunosouzas.com in light and dark.
145
+ - A line's colour comes from the map's data, so `pipe.fill`, `lineBadge.fill` and `zone.fill` also accept `#RRGGBB`. Every other colour is a token path, and a cover spec rejects any hex.
146
+ - `map.bendRadius` (32) is the radius of a bundle's centre line at a 45° or 90° bend; no piece reads it, the map engine draws the arcs (`pipe.d` takes `A`).
147
+ - The `map.*` tokens hold the measures (outline 3, shadow 3.5, line 12, casing 6, badge 20, node 46, ring 3, halo 1.5, licence 10, zone padding 32, bend radius 32…), `surface.*.zoneOpacity` the zone's opacity (0.12, dark 0.22), and `site.*` the page palette, fonts and type scale of brunosouzas.com in light and dark.
144
148
 
145
149
  ```ts
146
150
  import { lineBadge, pipe, stationMarker, toSvg } from "@brunosouzas/design-system";
@@ -186,6 +190,17 @@ The banner is one image that scales down as a whole. Words in it become small on
186
190
 
187
191
  Add it to `src/pieces/library.ts` (or `src/pieces/metro.ts` for a map or diagram piece) with `definePiece(name, schema, draw)`, take its colours and sizes from the tokens (a new colour goes to `tokens/source.json` first), register it in `PIECES`, and give it a sample in `scripts/pieces-sheet.ts`. The catalogue test then checks that it draws with only its required parameters and uses token colours only.
188
192
 
193
+ ### UI and link icons
194
+
195
+ - `uiIcon({ name, x, y, size, colour })` draws one of seven glyphs of the sites' chrome in a 24-unit box and one token colour: `sun`, `moon`, `zoomIn`, `zoomOut`, `reset`, `calendar`, `download` (data in `UI_ICONS`). `colour: "currentColor"` makes the icon follow its element's text colour (a button that inverts on hover, say); it only works when the SVG is inline in the page, since an `<img>` does not inherit the page's colour, and a cover spec refuses it.
196
+ - `linkIcon({ icon, x, y, size, surface })` draws a link sticker as brunosouzas.com draws its social icons (46-unit box, light or dark frame): `github`, `linkedin`, `email`, `website` (data in `LINK_ICONS`). The GitHub mark is from [Font Awesome](https://fontawesome.com/), licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
197
+ - Neither holds a link, a style, a script or a URL, so a consumer can write one with `toSvg` inside its own `<svg xmlns=…>` and ship it as a static file.
198
+
199
+ ```ts
200
+ import { el, linkIcon, toSvg } from "@brunosouzas/design-system";
201
+ const github = toSvg(el("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 46 46", width: 28, height: 28 }, ...linkIcon({ icon: "github", x: 0, y: 0, surface: "dark" }).body));
202
+ ```
203
+
189
204
  ### EIP icons
190
205
 
191
206
  `src/pieces/eip-icons.ts` holds the 26 Enterprise Integration Patterns icons as data. They are redrawn from the icons by Gregor Hohpe and Bobby Woolf ([enterpriseintegrationpatterns.com](https://www.enterpriseintegrationpatterns.com/)), licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); each icon keeps that credit in its description.
@@ -10,7 +10,7 @@
10
10
  import { tokens } from "../../tokens/generated/tokens.js";
11
11
  import { PIECE_NAMES, PIECES, rings } from "./library.js";
12
12
  import { el, g } from "./node.js";
13
- import { COLOUR_TOKENS, checkParams, isColourLiteral, MAX_SIZE, PATH_DATA } from "./params.js";
13
+ import { COLOUR_TOKENS, CURRENT_COLOUR, checkParams, isColourLiteral, MAX_SIZE, PATH_DATA } from "./params.js";
14
14
  export const SAFE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
15
  const TOP_LEVEL = ["id", "title", "description", "format", "background", "rings", "pieces", "custom"];
16
16
  /** Cover (800x420, the default) or banner (1200x420, rounded corners): spec cover-cli, "Banner output". */
@@ -191,6 +191,12 @@ export const validateCoverSpec = (spec) => {
191
191
  return void out.push({ path: `${at}.piece`, message: "rings belong to the cover's rings field" });
192
192
  const definition = PIECES[piece];
193
193
  const own = checkParams(definition.schema, params, at);
194
+ // A cover is a standalone image: currentColor (accepted by uiIcon for inline page icons) is refused.
195
+ for (const [name, spec] of Object.entries(definition.schema)) {
196
+ if (spec.kind === "color" && spec.currentColor && params[name] === CURRENT_COLOUR) {
197
+ own.push({ path: `${at}.${name}`, message: "currentColor is for icons inline in a page; a cover uses a token path" });
198
+ }
199
+ }
194
200
  out.push(...own);
195
201
  // Limits between parameters are reported with the schema's problems, except on a parameter
196
202
  // already reported; a relation that cannot read a broken parameter reports nothing.
@@ -1,4 +1,6 @@
1
1
  export * from "./metro.ts";
2
+ export * from "./ui-icons.ts";
3
+ export * from "./link-icons.ts";
2
4
  type Point = [number, number];
3
5
  export type PillParams = {
4
6
  x: number;
@@ -269,6 +271,10 @@ export declare const PIECES: {
269
271
  readonly hexNode: import("./piece.ts").Piece<import("./metro.ts").NodeParams>;
270
272
  readonly messageArrow: import("./piece.ts").Piece<import("./metro.ts").MessageArrowParams>;
271
273
  readonly ledgerMark: import("./piece.ts").Piece<import("./metro.ts").LedgerMarkParams>;
274
+ readonly renewalRing: import("./piece.ts").Piece<import("./metro.ts").RenewalRingParams>;
275
+ readonly suggestedHalo: import("./piece.ts").Piece<import("./metro.ts").SuggestedHaloParams>;
276
+ readonly licenceMark: import("./piece.ts").Piece<import("./metro.ts").LicenceMarkParams>;
277
+ readonly zone: import("./piece.ts").Piece<import("./metro.ts").ZoneParams>;
272
278
  readonly pill: import("./piece.ts").Piece<PillParams>;
273
279
  readonly rings: import("./piece.ts").Piece<RingsParams>;
274
280
  readonly sheet: import("./piece.ts").Piece<SheetParams>;
@@ -295,6 +301,8 @@ export declare const PIECES: {
295
301
  readonly book: import("./piece.ts").Piece<BookParams>;
296
302
  readonly muleFlow: import("./piece.ts").Piece<MuleFlowParams>;
297
303
  readonly eipIcon: import("./piece.ts").Piece<EipIconParams>;
304
+ readonly uiIcon: import("./piece.ts").Piece<import("./ui-icons.ts").UiIconParams>;
305
+ readonly linkIcon: import("./piece.ts").Piece<import("./link-icons.ts").LinkIconParams>;
298
306
  };
299
307
  export type PieceName = keyof typeof PIECES;
300
308
  export declare const PIECE_NAMES: PieceName[];
@@ -12,7 +12,11 @@ import { drawn, el, g, r2, shadowed } from "./node.js";
12
12
  import { col, colour, count, label, MAX_SIZE, PATH_DATA, pos, size } from "./params.js";
13
13
  import { definePiece } from "./piece.js";
14
14
  import { METRO_PIECES } from "./metro.js";
15
+ import { uiIcon } from "./ui-icons.js";
16
+ import { linkIcon } from "./link-icons.js";
15
17
  export * from "./metro.js";
18
+ export * from "./ui-icons.js";
19
+ export * from "./link-icons.js";
16
20
  const ink = tokens.color.ink;
17
21
  const mono = tokens.font.mono;
18
22
  /** Pill label centred on (x, y), usually overlapping the top edge of what it names. */
@@ -375,5 +379,7 @@ export const PIECES = {
375
379
  muleFlow,
376
380
  eipIcon,
377
381
  ...METRO_PIECES,
382
+ uiIcon,
383
+ linkIcon,
378
384
  };
379
385
  export const PIECE_NAMES = Object.keys(PIECES);
@@ -0,0 +1,22 @@
1
+ import { type SvgNode } from "./node.ts";
2
+ import { type SurfaceName } from "./metro.ts";
3
+ export type LinkIcon = {
4
+ readonly name: string;
5
+ readonly title: string;
6
+ readonly description: string; /** A colour token path. */
7
+ readonly face: string;
8
+ readonly glyph: readonly SvgNode[];
9
+ };
10
+ /** Every link icon draws in a 46x46 box. */
11
+ export declare const LINK_ICON_SIZE = 46;
12
+ export declare const LINK_ICONS: readonly LinkIcon[];
13
+ export declare const LINK_ICON_NAMES: string[];
14
+ export type LinkIconParams = {
15
+ icon: string;
16
+ x: number;
17
+ y: number;
18
+ size?: number;
19
+ surface?: SurfaceName;
20
+ };
21
+ /** One link sticker at (x, y), `size` wide (46 by default), on the light or the dark page. */
22
+ export declare const linkIcon: import("./piece.ts").Piece<LinkIconParams>;
@@ -0,0 +1,80 @@
1
+ // Link icons as data (spec illustration-pieces, "Link icons as data"; design §6
2
+ // of stackledger-map-and-link-pieces): the social stickers of brunosouzas.com
3
+ // (public/images/home/social-*.svg at e29b3dd), with every colour replaced by
4
+ // its token, plus a `website` globe in the same style. `linkIcon` draws the
5
+ // site's frame (light, or dark with its rim) around the face and the glyph, at
6
+ // the site's measures (46 box, face 38 at 2, radius 9, outline 2.5).
7
+ //
8
+ // Credit: the GitHub mark is Font Awesome's (fontawesome.com), licensed
9
+ // CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/); its description
10
+ // keeps the credit.
11
+ //
12
+ // What can go wrong: an unknown icon is rejected by the schema. No node holds
13
+ // a link, a style, a script or a URL, so the output can ship as a static file.
14
+ import { tokens } from "../../tokens/generated/tokens.js";
15
+ import { drawn, el, g, r2 } from "./node.js";
16
+ import { colour, pos, size } from "./params.js";
17
+ import { definePiece } from "./piece.js";
18
+ import { SURFACES } from "./metro.js";
19
+ const C = tokens.color;
20
+ const INK = C.ink;
21
+ /** Every link icon draws in a 46x46 box. */
22
+ export const LINK_ICON_SIZE = 46;
23
+ export const LINK_ICONS = [
24
+ {
25
+ name: "github",
26
+ title: "GitHub",
27
+ description: "GitHub icon. The GitHub mark is from Font Awesome (fontawesome.com), CC BY 4.0.",
28
+ face: "color.white",
29
+ glyph: [g({ transform: "translate(9.5 9) scale(0.0455)" }, el("path", { d: "M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z", fill: INK, stroke: "none" }))],
30
+ },
31
+ {
32
+ name: "linkedin",
33
+ title: "LinkedIn",
34
+ description: "LinkedIn icon.",
35
+ face: "color.caller",
36
+ glyph: [
37
+ el("rect", { x: 12, y: 18.5, width: 4, height: 11.5, rx: 1, fill: INK, stroke: "none" }),
38
+ el("circle", { cx: 14, cy: 13.8, r: 2.4, fill: INK, stroke: "none" }),
39
+ el("path", { d: "M19.5,30 V18.5 H23.5 V20.3 C24.5,19 26.2,18.1 28,18.1 C31.2,18.1 32.5,20.2 32.5,23.6 V30 H28.5 V24.3 C28.5,22.7 27.8,21.9 26.6,21.9 C25.2,21.9 23.5,22.8 23.5,24.7 V30 Z", fill: INK, stroke: "none" }),
40
+ ],
41
+ },
42
+ {
43
+ name: "email",
44
+ title: "Email",
45
+ description: "Email icon.",
46
+ face: "color.transitional",
47
+ glyph: [
48
+ el("rect", { x: 9, y: 13, width: 24, height: 17, rx: 3, fill: C.white, stroke: INK, "stroke-width": 2.5 }),
49
+ el("path", { d: "M10,15 L21,23 L32,15", fill: "none", stroke: INK, "stroke-width": 2.5, "stroke-linejoin": "round" }),
50
+ ],
51
+ },
52
+ {
53
+ name: "website",
54
+ title: "Website",
55
+ description: "Website icon: a globe.",
56
+ face: "color.healthy",
57
+ glyph: [
58
+ el("circle", { cx: 21, cy: 21, r: 10.5, fill: C.white, stroke: INK, "stroke-width": 2.5 }),
59
+ el("ellipse", { cx: 21, cy: 21, rx: 4.6, ry: 10.5, fill: "none", stroke: INK, "stroke-width": 2 }),
60
+ el("path", { d: "M10.5 21H31.5M12.5 15.5H29.5M12.5 26.5H29.5", fill: "none", stroke: INK, "stroke-width": 2, "stroke-linecap": "round" }),
61
+ ],
62
+ },
63
+ ];
64
+ export const LINK_ICON_NAMES = LINK_ICONS.map((icon) => icon.name);
65
+ /** The site's frame: the shadow (and on dark the rim) behind the face. */
66
+ const frame = (surface) => {
67
+ const s = tokens.surface[surface];
68
+ return s.rimWidth > 0
69
+ ? [
70
+ el("rect", { x: 4, y: 4, width: 41, height: 41, rx: 11, fill: s.shadow, stroke: "none" }),
71
+ el("rect", { x: 0.5, y: 0.5, width: 41, height: 41, rx: 11, fill: s.rim, stroke: "none" }),
72
+ ]
73
+ : [el("rect", { x: 5, y: 5, width: 38, height: 38, rx: 9, fill: s.shadow, stroke: "none" })];
74
+ };
75
+ /** One link sticker at (x, y), `size` wide (46 by default), on the light or the dark page. */
76
+ export const linkIcon = definePiece("linkIcon", { icon: { kind: "enum", required: true, values: LINK_ICON_NAMES }, x: pos(), y: pos(), size: size(), surface: { kind: "enum", values: SURFACES } }, ({ icon, x, y, size: side = LINK_ICON_SIZE, surface = "light" }) => {
77
+ const data = LINK_ICONS.find((candidate) => candidate.name === icon);
78
+ const face = el("rect", { x: 2, y: 2, width: 38, height: 38, rx: 9, fill: colour(data.face, "face"), stroke: INK, "stroke-width": 2.5 });
79
+ return drawn([g({ transform: `translate(${r2(x)} ${r2(y)}) scale(${side / LINK_ICON_SIZE})`, stroke: "none" }, ...frame(surface), face, ...data.glyph)]);
80
+ });
@@ -8,13 +8,13 @@ export type Ink = {
8
8
  export type LineBadgeParams = Ink & {
9
9
  x: number;
10
10
  y: number;
11
- text: string;
11
+ text?: string;
12
12
  fill: string;
13
13
  width?: number;
14
14
  height?: number;
15
15
  flat?: boolean;
16
16
  };
17
- /** A line's terminal badge: a pill in the line colour with its code in white, centred on (x, y). */
17
+ /** A line's terminal badge: a pill in the line colour with its code in white, centred on (x, y); without `text`, the pill alone. */
18
18
  export declare const lineBadge: import("./piece.ts").Piece<LineBadgeParams>;
19
19
  export type StationMarkerParams = Ink & {
20
20
  cx: number;
@@ -76,9 +76,44 @@ export type LedgerMarkParams = {
76
76
  y: number;
77
77
  size?: number;
78
78
  line?: string;
79
+ surface?: SurfaceName;
79
80
  };
80
- /** stackledger's mark (favicon, header): an ink square with a bent line and two stations, 32 units scaled to `size`. */
81
+ /** stackledger's mark (favicon, header): an ink square with a bent line and two stations, 32 units scaled to `size`.
82
+ * On a surface with a rim (dark), a light rim of 2 × rimWidth lines the square inside its 32 box (card 167). */
81
83
  export declare const ledgerMark: import("./piece.ts").Piece<LedgerMarkParams>;
84
+ export type RenewalRingParams = Ink & {
85
+ cx: number;
86
+ cy: number;
87
+ r: number;
88
+ width?: number;
89
+ };
90
+ /** The renewal ring: bands like a bent pipe round a station (rim on dark, ink, then color.blocked), no shadow. */
91
+ export declare const renewalRing: import("./piece.ts").Piece<RenewalRingParams>;
92
+ export type SuggestedHaloParams = {
93
+ cx: number;
94
+ cy: number;
95
+ r: number;
96
+ surface?: SurfaceName;
97
+ };
98
+ /** The suggested-position halo: a dashed muted circle, no ink. */
99
+ export declare const suggestedHalo: import("./piece.ts").Piece<SuggestedHaloParams>;
100
+ export type LicenceMarkParams = Ink & {
101
+ cx: number;
102
+ cy: number;
103
+ state?: "owned" | "toBuy";
104
+ size?: number;
105
+ };
106
+ /** A licence: a small disc sticker, green when held, white to buy. Its own room rule: size ≥ 3 × outline. */
107
+ export declare const licenceMark: import("./piece.ts").Piece<LicenceMarkParams>;
108
+ export type ZoneParams = {
109
+ points: Point[];
110
+ fill: string;
111
+ title?: string;
112
+ titleAt?: Point;
113
+ surface?: SurfaceName;
114
+ };
115
+ /** A zone: the outline the caller gives (stackledger's hull), widened by a round stroke, at the surface's zone opacity; an optional title. */
116
+ export declare const zone: import("./piece.ts").Piece<ZoneParams>;
82
117
  export declare const METRO_PIECES: {
83
118
  readonly lineBadge: import("./piece.ts").Piece<LineBadgeParams>;
84
119
  readonly stationMarker: import("./piece.ts").Piece<StationMarkerParams>;
@@ -89,5 +124,9 @@ export declare const METRO_PIECES: {
89
124
  readonly hexNode: import("./piece.ts").Piece<NodeParams>;
90
125
  readonly messageArrow: import("./piece.ts").Piece<MessageArrowParams>;
91
126
  readonly ledgerMark: import("./piece.ts").Piece<LedgerMarkParams>;
127
+ readonly renewalRing: import("./piece.ts").Piece<RenewalRingParams>;
128
+ readonly suggestedHalo: import("./piece.ts").Piece<SuggestedHaloParams>;
129
+ readonly licenceMark: import("./piece.ts").Piece<LicenceMarkParams>;
130
+ readonly zone: import("./piece.ts").Piece<ZoneParams>;
92
131
  };
93
132
  export {};
@@ -50,9 +50,10 @@ const sticker = (shape, face, inkParams) => {
50
50
  // Every text sets its own anchor, size and weight, so a surrounding group (a cover's label style) does not leak in.
51
51
  const words = (x, y, value, attrs) => el("text", { x: r2(x), y: r2(y), "font-family": font, ...attrs }, value);
52
52
  const pill = (x, y, w, h) => (dx, dy, attrs) => el("rect", { x: r2(x + dx), y: r2(y + dy), width: r2(w), height: r2(h), rx: r2(h / 2), ...attrs });
53
- const badgeWidth = ({ width, text }) => width ?? Math.max(30, 7.6 * Array.from(text).length + 18);
54
- /** A line's terminal badge: a pill in the line colour with its code in white, centred on (x, y). */
55
- export const lineBadge = definePiece("lineBadge", { x: pos(), y: pos(), text: label(), fill: dataColour(), width: size(), height: size(), flat: { kind: "boolean" }, ...inkSchema }, (params) => {
53
+ // Without a text, the default width is the one-character width (card 167).
54
+ const badgeWidth = ({ width, text }) => width ?? Math.max(30, 7.6 * (text === undefined ? 1 : Array.from(text).length) + 18);
55
+ /** A line's terminal badge: a pill in the line colour with its code in white, centred on (x, y); without `text`, the pill alone. */
56
+ export const lineBadge = definePiece("lineBadge", { x: pos(), y: pos(), text: label(false), fill: dataColour(), width: size(), height: size(), flat: { kind: "boolean" }, ...inkSchema }, (params) => {
56
57
  const { x, y, text, fill, height = M.badgeHeight, flat = false } = params;
57
58
  const width = badgeWidth(params);
58
59
  const face = paint(fill);
@@ -60,7 +61,7 @@ export const lineBadge = definePiece("lineBadge", { x: pos(), y: pos(), text: la
60
61
  const inset = flat ? outline / 2 : 0;
61
62
  const shape = pill(x - width / 2 + inset, y - height / 2 + inset, width - 2 * inset, height - 2 * inset);
62
63
  const body = flat ? [shape(0, 0, { fill: face, stroke: ink, "stroke-width": r2(outline), "stroke-linejoin": "round" })] : sticker(shape, face, params);
63
- return drawn(body, [words(x, y + 4.5, text, { fill: white, "font-size": M.badgeTextSize, "font-weight": bold, "text-anchor": "middle" })]);
64
+ return drawn(body, text === undefined ? [] : [words(x, y + 4.5, text, { fill: white, "font-size": M.badgeTextSize, "font-weight": bold, "text-anchor": "middle" })]);
64
65
  }, (params) => {
65
66
  const height = params.height ?? M.badgeHeight;
66
67
  return limits(atLeast("width", badgeWidth(params), height, "the height"), roomFor("height", height, params));
@@ -125,10 +126,11 @@ const nodeSchema = {
125
126
  const nodeLimits = (kind) => (params) => {
126
127
  const { width, height = M.nodeHeight, subtitle, state = "normal" } = params;
127
128
  const radius = params.radius ?? M.nodeRadius;
128
- return limits(atLeast("width", width, 2 * height, "twice the height"), kind === "box" && radius > height / 2 && { path: "radius", message: `must be at most half the height (${r2(height / 2)}), found ${r2(radius)}` }, state === "placeholder" && subtitle !== undefined && { path: "subtitle", message: "a placeholder has no subtitle" }, state !== "placeholder" && roomFor("height", height, params));
129
+ return limits(atLeast("width", width, 2 * height, "twice the height"), kind === "box" && radius > height / 2 && { path: "radius", message: `must be at most half the height (${r2(height / 2)}), found ${r2(radius)}` }, state !== "placeholder" && roomFor("height", height, params));
129
130
  };
130
131
  const drawNode = (kind, params) => {
131
132
  const { x, y, width, height = M.nodeHeight, title, subtitle, state = "normal" } = params;
133
+ const inset = kind === "box" ? 12 : height / 2;
132
134
  const radius = kind === "pill" ? height / 2 : (params.radius ?? M.nodeRadius);
133
135
  const shape = kind === "hex"
134
136
  ? (dx, dy, attrs) => {
@@ -140,10 +142,16 @@ const drawNode = (kind, params) => {
140
142
  const { s } = inkOf(params);
141
143
  const middle = y + height / 2;
142
144
  if (state === "placeholder") {
143
- return drawn([shape(0, 0, { fill: "none", stroke: s.muted, "stroke-width": 1.5, "stroke-dasharray": "5 4", "stroke-linejoin": "round" })], [words(x + width / 2, middle + 4.5, title, { fill: s.muted, "font-size": M.labelSize, "font-weight": 400, "text-anchor": "middle" })]);
145
+ const dashed = [shape(0, 0, { fill: "none", stroke: s.muted, "stroke-width": 1.5, "stroke-dasharray": "5 4", "stroke-linejoin": "round" })];
146
+ // With a subtitle, a normal node's text layout, every word muted (stackledger's unlinked box; card 167).
147
+ return drawn(dashed, subtitle === undefined
148
+ ? [words(x + width / 2, middle + 4.5, title, { fill: s.muted, "font-size": M.labelSize, "font-weight": 400, "text-anchor": "middle" })]
149
+ : [
150
+ words(x + inset, middle - 3, title, { fill: s.muted, "font-size": M.labelSize, "font-weight": bold, "text-anchor": "start" }),
151
+ words(x + inset, middle + 14, subtitle, { fill: s.muted, "font-size": 11, "font-weight": 400, "text-anchor": "start" }),
152
+ ]);
144
153
  }
145
154
  const focus = state === "focus";
146
- const inset = kind === "box" ? 12 : height / 2;
147
155
  const titleFill = focus ? ink : s.text;
148
156
  const text = subtitle === undefined
149
157
  ? [words(x + inset, middle + 4.5, title, { fill: titleFill, "font-size": M.labelSize, "font-weight": bold, "text-anchor": "start" })]
@@ -198,8 +206,61 @@ export const messageArrow = definePiece("messageArrow", {
198
206
  const distance = Math.hypot(to[0] - from[0], to[1] - from[1]);
199
207
  return limits(distance === 0 && { path: "to", message: "must differ from from" }, distance > 0 && head && distance < 10 * width && { path: "to", message: `is too close to from for the head: needs at least ${r2(10 * width)}, found ${r2(distance)}` });
200
208
  });
201
- /** stackledger's mark (favicon, header): an ink square with a bent line and two stations, 32 units scaled to `size`. */
202
- export const ledgerMark = definePiece("ledgerMark", { x: pos(), y: pos(), size: { kind: "number", min: 12, max: 512 }, line: col() }, ({ x, y, size: side = 32, line = "illustration.accent" }) => drawn([
203
- g({ transform: `translate(${r2(x)} ${r2(y)}) scale(${side / 32})` }, el("rect", { x: 0, y: 0, width: 32, height: 32, rx: 7, fill: ink, stroke: "none" }), el("path", { d: "M7 23 L14 16 L25 16", fill: "none", stroke: colour(line, "line"), "stroke-width": 4.5, "stroke-linecap": "round", "stroke-linejoin": "round" }), el("circle", { cx: 14, cy: 16, r: 4, fill: white, stroke: ink, "stroke-width": 2 }), el("circle", { cx: 25, cy: 16, r: 2.6, fill: white, stroke: ink, "stroke-width": 1.6 })),
204
- ]));
205
- export const METRO_PIECES = { lineBadge, stationMarker, pipe, countBadge, nodeBlock, pillNode, hexNode, messageArrow, ledgerMark };
209
+ /** stackledger's mark (favicon, header): an ink square with a bent line and two stations, 32 units scaled to `size`.
210
+ * On a surface with a rim (dark), a light rim of 2 × rimWidth lines the square inside its 32 box (card 167). */
211
+ export const ledgerMark = definePiece("ledgerMark", { x: pos(), y: pos(), size: { kind: "number", min: 12, max: 512 }, line: col(), surface: surfaceParam }, ({ x, y, size: side = 32, line = "illustration.accent", surface = "light" }) => {
212
+ const { rimWidth, rim } = tokens.surface[surface];
213
+ const band = 2 * rimWidth;
214
+ const square = rimWidth > 0
215
+ ? [
216
+ el("rect", { x: 0, y: 0, width: 32, height: 32, rx: 7, fill: rim, stroke: "none" }),
217
+ el("rect", { x: band, y: band, width: 32 - 2 * band, height: 32 - 2 * band, rx: 7 - band, fill: ink, stroke: "none" }),
218
+ ]
219
+ : [el("rect", { x: 0, y: 0, width: 32, height: 32, rx: 7, fill: ink, stroke: "none" })];
220
+ return drawn([
221
+ g({ transform: `translate(${r2(x)} ${r2(y)}) scale(${side / 32})` }, ...square, el("path", { d: "M7 23 L14 16 L25 16", fill: "none", stroke: colour(line, "line"), "stroke-width": 4.5, "stroke-linecap": "round", "stroke-linejoin": "round" }), el("circle", { cx: 14, cy: 16, r: 4, fill: white, stroke: ink, "stroke-width": 2 }), el("circle", { cx: 25, cy: 16, r: 2.6, fill: white, stroke: ink, "stroke-width": 1.6 })),
222
+ ]);
223
+ });
224
+ /** The renewal ring: bands like a bent pipe round a station (rim on dark, ink, then color.blocked), no shadow. */
225
+ export const renewalRing = definePiece("renewalRing", { cx: pos(), cy: pos(), r: size(true), width: { kind: "number", min: 0, exclusiveMin: true, max: 50 }, ...inkSchema }, (params) => {
226
+ const { cx, cy, r, width = M.ringWidth } = params;
227
+ const { s, outline, rim } = inkOf(params);
228
+ // Radius and band widths as given, like pipe's widths: a valid hairline must not round to zero.
229
+ const band = (stroke, strokeWidth) => el("circle", { cx: r2(cx), cy: r2(cy), r, fill: "none", stroke, "stroke-width": strokeWidth });
230
+ return drawn([...(s.rimWidth > 0 ? [band(s.rim, width + outline + 2 * rim)] : []), band(ink, width + outline), band(tokens.color.blocked, width)]);
231
+ }, (params) => {
232
+ const { outline, rim } = inkOf(params);
233
+ const need = ((params.width ?? M.ringWidth) + outline + 2 * rim) / 2;
234
+ return limits(params.r < need && { path: "r", message: `leaves no room for the ring's bands: needs at least ${r2(need)}` });
235
+ });
236
+ /** The suggested-position halo: a dashed muted circle, no ink. */
237
+ export const suggestedHalo = definePiece("suggestedHalo", { cx: pos(), cy: pos(), r: size(true), surface: surfaceParam }, ({ cx, cy, r, surface = "light" }) => drawn([el("circle", { cx: r2(cx), cy: r2(cy), r, fill: "none", stroke: tokens.surface[surface].muted, "stroke-width": M.haloWidth, "stroke-dasharray": "4 3" })]));
238
+ /** A licence: a small disc sticker, green when held, white to buy. Its own room rule: size ≥ 3 × outline. */
239
+ export const licenceMark = definePiece("licenceMark", { cx: pos(), cy: pos(), state: { kind: "enum", values: ["owned", "toBuy"] }, size: size(), ...inkSchema }, (params) => {
240
+ const { cx, cy, state = "owned", size: side = M.licenceSize } = params;
241
+ const shape = (dx, dy, attrs) => el("circle", { cx: r2(cx + dx), cy: r2(cy + dy), r: r2(side / 2), ...attrs });
242
+ return drawn(sticker(shape, state === "owned" ? tokens.color.healthy : white, params));
243
+ }, (params) => {
244
+ const need = 3 * inkOf(params).outline;
245
+ const side = params.size ?? M.licenceSize;
246
+ return limits(side < need && { path: "size", message: `leaves no room for the licence's face: needs at least ${r2(need)} at this ink scale` });
247
+ });
248
+ /** Mixes a #RRGGBB colour with white by `amount` (stackledger's zone title tint). */
249
+ const tint = (hex, amount) => "#" + [1, 3, 5].map((i) => Math.round(parseInt(hex.slice(i, i + 2), 16) * (1 - amount) + 255 * amount).toString(16).padStart(2, "0").toUpperCase()).join("");
250
+ /** A zone: the outline the caller gives (stackledger's hull), widened by a round stroke, at the surface's zone opacity; an optional title. */
251
+ export const zone = definePiece("zone", { points: { kind: "array", required: true, minItems: 1, maxItems: 10000, of: point() }, fill: dataColour(), title: label(false), titleAt: point(false), surface: surfaceParam }, ({ points, fill, title, titleAt, surface = "light" }) => {
252
+ const colourOf = paint(fill);
253
+ const pad = M.zonePadding;
254
+ const at = (p) => `${r2(p[0])},${r2(p[1])}`;
255
+ const shape = points.length === 1
256
+ ? el("circle", { cx: r2(points[0][0]), cy: r2(points[0][1]), r: pad, fill: colourOf })
257
+ : points.length === 2
258
+ ? el("line", { x1: r2(points[0][0]), y1: r2(points[0][1]), x2: r2(points[1][0]), y2: r2(points[1][1]), stroke: colourOf, "stroke-width": 2 * pad, "stroke-linecap": "round" })
259
+ : el("polygon", { points: points.map(at).join(" "), fill: colourOf, stroke: colourOf, "stroke-width": 2 * pad, "stroke-linejoin": "round" });
260
+ const titleFill = surface === "dark" ? tint(colourOf, M.zoneTitleTint) : colourOf;
261
+ const text = title !== undefined && titleAt !== undefined
262
+ ? [words(titleAt[0], titleAt[1] + 13, title, { fill: titleFill, "font-size": M.labelSize, "font-weight": bold, "text-anchor": "start" })]
263
+ : [];
264
+ return drawn([g({ opacity: tokens.surface[surface].zoneOpacity, stroke: "none" }, shape)], text);
265
+ }, ({ title, titleAt }) => limits(title !== undefined && titleAt === undefined && { path: "titleAt", message: "is required with a title" }, titleAt !== undefined && title === undefined && { path: "title", message: "is required with titleAt" }));
266
+ export const METRO_PIECES = { lineBadge, stationMarker, pipe, countBadge, nodeBlock, pillNode, hexNode, messageArrow, ledgerMark, renewalRing, suggestedHalo, licenceMark, zone };
@@ -5,9 +5,12 @@ export type ParamSpec = {
5
5
  exclusiveMin?: boolean;
6
6
  max?: number;
7
7
  integer?: boolean;
8
- } | {
8
+ }
9
+ /** A token path; with `currentColor`, also that exact keyword (uiIcon.colour only, card 170). */
10
+ | {
9
11
  kind: "color";
10
12
  required?: boolean;
13
+ currentColor?: boolean;
11
14
  }
12
15
  /** A token path, or a colour from the consumer's data (#RRGGBB): pipe and lineBadge fills only. */
13
16
  | {
@@ -72,6 +75,10 @@ export declare const size: (required?: boolean, min?: number) => ParamSpec;
72
75
  /** A position: any finite number, because a map position has no maximum. */
73
76
  export declare const pos: () => ParamSpec;
74
77
  export declare const col: () => ParamSpec;
78
+ /** The paint keyword that follows the text colour of an inline SVG's element. */
79
+ export declare const CURRENT_COLOUR = "currentColor";
80
+ /** A token path or `currentColor`: for icons written inline in a page. */
81
+ export declare const colOrCurrent: () => ParamSpec;
75
82
  /** SVG path data: commands, digits, signs, dots, commas and spaces only. */
76
83
  export declare const PATH_DATA: RegExp;
77
84
  export declare const label: (required?: boolean) => ParamSpec;
@@ -55,6 +55,8 @@ const checkValue = (spec, value, path, out) => {
55
55
  return;
56
56
  }
57
57
  case "color":
58
+ if (spec.currentColor && value === CURRENT_COLOUR)
59
+ return;
58
60
  if (typeof value !== "string")
59
61
  return report(`expected a colour token path, found ${describe(value)}`);
60
62
  if (isColourLiteral(value))
@@ -144,6 +146,10 @@ export const size = (required = false, min) => min === undefined ? { kind: "numb
144
146
  /** A position: any finite number, because a map position has no maximum. */
145
147
  export const pos = () => ({ kind: "number", required: true });
146
148
  export const col = () => ({ kind: "color" });
149
+ /** The paint keyword that follows the text colour of an inline SVG's element. */
150
+ export const CURRENT_COLOUR = "currentColor";
151
+ /** A token path or `currentColor`: for icons written inline in a page. */
152
+ export const colOrCurrent = () => ({ kind: "color", currentColor: true });
147
153
  /** SVG path data: commands, digits, signs, dots, commas and spaces only. */
148
154
  export const PATH_DATA = /^[MmLlHhVvCcSsQqTtAaZz0-9eE.,+\-\s]+$/;
149
155
  export const label = (required = true) => ({ kind: "string", nonEmpty: true, required });
@@ -0,0 +1,21 @@
1
+ import type { SvgNode } from "./node.ts";
2
+ export type UiIcon = {
3
+ readonly name: string;
4
+ readonly title: string;
5
+ readonly description: string;
6
+ readonly nodes: readonly SvgNode[];
7
+ };
8
+ /** Every UI icon draws in a 24x24 box. */
9
+ export declare const UI_ICON_SIZE = 24;
10
+ export declare const UI_ICONS: readonly UiIcon[];
11
+ export declare const UI_ICON_NAMES: string[];
12
+ export type UiIconParams = {
13
+ name: string;
14
+ x: number;
15
+ y: number;
16
+ size?: number;
17
+ colour?: string;
18
+ };
19
+ /** One UI icon at (x, y), `size` wide (24 by default), in one token colour (`color.ink` by default)
20
+ * or `currentColor`, so an icon inline in a page follows its element's text colour. */
21
+ export declare const uiIcon: import("./piece.ts").Piece<UiIconParams>;
@@ -0,0 +1,35 @@
1
+ import { el, g, drawn, r2 } from "./node.js";
2
+ import { CURRENT_COLOUR, colOrCurrent, pos, size } from "./params.js";
3
+ import { definePiece } from "./piece.js";
4
+ import { colour } from "./params.js";
5
+ /** Every UI icon draws in a 24x24 box. */
6
+ export const UI_ICON_SIZE = 24;
7
+ const stroked = (d) => el("path", { d, fill: "none", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round" });
8
+ const filled = (d) => el("path", { d, stroke: "none" });
9
+ export const UI_ICONS = [
10
+ {
11
+ name: "sun",
12
+ title: "Sun",
13
+ description: "Light theme: a disc with eight rays.",
14
+ nodes: [el("circle", { cx: 12, cy: 12, r: 4.5, stroke: "none" }), stroked("M12 2.5v2.5M12 19v2.5M2.5 12H5M19 12h2.5M5.3 5.3l1.8 1.8M16.9 16.9l1.8 1.8M5.3 18.7l1.8-1.8M16.9 7.1l1.8-1.8")],
15
+ },
16
+ { name: "moon", title: "Moon", description: "Dark theme: a crescent moon.", nodes: [filled("M20.5 14.6A8.6 8.6 0 0 1 9.4 3.5a8.6 8.6 0 1 0 11.1 11.1Z")] },
17
+ { name: "zoomIn", title: "Zoom in", description: "A magnifier with a plus.", nodes: [stroked("M10.5 4a6.5 6.5 0 1 0 0 13a6.5 6.5 0 1 0 0-13ZM15.3 15.3l5.2 5.2M7.5 10.5h6M10.5 7.5v6")] },
18
+ { name: "zoomOut", title: "Zoom out", description: "A magnifier with a minus.", nodes: [stroked("M10.5 4a6.5 6.5 0 1 0 0 13a6.5 6.5 0 1 0 0-13ZM15.3 15.3l5.2 5.2M7.5 10.5h6")] },
19
+ { name: "reset", title: "Reset", description: "A circular arrow: back to the start.", nodes: [stroked("M4.5 12A7.5 7.5 0 1 0 6.7 6.7M4 3.5v4.5h4.5")] },
20
+ {
21
+ name: "calendar",
22
+ title: "Calendar",
23
+ description: "A calendar page with one day marked.",
24
+ nodes: [stroked("M5.5 5h13a2 2 0 0 1 2 2v11.5a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2ZM3.5 10h17M8 3v4M16 3v4"), el("rect", { x: 7, y: 13, width: 3, height: 3, rx: 0.5, stroke: "none" })],
25
+ },
26
+ { name: "download", title: "Download", description: "An arrow down into a tray.", nodes: [stroked("M12 3.5v11M7.5 10l4.5 4.5 4.5-4.5M4.5 16.5v3h15v-3")] },
27
+ ];
28
+ export const UI_ICON_NAMES = UI_ICONS.map((icon) => icon.name);
29
+ /** One UI icon at (x, y), `size` wide (24 by default), in one token colour (`color.ink` by default)
30
+ * or `currentColor`, so an icon inline in a page follows its element's text colour. */
31
+ export const uiIcon = definePiece("uiIcon", { name: { kind: "enum", required: true, values: UI_ICON_NAMES }, x: pos(), y: pos(), size: size(), colour: colOrCurrent() }, ({ name, x, y, size: side = UI_ICON_SIZE, colour: path = "color.ink" }) => {
32
+ const icon = UI_ICONS.find((candidate) => candidate.name === name);
33
+ const paint = path === CURRENT_COLOUR ? CURRENT_COLOUR : colour(path, "colour");
34
+ return drawn([g({ transform: `translate(${r2(x)} ${r2(y)}) scale(${side / UI_ICON_SIZE})`, fill: paint, stroke: paint }, ...icon.nodes)]);
35
+ });
@@ -132,18 +132,24 @@ export declare const tokens: {
132
132
  readonly map: {
133
133
  readonly badgeHeight: 20;
134
134
  readonly badgeTextSize: 12;
135
+ readonly bendRadius: 32;
135
136
  readonly casing: 6;
136
137
  readonly countHeight: 16;
137
138
  readonly countTextSize: 11;
138
139
  readonly edgeWidth: 2.5;
140
+ readonly haloWidth: 1.5;
139
141
  readonly interchangeExtra: 2;
140
142
  readonly labelSize: 13;
143
+ readonly licenceSize: 10;
141
144
  readonly lineWidth: 12;
142
145
  readonly nodeHeight: 46;
143
146
  readonly nodeRadius: 10;
144
147
  readonly outline: 3;
148
+ readonly ringWidth: 3;
145
149
  readonly shadow: 3.5;
146
150
  readonly stopRadius: 12;
151
+ readonly zonePadding: 32;
152
+ readonly zoneTitleTint: 0.6;
147
153
  };
148
154
  readonly motion: {
149
155
  readonly dimOpacity: 0.42;
@@ -233,6 +239,7 @@ export declare const tokens: {
233
239
  readonly rimWidth: 1;
234
240
  readonly shadow: "#4C5CB8";
235
241
  readonly text: "#EEF2FF";
242
+ readonly zoneOpacity: 0.22;
236
243
  };
237
244
  readonly light: {
238
245
  readonly background: "#EEF2FF";
@@ -243,6 +250,7 @@ export declare const tokens: {
243
250
  readonly rimWidth: 0;
244
251
  readonly shadow: "#0A1033";
245
252
  readonly text: "#0A1033";
253
+ readonly zoneOpacity: 0.12;
246
254
  };
247
255
  };
248
256
  };
@@ -133,18 +133,24 @@ export const tokens = {
133
133
  "map": {
134
134
  "badgeHeight": 20,
135
135
  "badgeTextSize": 12,
136
+ "bendRadius": 32,
136
137
  "casing": 6,
137
138
  "countHeight": 16,
138
139
  "countTextSize": 11,
139
140
  "edgeWidth": 2.5,
141
+ "haloWidth": 1.5,
140
142
  "interchangeExtra": 2,
141
143
  "labelSize": 13,
144
+ "licenceSize": 10,
142
145
  "lineWidth": 12,
143
146
  "nodeHeight": 46,
144
147
  "nodeRadius": 10,
145
148
  "outline": 3,
149
+ "ringWidth": 3,
146
150
  "shadow": 3.5,
147
- "stopRadius": 12
151
+ "stopRadius": 12,
152
+ "zonePadding": 32,
153
+ "zoneTitleTint": 0.6
148
154
  },
149
155
  "motion": {
150
156
  "dimOpacity": 0.42,
@@ -233,7 +239,8 @@ export const tokens = {
233
239
  "rim": "#EEF2FF",
234
240
  "rimWidth": 1,
235
241
  "shadow": "#4C5CB8",
236
- "text": "#EEF2FF"
242
+ "text": "#EEF2FF",
243
+ "zoneOpacity": 0.22
237
244
  },
238
245
  "light": {
239
246
  "background": "#EEF2FF",
@@ -243,7 +250,8 @@ export const tokens = {
243
250
  "rim": "#EEF2FF",
244
251
  "rimWidth": 0,
245
252
  "shadow": "#0A1033",
246
- "text": "#0A1033"
253
+ "text": "#0A1033",
254
+ "zoneOpacity": 0.12
247
255
  }
248
256
  }
249
257
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunosouzas/design-system",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Design tokens and illustration pieces of brunosouzas.com: tokens as CSS, JSON and TypeScript, pieces that draw SVG (and React), and a cover command.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -78,18 +78,24 @@
78
78
  --ds-illustration-transitional-shade: #E8B830;
79
79
  --ds-map-badge-height: 20px;
80
80
  --ds-map-badge-text-size: 12px;
81
+ --ds-map-bend-radius: 32px;
81
82
  --ds-map-casing: 6px;
82
83
  --ds-map-count-height: 16px;
83
84
  --ds-map-count-text-size: 11px;
84
85
  --ds-map-edge-width: 2.5px;
86
+ --ds-map-halo-width: 1.5px;
85
87
  --ds-map-interchange-extra: 2px;
86
88
  --ds-map-label-size: 13px;
89
+ --ds-map-licence-size: 10px;
87
90
  --ds-map-line-width: 12px;
88
91
  --ds-map-node-height: 46px;
89
92
  --ds-map-node-radius: 10px;
90
93
  --ds-map-outline: 3px;
94
+ --ds-map-ring-width: 3px;
91
95
  --ds-map-shadow: 3.5px;
92
96
  --ds-map-stop-radius: 12px;
97
+ --ds-map-zone-padding: 32px;
98
+ --ds-map-zone-title-tint: 0.6;
93
99
  --ds-motion-dim-opacity: 0.42;
94
100
  --ds-motion-draw-arrow: 20;
95
101
  --ds-motion-fade: 10;
@@ -152,6 +158,7 @@
152
158
  --ds-surface-dark-rim-width: 1px;
153
159
  --ds-surface-dark-shadow: #4C5CB8;
154
160
  --ds-surface-dark-text: #EEF2FF;
161
+ --ds-surface-dark-zone-opacity: 0.22;
155
162
  --ds-surface-light-background: #EEF2FF;
156
163
  --ds-surface-light-grid: #E0E7FB;
157
164
  --ds-surface-light-muted: #5B6694;
@@ -160,4 +167,5 @@
160
167
  --ds-surface-light-rim-width: 0px;
161
168
  --ds-surface-light-shadow: #0A1033;
162
169
  --ds-surface-light-text: #0A1033;
170
+ --ds-surface-light-zone-opacity: 0.12;
163
171
  }
@@ -132,18 +132,24 @@
132
132
  "map": {
133
133
  "badgeHeight": 20,
134
134
  "badgeTextSize": 12,
135
+ "bendRadius": 32,
135
136
  "casing": 6,
136
137
  "countHeight": 16,
137
138
  "countTextSize": 11,
138
139
  "edgeWidth": 2.5,
140
+ "haloWidth": 1.5,
139
141
  "interchangeExtra": 2,
140
142
  "labelSize": 13,
143
+ "licenceSize": 10,
141
144
  "lineWidth": 12,
142
145
  "nodeHeight": 46,
143
146
  "nodeRadius": 10,
144
147
  "outline": 3,
148
+ "ringWidth": 3,
145
149
  "shadow": 3.5,
146
- "stopRadius": 12
150
+ "stopRadius": 12,
151
+ "zonePadding": 32,
152
+ "zoneTitleTint": 0.6
147
153
  },
148
154
  "motion": {
149
155
  "dimOpacity": 0.42,
@@ -232,7 +238,8 @@
232
238
  "rim": "#EEF2FF",
233
239
  "rimWidth": 1,
234
240
  "shadow": "#4C5CB8",
235
- "text": "#EEF2FF"
241
+ "text": "#EEF2FF",
242
+ "zoneOpacity": 0.22
236
243
  },
237
244
  "light": {
238
245
  "background": "#EEF2FF",
@@ -242,7 +249,8 @@
242
249
  "rim": "#EEF2FF",
243
250
  "rimWidth": 0,
244
251
  "shadow": "#0A1033",
245
- "text": "#0A1033"
252
+ "text": "#0A1033",
253
+ "zoneOpacity": 0.12
246
254
  }
247
255
  }
248
256
  }
@@ -134,18 +134,24 @@ export const tokens = {
134
134
  "map": {
135
135
  "badgeHeight": 20,
136
136
  "badgeTextSize": 12,
137
+ "bendRadius": 32,
137
138
  "casing": 6,
138
139
  "countHeight": 16,
139
140
  "countTextSize": 11,
140
141
  "edgeWidth": 2.5,
142
+ "haloWidth": 1.5,
141
143
  "interchangeExtra": 2,
142
144
  "labelSize": 13,
145
+ "licenceSize": 10,
143
146
  "lineWidth": 12,
144
147
  "nodeHeight": 46,
145
148
  "nodeRadius": 10,
146
149
  "outline": 3,
150
+ "ringWidth": 3,
147
151
  "shadow": 3.5,
148
- "stopRadius": 12
152
+ "stopRadius": 12,
153
+ "zonePadding": 32,
154
+ "zoneTitleTint": 0.6
149
155
  },
150
156
  "motion": {
151
157
  "dimOpacity": 0.42,
@@ -234,7 +240,8 @@ export const tokens = {
234
240
  "rim": "#EEF2FF",
235
241
  "rimWidth": 1,
236
242
  "shadow": "#4C5CB8",
237
- "text": "#EEF2FF"
243
+ "text": "#EEF2FF",
244
+ "zoneOpacity": 0.22
238
245
  },
239
246
  "light": {
240
247
  "background": "#EEF2FF",
@@ -244,7 +251,8 @@ export const tokens = {
244
251
  "rim": "#EEF2FF",
245
252
  "rimWidth": 0,
246
253
  "shadow": "#0A1033",
247
- "text": "#0A1033"
254
+ "text": "#0A1033",
255
+ "zoneOpacity": 0.12
248
256
  }
249
257
  }
250
258
  } as const;