@brunosouzas/design-system 0.5.0 → 0.5.2
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 +7 -5
- package/dist/src/pieces/cast.d.ts +3 -0
- package/dist/src/pieces/cast.js +60 -7
- package/dist/src/pieces/diagram.js +1 -9
- package/dist/src/pieces/ink.d.ts +5 -0
- package/dist/src/pieces/ink.js +10 -0
- package/dist/src/pieces/library.d.ts +4 -2
- package/dist/src/pieces/library.js +13 -5
- package/dist/src/pieces/metro.d.ts +4 -0
- package/dist/src/pieces/metro.js +46 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,13 +136,14 @@ The components take every colour and size from the tokens, and what is visible a
|
|
|
136
136
|
|
|
137
137
|
### Maps and diagrams
|
|
138
138
|
|
|
139
|
-
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, straight or through two `controls` points, with a head and an optional label; `kind` `reply`, `failure` or `failFast` dashes it and paints it as ADR 0003 says), `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
|
+
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, straight or through two `controls` points, with a head and an optional label; `kind` `reply`, `failure` or `failFast` dashes it and paints it as ADR 0003 says; `heads: "start"` or `"both"` puts heads at `from`, `dashed` overrides the kind's dash), `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.
|
|
140
140
|
|
|
141
141
|
- 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).
|
|
142
142
|
- The licence's disc is small, so it has its own room rule: `size` at least three outlines (9 at ink scale 1).
|
|
143
143
|
|
|
144
144
|
- `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).
|
|
145
145
|
- `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.
|
|
146
|
+
- `countBadge` takes `textSize` (6 to 48, default 11): without `height`, the height, width and baseline grow with it.
|
|
146
147
|
- 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.
|
|
147
148
|
- `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`).
|
|
148
149
|
- 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.
|
|
@@ -159,14 +160,15 @@ const badge = lineBadge({ x: 250, y: 40, text: "SH", fill: "#7B3FE4", surface: "
|
|
|
159
160
|
Ten pieces draw what the blog's body diagrams need beyond the map pieces: `diagramBox` (a box or pill with a fill and centred lines), `decisionNode` (diamond), `storeNode` (cylinder), `queueNode` (box with two rails), `exchangeNode` (hexagon), `taggedContainer` (a frame with its name on a tab), `lifeline` (dashed, or sparse dots once nothing calls it), `phaseBand` (a pill across a sequence in its state's colour), `altFrame` (alt/else frame with a tag and condition per part) and `edgeLabel` (words next to an arrow, optionally on a backdrop). They take `surface` and, the stickers, `inkScale`, like the map pieces, and paint what the caller lays out.
|
|
160
161
|
|
|
161
162
|
- The shapes take one to three centred lines (`text`), an optional `subtitle` and `textSize`. Without `fill` the face is the surface's panel; with a fill, every word is ink.
|
|
162
|
-
- Backticks mark code: `` "adopts a `correlationId`" `` draws `correlationId` in the monospace stack, as a `tspan`. An odd number of backticks is drawn as is.
|
|
163
|
+
- Backticks mark code: `` "adopts a `correlationId`" `` draws `correlationId` in the monospace stack, as a `tspan`. An odd number of backticks is drawn as is. The cover `pill` follows the same rule (`` "`main`" ``).
|
|
164
|
+
- `milestonePin` takes `size`, the head's radius (6 to 48, default 18); the head, its outline and the label scale with it.
|
|
163
165
|
- Widths the piece estimates (`tagWidth`, `conditionWidth`, a label's `width`) can be passed by a caller that measures its text.
|
|
164
166
|
- A body diagram is 800 wide. Lay it out in 560 units and show it 800 wide: the pieces then come out at the sizes of the published diagrams (text about 18, outline about 4); only the hard shadow is shallower (5 instead of 8). The cover command does this for you with `"format": "diagram"` (below).
|
|
165
167
|
- `stateStart({ cx, cy, r, surface })` draws the filled start dot of a state machine (7.7 units, 11 at 800), in the surface's text colour, with no outline and no shadow.
|
|
166
168
|
|
|
167
169
|
### The cast and the emotes
|
|
168
170
|
|
|
169
|
-
`mascot({ who, x, y, face, action, scale, look, headTilt, prop })` draws one character standing with the centre of its ground line at `(x, y)`. Each one carries its identity in three layers, so no video has to introduce it: a body shape, a mark always visible, and a prop when its hands are free.
|
|
171
|
+
`mascot({ who, x, y, face, action, scale, look, headTilt, prop, surface })` draws one character standing with the centre of its ground line at `(x, y)`. Each one carries its identity in three layers, so no video has to introduce it: a body shape, a mark always visible, and a prop when its hands are free.
|
|
170
172
|
|
|
171
173
|
| `who` | Body | Mark | Prop | Colour |
|
|
172
174
|
|---|---|---|---|---|
|
|
@@ -182,7 +184,7 @@ Ten pieces draw what the blog's body diagrams need beyond the map pieces: `diagr
|
|
|
182
184
|
- `action`: `none` (the default: an expression has no hands or feet), `rest`, `wave`, `hold`, `reach`, `up`, `shrug`, `type`. Arms and legs start at the body's sides; hands are mittens.
|
|
183
185
|
- `look` (-1 to 1) moves the gaze, `headTilt` (-30 to 30) turns the agent's head on its neck, `prop: false` hides the prop. Every part is a group named by `data-part` (`body`, `face`, `eyes`, `brows`, `mouth`, `arm-left`, `arm-right`, `legs`, `prop`, `mark`, …), so a scene can animate it.
|
|
184
186
|
|
|
185
|
-
`emote({ kind, x, y, scale, burst })` draws a reaction beside a head, never over the face. Each kind keeps one meaning in every video, like the colours:
|
|
187
|
+
`emote({ kind, x, y, scale, burst, surface })` draws a reaction beside a head, never over the face. Each kind keeps one meaning in every video, like the colours:
|
|
186
188
|
|
|
187
189
|
| `kind` | Shape | Meaning |
|
|
188
190
|
|---|---|---|
|
|
@@ -197,7 +199,7 @@ Ten pieces draw what the blog's body diagrams need beyond the map pieces: `diagr
|
|
|
197
199
|
|
|
198
200
|
In a scene, an emote pops in on the frame the face changes (`emotePop` in `src/brand/cast-motion.ts`, with `leave`, `blinking`, `breathe`, `squashAfter` and `hop`), one at a time, and the body acts through `pose`. A mascot's colour is its identity and never changes with state. The approved motion test is the Studio composition `cast-motion-test` (folder `cast`): `npx remotion render src/index.ts cast-motion-test`. It is a proof of the cast, not an article scene, so `npm run stills` and `npm run render` do not list it.
|
|
199
201
|
|
|
200
|
-
|
|
202
|
+
On a dark page pass `surface: "dark"` (AB#187, 0.5.2): the hard shadows take `surface.dark.shadow`, the ground takes `surface.dark.grid`, and a light rim (`surface.dark.rim`) lines the whole outline, limbs and props included; faces are unchanged. Without `surface`, the cast draws as in 0.5.0.
|
|
201
203
|
|
|
202
204
|
### A cover
|
|
203
205
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SvgNode } from "./node.ts";
|
|
2
|
+
import { type SurfaceName } from "./ink.ts";
|
|
2
3
|
export declare const MASCOTS: readonly ["dev", "agent", "architect", "devops", "data", "request", "message"];
|
|
3
4
|
export declare const MASCOT_FACES: readonly ["normal", "blink", "happy", "wink", "surprised", "worried", "thinking", "focused"];
|
|
4
5
|
export declare const MASCOT_ACTIONS: readonly ["none", "rest", "wave", "hold", "reach", "up", "shrug", "type"];
|
|
@@ -18,6 +19,7 @@ export type MascotParams = {
|
|
|
18
19
|
look?: number;
|
|
19
20
|
headTilt?: number;
|
|
20
21
|
prop?: boolean;
|
|
22
|
+
surface?: SurfaceName;
|
|
21
23
|
};
|
|
22
24
|
export type EmoteParams = {
|
|
23
25
|
kind: EmoteKind;
|
|
@@ -25,6 +27,7 @@ export type EmoteParams = {
|
|
|
25
27
|
y: number;
|
|
26
28
|
scale?: number;
|
|
27
29
|
burst?: boolean;
|
|
30
|
+
surface?: SurfaceName;
|
|
28
31
|
};
|
|
29
32
|
/** One character standing with the centre of its ground line at (x, y). */
|
|
30
33
|
export declare const mascot: import("./piece.ts").Piece<MascotParams>;
|
package/dist/src/pieces/cast.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// (~/.hermes/spikes/design-system/cast-and-emotes/cast.mts). The frozen fixture
|
|
11
11
|
// test/fixtures/cast-0.5.0.json pins every call's output, so a refactor here
|
|
12
12
|
// must keep every hash.
|
|
13
|
+
import { tokens } from "../../tokens/generated/tokens.js";
|
|
13
14
|
import { drawn, el, g } from "./node.js";
|
|
15
|
+
import { surfaceParam } from "./ink.js";
|
|
14
16
|
import { colour, pos } from "./params.js";
|
|
15
17
|
import { definePiece } from "./piece.js";
|
|
16
18
|
const c = (path) => colour(path);
|
|
@@ -20,10 +22,59 @@ export const MASCOT_ACTIONS = ["none", "rest", "wave", "hold", "reach", "up", "s
|
|
|
20
22
|
export const EMOTE_KINDS = ["idea", "doubt", "problem", "worked", "refused", "pressure", "liked", "waiting"];
|
|
21
23
|
const ink = () => c("color.ink");
|
|
22
24
|
const O = () => ({ stroke: ink(), "stroke-width": 4, "stroke-linejoin": "round" });
|
|
23
|
-
|
|
25
|
+
// Every hard shadow is made by `hard`, so the dark drawing finds it by identity, not by its look.
|
|
26
|
+
const SHADOWS = new WeakSet();
|
|
27
|
+
const hard = (n) => (SHADOWS.add(n), n);
|
|
28
|
+
const shade = (d, dx = 8) => hard(el("path", { d, fill: ink(), transform: `translate(${dx} ${dx})` }));
|
|
24
29
|
const solid = (d, fill, dx = 8) => [shade(d, dx), el("path", { d, fill, ...O() })];
|
|
25
30
|
const shine = (d, col) => el("path", { d, fill: "none", stroke: col, "stroke-width": 9, "stroke-linecap": "round" });
|
|
26
31
|
const rr = (x, y, w, h, r) => `M${x + r},${y} H${x + w - r} Q${x + w},${y} ${x + w},${y + r} V${y + h - r} Q${x + w},${y + h} ${x + w - r},${y + h} H${x + r} Q${x},${y + h} ${x},${y + h - r} V${y + r} Q${x},${y} ${x + r},${y} Z`;
|
|
32
|
+
// ---------------------------------------------------------------- dark surface (card 187)
|
|
33
|
+
// On the dark surface (ADR 0003 amendment) the light drawing is rewritten, so the
|
|
34
|
+
// light output stays the frozen 0.5.0 tree: each hard shadow (made by `hard`) takes the surface's
|
|
35
|
+
// shadow colour, the ground takes the surface's grid colour, and a light rim
|
|
36
|
+
// (the drawing in the rim colour, every stroke widened by 2 × 2 × rimWidth) sits
|
|
37
|
+
// behind the whole outline, arms and legs included. The face is unchanged.
|
|
38
|
+
const walk = (n, f) => {
|
|
39
|
+
const m = f(n);
|
|
40
|
+
if (!m)
|
|
41
|
+
return null;
|
|
42
|
+
const kids = m.children.map((k) => (typeof k === "string" ? k : walk(k, f))).filter((k) => k !== null);
|
|
43
|
+
return { ...m, children: kids };
|
|
44
|
+
};
|
|
45
|
+
const part = (n) => String(n.attrs["data-part"] ?? "");
|
|
46
|
+
const onDark = (n) => {
|
|
47
|
+
const d = tokens.surface.dark;
|
|
48
|
+
return walk(n, (x) => {
|
|
49
|
+
if (SHADOWS.has(x))
|
|
50
|
+
return { ...x, attrs: { ...x.attrs, fill: d.shadow } };
|
|
51
|
+
if (part(x) === "ground")
|
|
52
|
+
return { ...x, children: x.children.map((k) => (typeof k === "string" ? k : { ...k, attrs: { ...k.attrs, fill: d.grid } })) };
|
|
53
|
+
return x;
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const rimOf = (n) => {
|
|
57
|
+
const d = tokens.surface.dark;
|
|
58
|
+
return walk(n, (x) => {
|
|
59
|
+
if (SHADOWS.has(x) || ["ground", "face"].includes(part(x)) || x.attrs.opacity !== undefined)
|
|
60
|
+
return null;
|
|
61
|
+
if (x.tag === "g")
|
|
62
|
+
return x;
|
|
63
|
+
const sw = x.attrs.stroke !== undefined && x.attrs.stroke !== "none" ? Number(x.attrs["stroke-width"] ?? 4) : 0;
|
|
64
|
+
const attrs = { ...x.attrs, stroke: d.rim, "stroke-width": sw + 4 * d.rimWidth, "stroke-linejoin": "round" };
|
|
65
|
+
if (x.attrs.fill !== undefined && x.attrs.fill !== "none")
|
|
66
|
+
attrs.fill = d.rim;
|
|
67
|
+
return { ...x, attrs };
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
/** The light drawing redrawn for the dark surface: ground, rim, then the drawing. */
|
|
71
|
+
const darken = (root) => {
|
|
72
|
+
// The rim is taken from the light tree, where shadows are still known by identity.
|
|
73
|
+
const isGround = (k) => typeof k === "object" && part(k) === "ground";
|
|
74
|
+
const rim = rimOf({ ...root, attrs: {}, children: root.children.filter((k) => !isGround(k)) });
|
|
75
|
+
const dark = onDark(root);
|
|
76
|
+
return { ...dark, children: [...dark.children.filter(isGround), ...(rim ? [g({ "data-part": "rim" }, ...rim.children)] : []), ...dark.children.filter((k) => !isGround(k))] };
|
|
77
|
+
};
|
|
27
78
|
// ---------------------------------------------------------------- face (macs.ts)
|
|
28
79
|
export const face = (cx, cy, f, col, mouthFill) => {
|
|
29
80
|
const k = 1, dx = 24 * k;
|
|
@@ -206,7 +257,7 @@ const drawMascot = (p) => {
|
|
|
206
257
|
parts = [...parts, g({ "data-part": "head", transform: `rotate(${tilt} ${top[0]} ${top[1]})` }, ...solid(rr(top[0] - 96, 0, 192, 132, 22), c("color.white")), el("path", { d: rr(top[0] - 80, 14, 160, 104, 12), fill: c("illustration.codePanel"), stroke: ink(), "stroke-width": 4 }), faceWrapped)];
|
|
207
258
|
}
|
|
208
259
|
const root = g({ "data-mascot": p.who, transform: `translate(${p.x} ${p.y}) scale(${k}) translate(${-ax} ${-ay})`, stroke: "none" }, g({ "data-part": "ground" }, el("ellipse", { cx: ax, cy: ay, rx: ch.ground, ry: 9, fill: c("cast.ground") })), ...(front ? [] : arms), legs, ...parts, ch.faceInHead ? null : faceWrapped, showProp ? g({ "data-part": "prop" }, ...ch.prop()) : null, ...(front ? arms : []));
|
|
209
|
-
return drawn([root]);
|
|
260
|
+
return drawn([p.surface === "dark" ? darken(root) : root]);
|
|
210
261
|
};
|
|
211
262
|
// ---------------------------------------------------------------- emotes (emotes.ts)
|
|
212
263
|
const rays = (cx, cy, r1, r2, n, from = -150, to = -30) => Array.from({ length: n }, (_, i) => {
|
|
@@ -215,7 +266,7 @@ const rays = (cx, cy, r1, r2, n, from = -150, to = -30) => Array.from({ length:
|
|
|
215
266
|
return el("path", { d: `M${r(cx + r1 * Math.cos(a))},${r(cy + r1 * Math.sin(a))} L${r(cx + r2 * Math.cos(a))},${r(cy + r2 * Math.sin(a))}`, stroke: ink(), "stroke-width": 6, "stroke-linecap": "round" });
|
|
216
267
|
});
|
|
217
268
|
const EO = () => ({ stroke: ink(), "stroke-width": 5, "stroke-linejoin": "round", "stroke-linecap": "round" });
|
|
218
|
-
const disc = (cx, cy, r, fill) => [el("circle", { cx: cx + 6, cy: cy + 6, r, fill: ink() }), el("circle", { cx, cy, r, fill, ...EO() })];
|
|
269
|
+
const disc = (cx, cy, r, fill) => [hard(el("circle", { cx: cx + 6, cy: cy + 6, r, fill: ink() })), el("circle", { cx, cy, r, fill, ...EO() })];
|
|
219
270
|
const zed = (x, y, s) => {
|
|
220
271
|
// A Z of height s with its top-left at (x, y - s): ink casing, caller fill.
|
|
221
272
|
const w = s * 0.72, d = `M${x},${y - s} H${x + w} L${x},${y} H${x + w}`;
|
|
@@ -230,7 +281,7 @@ const drawEmote = (p) => {
|
|
|
230
281
|
if (burst)
|
|
231
282
|
parts.push(el("circle", { cx, cy: cy - 6, r: 62, fill: c("color.transitional"), opacity: 0.35 }), ...rays(cx, cy - 6, 50, 72, 7, -180, 0));
|
|
232
283
|
const bulb = `M${cx - 30},${cy - 10} A34,34 0 1 1 ${cx + 30},${cy - 10} Q${cx + 22},${cy + 6} ${cx + 18},${cy + 22} H${cx - 18} Q${cx - 22},${cy + 6} ${cx - 30},${cy - 10} Z`;
|
|
233
|
-
parts.push(el("path", { d: bulb, fill: ink(), transform: "translate(6 6)" }), el("path", { d: bulb, fill: c("color.transitional"), ...EO() }), el("path", { d: `M${cx - 16},${cy - 22} Q${cx - 12},${cy - 32} ${cx - 2},${cy - 34}`, fill: "none", stroke: c("color.white"), "stroke-width": 6, "stroke-linecap": "round" }), el("path", { d: `M${cx - 8},${cy + 22} V${cy} L${cx},${cy + 8} L${cx + 8},${cy} V${cy + 22}`, fill: "none", stroke: ink(), "stroke-width": 4, "stroke-linejoin": "round" }), el("rect", { x: cx - 18, y: cy + 22, width: 36, height: 12, rx: 3, fill: c("cast.metal"), ...EO(), "stroke-width": 4 }), el("rect", { x: cx - 14, y: cy + 34, width: 28, height: 10, rx: 5, fill: c("cast.metal"), ...EO(), "stroke-width": 4 }));
|
|
284
|
+
parts.push(hard(el("path", { d: bulb, fill: ink(), transform: "translate(6 6)" })), el("path", { d: bulb, fill: c("color.transitional"), ...EO() }), el("path", { d: `M${cx - 16},${cy - 22} Q${cx - 12},${cy - 32} ${cx - 2},${cy - 34}`, fill: "none", stroke: c("color.white"), "stroke-width": 6, "stroke-linecap": "round" }), el("path", { d: `M${cx - 8},${cy + 22} V${cy} L${cx},${cy + 8} L${cx + 8},${cy} V${cy + 22}`, fill: "none", stroke: ink(), "stroke-width": 4, "stroke-linejoin": "round" }), el("rect", { x: cx - 18, y: cy + 22, width: 36, height: 12, rx: 3, fill: c("cast.metal"), ...EO(), "stroke-width": 4 }), el("rect", { x: cx - 14, y: cy + 34, width: 28, height: 10, rx: 5, fill: c("cast.metal"), ...EO(), "stroke-width": 4 }));
|
|
234
285
|
break;
|
|
235
286
|
}
|
|
236
287
|
case "doubt":
|
|
@@ -239,7 +290,7 @@ const drawEmote = (p) => {
|
|
|
239
290
|
case "problem":
|
|
240
291
|
if (burst)
|
|
241
292
|
parts.push(...rays(cx, cy, 48, 70, 5));
|
|
242
|
-
parts.push(el("path", { d: `M${cx},${cy - 44} L${cx + 46},${cy + 34} H${cx - 46} Z`, fill: ink(), transform: "translate(6 6)" }), el("path", { d: `M${cx},${cy - 44} L${cx + 46},${cy + 34} H${cx - 46} Z`, fill: c("color.failure"), ...EO() }), el("path", { d: `M${cx},${cy - 18} V${cy + 8}`, stroke: ink(), "stroke-width": 9, "stroke-linecap": "round" }), el("circle", { cx, cy: cy + 22, r: 5.5, fill: ink() }));
|
|
293
|
+
parts.push(hard(el("path", { d: `M${cx},${cy - 44} L${cx + 46},${cy + 34} H${cx - 46} Z`, fill: ink(), transform: "translate(6 6)" })), el("path", { d: `M${cx},${cy - 44} L${cx + 46},${cy + 34} H${cx - 46} Z`, fill: c("color.failure"), ...EO() }), el("path", { d: `M${cx},${cy - 18} V${cy + 8}`, stroke: ink(), "stroke-width": 9, "stroke-linecap": "round" }), el("circle", { cx, cy: cy + 22, r: 5.5, fill: ink() }));
|
|
243
294
|
break;
|
|
244
295
|
case "pressure":
|
|
245
296
|
for (const [dx, dy, k] of [[0, 0, 1], [30, 22, 0.6]])
|
|
@@ -261,7 +312,8 @@ const drawEmote = (p) => {
|
|
|
261
312
|
break;
|
|
262
313
|
}
|
|
263
314
|
const attrs = s === 1 ? { "data-emote": kind, stroke: "none" } : { "data-emote": kind, stroke: "none", transform: `translate(${cx} ${cy}) scale(${s}) translate(${-cx} ${-cy})` };
|
|
264
|
-
|
|
315
|
+
const root = g(attrs, ...parts);
|
|
316
|
+
return drawn([p.surface === "dark" ? darken(root) : root]);
|
|
265
317
|
};
|
|
266
318
|
const unit = { kind: "number", min: 0, exclusiveMin: true, max: 10 };
|
|
267
319
|
/** One character standing with the centre of its ground line at (x, y). */
|
|
@@ -275,6 +327,7 @@ export const mascot = definePiece("mascot", {
|
|
|
275
327
|
look: { kind: "number", min: -1, max: 1 },
|
|
276
328
|
headTilt: { kind: "number", min: -30, max: 30 },
|
|
277
329
|
prop: { kind: "boolean" },
|
|
330
|
+
surface: surfaceParam,
|
|
278
331
|
}, drawMascot, ({ who, headTilt }) => (headTilt !== undefined && headTilt !== 0 && who !== "agent" ? [{ path: "headTilt", message: "applies only to the agent" }] : []));
|
|
279
332
|
/** One reaction symbol with a fixed meaning, centred on (x, y), beside a head. */
|
|
280
|
-
export const emote = definePiece("emote", { kind: { kind: "enum", required: true, values: EMOTE_KINDS }, x: pos(), y: pos(), scale: unit, burst: { kind: "boolean" } }, drawEmote);
|
|
333
|
+
export const emote = definePiece("emote", { kind: { kind: "enum", required: true, values: EMOTE_KINDS }, x: pos(), y: pos(), scale: unit, burst: { kind: "boolean" }, surface: surfaceParam }, drawEmote);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// between parameters are checked in the order of design §3 and the first one is
|
|
8
8
|
// thrown as a RangeError naming the parameter.
|
|
9
9
|
import { tokens } from "../../tokens/generated/tokens.js";
|
|
10
|
-
import { inkOf, inkSchema, limits, roomFor, sticker, surfaceParam } from "./ink.js";
|
|
10
|
+
import { inkOf, inkSchema, limits, roomFor, spans, sticker, surfaceParam } from "./ink.js";
|
|
11
11
|
import { drawn, el, r2 } from "./node.js";
|
|
12
12
|
import { col, colour, label, MAX_SIZE, pos, size } from "./params.js";
|
|
13
13
|
import { definePiece } from "./piece.js";
|
|
@@ -20,14 +20,6 @@ const textSize = { kind: "number", min: 6, max: 48 };
|
|
|
20
20
|
// ---------- words ----------
|
|
21
21
|
/** Characters of a word for a width estimate: backticks do not count. */
|
|
22
22
|
const characters = (value) => Array.from(value.replace(/`/g, "")).length;
|
|
23
|
-
/** Backticks mark code (design §4): with an even number of them, each part between backticks becomes a tspan in
|
|
24
|
-
* the monospace stack; empty parts are dropped. With an odd number, the string is drawn as is. */
|
|
25
|
-
const spans = (value) => {
|
|
26
|
-
const parts = value.split("`");
|
|
27
|
-
if (parts.length % 2 === 0)
|
|
28
|
-
return [value];
|
|
29
|
-
return parts.flatMap((part, i) => (part === "" ? [] : i % 2 === 1 ? [el("tspan", { "font-family": tokens.font.mono }, part)] : [part]));
|
|
30
|
-
};
|
|
31
23
|
// Every text sets its own anchor, size and weight, so a surrounding group (a cover's label style) does not leak in.
|
|
32
24
|
const words = (x, y, value, attrs) => el("text", { x: r2(x), y: r2(y), "font-family": font, ...attrs }, ...spans(value));
|
|
33
25
|
/** No fill: the surface's panel with its text; a fill: that token with ink words (ADR 0003). */
|
package/dist/src/pieces/ink.d.ts
CHANGED
|
@@ -45,4 +45,9 @@ export declare const atLeast: (name: string, value: number, least: number, what:
|
|
|
45
45
|
export type Shape = (dx: number, dy: number, attrs: Attrs) => SvgNode;
|
|
46
46
|
/** The ink sticker (metro-and-diagram-pieces design §3): shadow copy, rim on dark, then the face. */
|
|
47
47
|
export declare const sticker: (shape: Shape, face: string, inkParams: Ink) => SvgNode[];
|
|
48
|
+
/** Backticks mark code (body-diagram-pieces design §4; the pill since card 190): with an even number of them, each part between backticks becomes a tspan in
|
|
49
|
+
* the monospace stack; empty parts are dropped. With an odd number, the string is drawn as is. */
|
|
50
|
+
export declare const spans: (value: string) => (SvgNode | string)[];
|
|
51
|
+
/** A pill's text length for its default width: backticks of code runs do not count; otherwise `length` as in 0.5.0. */
|
|
52
|
+
export declare const codeLength: (value: string) => number;
|
|
48
53
|
export declare const words: (x: number, y: number, value: string, attrs: Attrs) => SvgNode;
|
package/dist/src/pieces/ink.js
CHANGED
|
@@ -34,5 +34,15 @@ export const sticker = (shape, face, inkParams) => {
|
|
|
34
34
|
shape(0, 0, { fill: face, stroke: ink, "stroke-width": r2(outline), "stroke-linejoin": "round" }),
|
|
35
35
|
];
|
|
36
36
|
};
|
|
37
|
+
/** Backticks mark code (body-diagram-pieces design §4; the pill since card 190): with an even number of them, each part between backticks becomes a tspan in
|
|
38
|
+
* the monospace stack; empty parts are dropped. With an odd number, the string is drawn as is. */
|
|
39
|
+
export const spans = (value) => {
|
|
40
|
+
const parts = value.split("`");
|
|
41
|
+
if (parts.length % 2 === 0)
|
|
42
|
+
return [value];
|
|
43
|
+
return parts.flatMap((part, i) => (part === "" ? [] : i % 2 === 1 ? [el("tspan", { "font-family": tokens.font.mono }, part)] : [part]));
|
|
44
|
+
};
|
|
45
|
+
/** A pill's text length for its default width: backticks of code runs do not count; otherwise `length` as in 0.5.0. */
|
|
46
|
+
export const codeLength = (value) => (value.split("`").length % 2 === 1 && value.includes("`") ? value.replace(/`/g, "") : value).length;
|
|
37
47
|
// Every text sets its own anchor, size and weight, so a surrounding group (a cover's label style) does not leak in.
|
|
38
48
|
export const words = (x, y, value, attrs) => el("text", { x: r2(x), y: r2(y), "font-family": font, ...attrs }, value);
|
|
@@ -12,7 +12,7 @@ export type PillParams = {
|
|
|
12
12
|
fill?: string;
|
|
13
13
|
width?: number;
|
|
14
14
|
};
|
|
15
|
-
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. */
|
|
15
|
+
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. Backticks mark code (card 190). */
|
|
16
16
|
export declare const pill: import("./piece.ts").Piece<PillParams>;
|
|
17
17
|
export declare const RING_COUNT = 14;
|
|
18
18
|
/** Most rings one call draws: enough for a banner's farthest corner from any centre a cover spec accepts (548). */
|
|
@@ -140,8 +140,10 @@ export type MilestonePinParams = {
|
|
|
140
140
|
text: string;
|
|
141
141
|
fill?: string;
|
|
142
142
|
stem?: number;
|
|
143
|
+
size?: number;
|
|
143
144
|
};
|
|
144
|
-
/** A pin standing on (x, y): a stem and a round head with a short label.
|
|
145
|
+
/** A pin standing on (x, y): a stem and a round head with a short label. `size` is the head's radius (18) and
|
|
146
|
+
* scales the head, its outline and the label together (card 190). */
|
|
145
147
|
export declare const milestonePin: import("./piece.ts").Piece<MilestonePinParams>;
|
|
146
148
|
export type PriceTagParams = {
|
|
147
149
|
x: number;
|
|
@@ -16,6 +16,7 @@ import { DIAGRAM_PIECES, stateStart } from "./diagram.js";
|
|
|
16
16
|
import { uiIcon } from "./ui-icons.js";
|
|
17
17
|
import { linkIcon } from "./link-icons.js";
|
|
18
18
|
import { emote, mascot } from "./cast.js";
|
|
19
|
+
import { codeLength, spans } from "./ink.js";
|
|
19
20
|
export { emote, mascot };
|
|
20
21
|
export * from "./metro.js";
|
|
21
22
|
export * from "./diagram.js";
|
|
@@ -23,11 +24,11 @@ export * from "./ui-icons.js";
|
|
|
23
24
|
export * from "./link-icons.js";
|
|
24
25
|
const ink = tokens.color.ink;
|
|
25
26
|
const mono = tokens.font.mono;
|
|
26
|
-
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. */
|
|
27
|
+
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. Backticks mark code (card 190). */
|
|
27
28
|
export const pill = definePiece("pill", { x: pos(), y: pos(), text: label(), fill: col(), width: size() }, ({ x, y, text, fill = "color.white", width }) => {
|
|
28
29
|
const h = tokens.cover.pillHeight;
|
|
29
|
-
const w = width ?? Math.max(68, Math.round(text
|
|
30
|
-
return drawn([el("rect", { x: x - w / 2, y: y - h / 2, width: w, height: h, rx: h / 2, fill: colour(fill, "fill") })], [el("text", { x, y: y + 7 }, text)]);
|
|
30
|
+
const w = width ?? Math.max(68, Math.round(codeLength(text) * 10.5 + 34));
|
|
31
|
+
return drawn([el("rect", { x: x - w / 2, y: y - h / 2, width: w, height: h, rx: h / 2, fill: colour(fill, "fill") })], [el("text", { x, y: y + 7 }, ...spans(text))]);
|
|
31
32
|
});
|
|
32
33
|
// ---------- background ----------
|
|
33
34
|
export const RING_COUNT = 14;
|
|
@@ -194,8 +195,15 @@ export const road = definePiece("road", { d: { kind: "string", required: true, n
|
|
|
194
195
|
el("path", { d, fill: "none", stroke: tokens.illustration.road, "stroke-width": 40, "stroke-linecap": "round" }),
|
|
195
196
|
el("path", { d, fill: "none", stroke: tokens.color.white, "stroke-width": tokens.outline.block, "stroke-dasharray": "14 12" }),
|
|
196
197
|
]));
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
const PIN_SIZE = 18;
|
|
199
|
+
/** A pin standing on (x, y): a stem and a round head with a short label. `size` is the head's radius (18) and
|
|
200
|
+
* scales the head, its outline and the label together (card 190). */
|
|
201
|
+
export const milestonePin = definePiece("milestonePin", { x: pos(), y: pos(), text: label(), fill: col(), stem: size(), size: { kind: "number", min: 6, max: 48 } }, ({ x, y, text, fill = "color.healthy", stem = 44, size: r }) => {
|
|
202
|
+
// Without `size`, the 0.5.0 expressions exactly (no rounding), so every 0.5.0 call keeps its bytes.
|
|
203
|
+
const k = (r ?? PIN_SIZE) / PIN_SIZE;
|
|
204
|
+
const at = r === undefined ? (v) => v : r2;
|
|
205
|
+
return drawn([el("line", { x1: x, y1: y, x2: x, y2: y - stem }), el("circle", { cx: x, cy: at(y - stem - 14 * k), r: r ?? PIN_SIZE, fill: colour(fill, "fill"), "stroke-width": at(tokens.outline.small * k) })], [el("text", { x, y: at(y - stem - 7 * k), "font-size": r ?? PIN_SIZE, "font-weight": tokens.font.weightHeavy }, text)]);
|
|
206
|
+
});
|
|
199
207
|
/** Price or version tag pointing left from its tip (x, y), with a hole and an optional string. */
|
|
200
208
|
export const priceTag = definePiece("priceTag", { x: pos(), y: pos(), text: label(), width: size(), fill: col(), stringFrom: { kind: "point" } }, ({ x, y, text, width: w = 128, fill = "color.white", stringFrom }) => {
|
|
201
209
|
const h = 48;
|
|
@@ -34,6 +34,7 @@ export type CountBadgeParams = Ink & {
|
|
|
34
34
|
text: string;
|
|
35
35
|
width?: number;
|
|
36
36
|
height?: number;
|
|
37
|
+
textSize?: number;
|
|
37
38
|
};
|
|
38
39
|
/** A count (rules of a skill): a circle when as wide as high, a pill otherwise, in the surface's panel colour. */
|
|
39
40
|
export declare const countBadge: import("./piece.ts").Piece<CountBadgeParams>;
|
|
@@ -56,12 +57,15 @@ export declare const pillNode: import("./piece.ts").Piece<NodeParams>;
|
|
|
56
57
|
/** A diagram hexagon (an agent). */
|
|
57
58
|
export declare const hexNode: import("./piece.ts").Piece<NodeParams>;
|
|
58
59
|
export type ArrowKind = "message" | "reply" | "failure" | "failFast";
|
|
60
|
+
export type ArrowHeads = "end" | "start" | "both";
|
|
59
61
|
export type MessageArrowParams = {
|
|
60
62
|
from: Point;
|
|
61
63
|
to: Point;
|
|
62
64
|
curve?: "bezier" | "straight";
|
|
63
65
|
controls?: [Point, Point];
|
|
64
66
|
head?: boolean;
|
|
67
|
+
heads?: ArrowHeads;
|
|
68
|
+
dashed?: boolean;
|
|
65
69
|
label?: string;
|
|
66
70
|
width?: number;
|
|
67
71
|
kind?: ArrowKind;
|
package/dist/src/pieces/metro.js
CHANGED
|
@@ -70,20 +70,30 @@ export const pipe = definePiece("pipe", { d: PIPE_PATH, fill: dataColour(), widt
|
|
|
70
70
|
el("path", { ...line, stroke: paint(fill), "stroke-width": width }),
|
|
71
71
|
]);
|
|
72
72
|
});
|
|
73
|
-
|
|
73
|
+
// `textSize` scales the default height, the per-character width and the baseline with it (card 190);
|
|
74
|
+
// without it, k is 1 and every expression is the 0.5.0 one.
|
|
75
|
+
const countScale = ({ textSize }) => (textSize ?? M.countTextSize) / M.countTextSize;
|
|
76
|
+
const countHeight = (params) => params.height ?? (params.textSize === undefined ? M.countHeight : r2(M.countHeight * countScale(params)));
|
|
77
|
+
const countWidth = (params) => {
|
|
78
|
+
const { width, text } = params;
|
|
79
|
+
const height = countHeight(params);
|
|
80
|
+
const k = countScale(params);
|
|
74
81
|
const characters = Array.from(text).length;
|
|
75
|
-
return width ?? (characters === 1 ? height : Math.max(height, 6.4 * characters + 8));
|
|
82
|
+
return width ?? (characters === 1 ? height : Math.max(height, k === 1 ? 6.4 * characters + 8 : r2(k * (6.4 * characters + 8))));
|
|
76
83
|
};
|
|
77
84
|
/** A count (rules of a skill): a circle when as wide as high, a pill otherwise, in the surface's panel colour. */
|
|
78
|
-
export const countBadge = definePiece("countBadge", { cx: pos(), cy: pos(), text: label(), width: size(), height: size(), ...inkSchema }, (params) => {
|
|
79
|
-
const { cx, cy, text,
|
|
85
|
+
export const countBadge = definePiece("countBadge", { cx: pos(), cy: pos(), text: label(), width: size(), height: size(), textSize: { kind: "number", min: 6, max: 48 }, ...inkSchema }, (params) => {
|
|
86
|
+
const { cx, cy, text, textSize = M.countTextSize } = params;
|
|
87
|
+
const height = countHeight(params);
|
|
80
88
|
const width = countWidth(params);
|
|
81
89
|
const { s } = inkOf(params);
|
|
82
90
|
const shape = width === height ? (dx, dy, attrs) => el("circle", { cx: r2(cx + dx), cy: r2(cy + dy), r: r2(height / 2), ...attrs }) : pill(cx - width / 2, cy - height / 2, width, height);
|
|
83
|
-
return drawn(sticker(shape, s.panel, params), [words(cx, cy + 4, text, { fill: s.text, "font-size":
|
|
91
|
+
return drawn(sticker(shape, s.panel, params), [words(cx, cy + 4 * countScale(params), text, { fill: s.text, "font-size": textSize, "font-weight": bold, "text-anchor": "middle" })]);
|
|
84
92
|
}, (params) => {
|
|
85
|
-
const height = params
|
|
86
|
-
|
|
93
|
+
const height = countHeight(params);
|
|
94
|
+
// A height derived from textSize is reported at textSize, the parameter the caller passed (card 190).
|
|
95
|
+
const name = params.height === undefined && params.textSize !== undefined ? "textSize" : "height";
|
|
96
|
+
return limits(atLeast("width", countWidth(params), height, "the height"), roomFor(name, height, params));
|
|
87
97
|
});
|
|
88
98
|
const nodeSchema = {
|
|
89
99
|
x: pos(),
|
|
@@ -147,11 +157,13 @@ export const messageArrow = definePiece("messageArrow", {
|
|
|
147
157
|
curve: { kind: "enum", values: ["bezier", "straight"] },
|
|
148
158
|
controls: { kind: "array", minItems: 2, maxItems: 2, of: point() },
|
|
149
159
|
head: { kind: "boolean" },
|
|
160
|
+
heads: { kind: "enum", values: ["end", "start", "both"] },
|
|
161
|
+
dashed: { kind: "boolean" },
|
|
150
162
|
label: label(false),
|
|
151
163
|
width: { kind: "number", min: 1, max: 12 },
|
|
152
164
|
kind: { kind: "enum", values: ["message", "reply", "failure", "failFast"] },
|
|
153
165
|
surface: surfaceParam,
|
|
154
|
-
}, ({ from, to, curve = "bezier", controls, head = true, label: text, width = M.edgeWidth, kind = "message", surface = "light" }) => {
|
|
166
|
+
}, ({ from, to, curve = "bezier", controls, head = true, heads = "end", dashed, label: text, width = M.edgeWidth, kind = "message", surface = "light" }) => {
|
|
155
167
|
const s = tokens.surface[surface];
|
|
156
168
|
const paint_ = kind === "failure" ? tokens.color.failure : kind === "failFast" ? tokens.color.failFast : s.text;
|
|
157
169
|
const length = 5 * width;
|
|
@@ -166,21 +178,35 @@ export const messageArrow = definePiece("messageArrow", {
|
|
|
166
178
|
};
|
|
167
179
|
const tail = controls && [controls[1], controls[0], from].find((q) => q[0] !== to[0] || q[1] !== to[1]);
|
|
168
180
|
const direction = tail !== undefined ? towards(tail) : bent ? [Math.sign(dx), 0] : [dx / distance, (to[1] - from[1]) / distance];
|
|
169
|
-
const
|
|
181
|
+
const atEnd = head && heads !== "start";
|
|
182
|
+
const atStart = head && heads !== "end";
|
|
183
|
+
const end = atEnd ? [to[0] - direction[0] * length, to[1] - direction[1] * length] : to;
|
|
184
|
+
// A head at `from` points back at it, from the first of c1, c2, to that differs from `from` (card 190).
|
|
185
|
+
const away = (q) => {
|
|
186
|
+
const l = Math.hypot(from[0] - q[0], from[1] - q[1]);
|
|
187
|
+
return [(from[0] - q[0]) / l, (from[1] - q[1]) / l];
|
|
188
|
+
};
|
|
189
|
+
const lead = controls && [controls[0], controls[1], to].find((q) => q[0] !== from[0] || q[1] !== from[1]);
|
|
190
|
+
const back = lead !== undefined ? away(lead) : bent ? [-Math.sign(dx), 0] : [-direction[0], -direction[1]];
|
|
191
|
+
const start = atStart ? [from[0] - back[0] * length, from[1] - back[1] * length] : from;
|
|
170
192
|
const mx = (from[0] + to[0]) / 2;
|
|
171
193
|
const at = (p) => `${r2(p[0])},${r2(p[1])}`;
|
|
172
194
|
const d = controls
|
|
173
|
-
? `M${at(
|
|
195
|
+
? `M${at(start)} C${at(controls[0])} ${at(controls[1])} ${at(end)}`
|
|
174
196
|
: bent
|
|
175
|
-
? `M${at(
|
|
176
|
-
: `M${at(
|
|
177
|
-
const dash = kind
|
|
197
|
+
? `M${at(start)} C${r2(mx)},${r2(from[1])} ${r2(mx)},${r2(to[1])} ${at(end)}`
|
|
198
|
+
: `M${at(start)} L${at(end)}`;
|
|
199
|
+
const dash = !(dashed ?? kind !== "message") ? {} : { "stroke-dasharray": `${r2(2.4 * width)} ${r2(3.2 * width)}` };
|
|
178
200
|
const body = [el("path", { d, fill: "none", stroke: paint_, "stroke-width": r2(width), "stroke-linecap": "round", ...dash })];
|
|
179
|
-
|
|
180
|
-
const normal = [-
|
|
181
|
-
const points = [
|
|
201
|
+
const arrowhead = (tip, base, along) => {
|
|
202
|
+
const normal = [-along[1] * 2 * width, along[0] * 2 * width];
|
|
203
|
+
const points = [tip, [base[0] + normal[0], base[1] + normal[1]], [base[0] - normal[0], base[1] - normal[1]]];
|
|
182
204
|
body.push(el("polygon", { points: points.map(at).join(" "), fill: paint_, stroke: paint_, "stroke-width": 1, "stroke-linejoin": "round" }));
|
|
183
|
-
}
|
|
205
|
+
};
|
|
206
|
+
if (atEnd)
|
|
207
|
+
arrowhead(to, end, direction);
|
|
208
|
+
if (atStart)
|
|
209
|
+
arrowhead(from, start, back);
|
|
184
210
|
const words_ = [];
|
|
185
211
|
if (text !== undefined) {
|
|
186
212
|
const middle = [mx, (from[1] + to[1]) / 2];
|
|
@@ -189,9 +215,10 @@ export const messageArrow = definePiece("messageArrow", {
|
|
|
189
215
|
words_.push(words(middle[0], middle[1] + 4, text, { fill: s.text, "font-size": 11, "font-weight": 400, "text-anchor": "middle" }));
|
|
190
216
|
}
|
|
191
217
|
return drawn(body, words_);
|
|
192
|
-
}, ({ from, to, head = true, width = M.edgeWidth, controls, curve }) => {
|
|
218
|
+
}, ({ from, to, head = true, heads, width = M.edgeWidth, controls, curve }) => {
|
|
193
219
|
const distance = Math.hypot(to[0] - from[0], to[1] - from[1]);
|
|
194
|
-
|
|
220
|
+
const need = (heads === "both" ? 20 : 10) * width;
|
|
221
|
+
return limits(head === false && heads !== undefined && { path: "heads", message: "cannot be combined with head: false" }, distance === 0 && { path: "to", message: "must differ from from" }, distance > 0 && head && distance < need && { path: "to", message: `is too close to from for the ${heads === "both" ? "heads" : "head"}: needs at least ${r2(need)}, found ${r2(distance)}` }, controls !== undefined && curve !== undefined && { path: "controls", message: "cannot be combined with curve" });
|
|
195
222
|
});
|
|
196
223
|
/** stackledger's mark (favicon, header): an ink square with a bent line and two stations, 32 units scaled to `size`.
|
|
197
224
|
* On a surface with a rim (dark), a light rim of 2 × rimWidth lines the square inside its 32 box (card 167). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brunosouzas/design-system",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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": {
|