@brunosouzas/design-system 0.1.0 → 0.1.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
@@ -130,7 +130,7 @@ 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.
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
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.
135
135
 
136
136
  ### Maps and diagrams
@@ -169,6 +169,7 @@ A cover is a JSON spec kept with its article, outside this repository: drafts ar
169
169
 
170
170
  - `background` names one of the 21 pairs in `coverBackground` (a background and its ring colour, measured from the published covers). Each pair is used by one cover only.
171
171
  - `pieces` are drawn in order; their words go on top.
172
+ - The `rings` centre stays within ±10000 in a cover spec, and a piece that overflows is reported at its entry (`pieces[1]: the drawing overflows`).
172
173
  - `custom` (optional) is the cover's one free-form object, a JSON node tree limited to drawing tags and a fixed list of attributes, with no external references. An object that repeats becomes a piece instead.
173
174
  - The three published covers that proved the pieces are the test fixtures in `test/fixtures/covers/`.
174
175
 
@@ -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, PATH_DATA } from "./params.js";
13
+ import { COLOUR_TOKENS, 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", "background", "rings", "pieces", "custom"];
16
16
  const CUSTOM_TAGS = ["g", "rect", "circle", "ellipse", "line", "polyline", "polygon", "path", "text"];
@@ -153,8 +153,20 @@ export const validateCoverSpec = (spec) => {
153
153
  }
154
154
  if (!isPlainObject(spec.rings))
155
155
  out.push({ path: "rings", message: "missing or not an object (expected cx, cy and an optional radius)" });
156
- else
157
- out.push(...checkParams(rings.schema, spec.rings, "rings").filter((problem) => problem.path !== "rings.color"));
156
+ else {
157
+ const ringProblems = checkParams(rings.schema, spec.rings, "rings").filter((problem) => problem.path !== "rings.color");
158
+ out.push(...ringProblems);
159
+ // The piece takes any finite centre; a cover spec keeps 0.1.0's ±10000, which bounds a banner's ring count.
160
+ for (const axis of ["cx", "cy"]) {
161
+ const value = spec.rings[axis];
162
+ if (ringProblems.some((p) => p.path === `rings.${axis}`) || typeof value !== "number")
163
+ continue;
164
+ if (value < -MAX_SIZE)
165
+ out.push({ path: `rings.${axis}`, message: `expected a number at least -${MAX_SIZE}, found ${value}` });
166
+ else if (value > MAX_SIZE)
167
+ out.push({ path: `rings.${axis}`, message: `expected a number at most ${MAX_SIZE}, found ${value}` });
168
+ }
169
+ }
158
170
  if (isPlainObject(spec.rings) && Object.hasOwn(spec.rings, "color"))
159
171
  out.push({ path: "rings.color", message: "unknown parameter: the ring colour comes from the background" });
160
172
  if (!Array.isArray(spec.pieces))
@@ -186,6 +198,16 @@ export const validateCoverSpec = (spec) => {
186
198
  const reported = new Set(own.map((problem) => problem.path));
187
199
  out.push(...related.map((problem) => ({ path: `${at}.${problem.path}`, message: problem.message })).filter((problem) => !reported.has(problem.path)));
188
200
  }
201
+ // A valid entry is drawn once: an overflow is a spec problem at the entry, not a crash.
202
+ if (own.length === 0 && !(definition.relations?.(params) ?? []).length) {
203
+ try {
204
+ PIECES[piece](params);
205
+ }
206
+ catch (error) {
207
+ if (error instanceof RangeError && error.message.endsWith(": the drawing overflows"))
208
+ out.push({ path: at, message: "the drawing overflows" });
209
+ }
210
+ }
189
211
  });
190
212
  if (spec.custom !== undefined) {
191
213
  if (Array.isArray(spec.custom))
@@ -61,7 +61,7 @@ export declare const assertParams: (piece: string, schema: Schema, params: Recor
61
61
  export declare const num: (extra?: Omit<Extract<ParamSpec, {
62
62
  kind: "number";
63
63
  }>, "kind">) => ParamSpec;
64
- /** Largest size or coordinate magnitude a piece accepts, far above any banner. */
64
+ /** Largest size a piece accepts, far above any banner. Positions and points have no bound. */
65
65
  export declare const MAX_SIZE = 10000;
66
66
  /** Most items in any list parameter (rows, lines, nodes, tiles). */
67
67
  export declare const MAX_ITEMS = 50;
@@ -69,6 +69,7 @@ export declare const MAX_ITEMS = 50;
69
69
  export declare const count: (min: number, max: number) => ParamSpec;
70
70
  /** A size: greater than 0 (or at least `min`, where fixed insets need room), at most MAX_SIZE. */
71
71
  export declare const size: (required?: boolean, min?: number) => ParamSpec;
72
+ /** A position: any finite number, because a map position has no maximum. */
72
73
  export declare const pos: () => ParamSpec;
73
74
  export declare const col: () => ParamSpec;
74
75
  /** SVG path data: commands, digits, signs, dots, commas and spaces only. */
@@ -86,8 +86,9 @@ const checkValue = (spec, value, path, out) => {
86
86
  return report(`expected true or false, found ${describe(value)}`);
87
87
  return;
88
88
  case "point":
89
- if (!Array.isArray(value) || value.length !== 2 || !value.every((n) => typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= MAX_SIZE)) {
90
- return report(`expected a point [x, y] of finite numbers within ±${MAX_SIZE}, found ${describe(value)}`);
89
+ // Any finite number: a map position has no maximum (spec illustration-pieces, "Positions of any size").
90
+ if (!Array.isArray(value) || value.length !== 2 || !value.every((n) => typeof n === "number" && Number.isFinite(n))) {
91
+ return report(`expected a point [x, y] of finite numbers, found ${describe(value)}`);
91
92
  }
92
93
  return;
93
94
  case "array":
@@ -132,7 +133,7 @@ export const assertParams = (piece, schema, params) => {
132
133
  };
133
134
  // Shorthands for schemas.
134
135
  export const num = (extra = {}) => ({ kind: "number", ...extra });
135
- /** Largest size or coordinate magnitude a piece accepts, far above any banner. */
136
+ /** Largest size a piece accepts, far above any banner. Positions and points have no bound. */
136
137
  export const MAX_SIZE = 10000;
137
138
  /** Most items in any list parameter (rows, lines, nodes, tiles). */
138
139
  export const MAX_ITEMS = 50;
@@ -140,7 +141,8 @@ export const MAX_ITEMS = 50;
140
141
  export const count = (min, max) => ({ kind: "number", integer: true, min, max });
141
142
  /** A size: greater than 0 (or at least `min`, where fixed insets need room), at most MAX_SIZE. */
142
143
  export const size = (required = false, min) => min === undefined ? { kind: "number", min: 0, exclusiveMin: true, max: MAX_SIZE, required } : { kind: "number", min, max: MAX_SIZE, required };
143
- export const pos = () => ({ kind: "number", required: true, min: -MAX_SIZE, max: MAX_SIZE });
144
+ /** A position: any finite number, because a map position has no maximum. */
145
+ export const pos = () => ({ kind: "number", required: true });
144
146
  export const col = () => ({ kind: "color" });
145
147
  /** SVG path data: commands, digits, signs, dots, commas and spaces only. */
146
148
  export const PATH_DATA = /^[MmLlHhVvCcSsQqTtAaZz0-9eE.,+\-\s]+$/;
@@ -1,11 +1,28 @@
1
1
  import { assertParams } from "./params.js";
2
+ // A number the piece computed and would draw: finite, or the drawing overflows
3
+ // (spec illustration-pieces, "Drawing overflow"). Strings are the piece's own
4
+ // path data and transforms; caller path data (road.d, pipe.d) has its own check
5
+ // and only ever holds path characters, never the words Infinity or NaN.
6
+ const NON_FINITE = /Infinity|NaN/;
7
+ const overflows = (node) => {
8
+ if (typeof node === "string")
9
+ return false;
10
+ for (const value of Object.values(node.attrs)) {
11
+ if (typeof value === "number" ? !Number.isFinite(value) : NON_FINITE.test(value))
12
+ return true;
13
+ }
14
+ return node.children.some(overflows);
15
+ };
2
16
  export const definePiece = (name, schema, draw, relations) => {
3
17
  const piece = (params) => {
4
18
  assertParams(name, schema, params);
5
19
  const problems = relations?.(params) ?? [];
6
20
  if (problems.length > 0)
7
21
  throw new RangeError(`${name}: ${problems[0].path}: ${problems[0].message}`);
8
- return draw(params);
22
+ const drawn = draw(params);
23
+ if ([...drawn.body, ...drawn.text].some(overflows))
24
+ throw new RangeError(`${name}: the drawing overflows`);
25
+ return drawn;
9
26
  };
10
27
  return Object.assign(piece, { pieceName: name, schema, relations });
11
28
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunosouzas/design-system",
3
- "version": "0.1.0",
3
+ "version": "0.1.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": {