@brunosouzas/design-system 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/src/pieces/cover.d.ts +7 -1
- package/dist/src/pieces/cover.js +51 -7
- package/dist/src/pieces/index.d.ts +1 -1
- package/dist/src/pieces/index.js +1 -1
- package/dist/src/pieces/library.d.ts +4 -1
- package/dist/src/pieces/library.js +5 -3
- package/dist/src/pieces/params.d.ts +2 -1
- package/dist/src/pieces/params.js +6 -4
- package/dist/src/pieces/piece.js +18 -1
- package/dist/tokens/generated/tokens.d.ts +5 -0
- package/dist/tokens/generated/tokens.js +5 -0
- package/package.json +1 -1
- package/tokens/generated/tokens.css +3 -0
- package/tokens/generated/tokens.json +5 -0
- package/tokens/generated/tokens.ts +5 -0
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ npm run stills -- publish-subscribe # 24 phase/final PNGs (six per
|
|
|
90
90
|
npm run stills -- publish-subscribe --dry-run # print 24 commands, no effects
|
|
91
91
|
npm run render -- publish-subscribe --dry-run # print eight commands; real video waits for owner stills approval
|
|
92
92
|
npm run build # compile the package into dist/ (the package test needs it)
|
|
93
|
-
npm run cover -- cover.json --out card.svg # build a blog cover from its spec
|
|
93
|
+
npm run cover -- cover.json --out card.svg # build a blog cover (or a banner) from its spec
|
|
94
94
|
npm run pieces:sheet -- --out out/pieces.svg # every piece once, labelled, for review
|
|
95
95
|
npm run studio # Remotion Studio, for frame-by-frame review
|
|
96
96
|
```
|
|
@@ -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
|
|
@@ -151,7 +151,7 @@ const badge = lineBadge({ x: 250, y: 40, text: "SH", fill: "#7B3FE4", surface: "
|
|
|
151
151
|
|
|
152
152
|
### A cover
|
|
153
153
|
|
|
154
|
-
A cover is a JSON spec kept with its article, outside this repository: drafts are private, and only the pieces are public. `npm run cover -- <spec.json> --out <file.svg>` validates it, reports every problem with its JSON path, and writes an 800x420 SVG; the same spec always gives the same bytes.
|
|
154
|
+
A cover is a JSON spec kept with its article, outside this repository: drafts are private, and only the pieces are public. `npm run cover -- <spec.json> --out <file.svg>` validates it, reports every problem with its JSON path, and writes an 800x420 SVG; the same spec always gives the same bytes. With `"format": "banner"` the same command writes a site banner instead (see below).
|
|
155
155
|
|
|
156
156
|
```json
|
|
157
157
|
{
|
|
@@ -169,9 +169,19 @@ 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
|
|
|
176
|
+
### Banners
|
|
177
|
+
|
|
178
|
+
A spec with `"format": "banner"` (the default is `"cover"`) builds a home banner for a site: `banner.width` x `banner.height` (1200x420) with corners rounded at `banner.radius` (20), the measures of brunosouzas.com's published banner. Same pieces, validation and command. Two things differ from a cover:
|
|
179
|
+
|
|
180
|
+
- the rings go on until they reach the frame's farthest corner (22 rings for a centre at 600, 225 with radius 60), where a cover draws 14;
|
|
181
|
+
- pieces and labels are clipped to the rounded frame too, so nothing pokes past a corner.
|
|
182
|
+
|
|
183
|
+
The banner is one image that scales down as a whole. Words in it become small on a phone (a 19px label is about 6px at 375px wide), so keep them few or none. `test/fixtures/covers/home-banner.json` is the brunosouzas.com home banner redrawn from the pieces.
|
|
184
|
+
|
|
175
185
|
### A new piece
|
|
176
186
|
|
|
177
187
|
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.
|
|
@@ -2,6 +2,9 @@ import { type PieceName } from "./library.ts";
|
|
|
2
2
|
import { type SvgNode } from "./node.ts";
|
|
3
3
|
import { type Problem } from "./params.ts";
|
|
4
4
|
export declare const SAFE_ID: RegExp;
|
|
5
|
+
/** Cover (800x420, the default) or banner (1200x420, rounded corners): spec cover-cli, "Banner output". */
|
|
6
|
+
export declare const FORMATS: readonly ["cover", "banner"];
|
|
7
|
+
export type CoverFormat = (typeof FORMATS)[number];
|
|
5
8
|
export type CoverPiece = {
|
|
6
9
|
piece: PieceName;
|
|
7
10
|
} & Record<string, unknown>;
|
|
@@ -14,6 +17,7 @@ export type CoverSpec = {
|
|
|
14
17
|
id: string;
|
|
15
18
|
title: string;
|
|
16
19
|
description: string;
|
|
20
|
+
format?: CoverFormat;
|
|
17
21
|
background: string;
|
|
18
22
|
rings: {
|
|
19
23
|
cx: number;
|
|
@@ -28,5 +32,7 @@ export declare const backgroundNames: () => string[];
|
|
|
28
32
|
export declare const comparePaths: (a: string, b: string) => number;
|
|
29
33
|
/** Every problem of a parsed spec, sorted by path; empty when the spec is valid. */
|
|
30
34
|
export declare const validateCoverSpec: (spec: unknown) => Problem[];
|
|
31
|
-
/**
|
|
35
|
+
/** Rings a banner draws: out to the frame's farthest corner (spec cover-cli, "Banner rings reach the corners"). */
|
|
36
|
+
export declare const bannerRingCount: (cx: number, cy: number, radius?: number) => number;
|
|
37
|
+
/** The cover's (or banner's) node tree. The spec must be valid (validateCoverSpec returned no problem). */
|
|
32
38
|
export declare const coverTree: (spec: CoverSpec) => SvgNode;
|
package/dist/src/pieces/cover.js
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
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
|
-
const TOP_LEVEL = ["id", "title", "description", "background", "rings", "pieces", "custom"];
|
|
15
|
+
const TOP_LEVEL = ["id", "title", "description", "format", "background", "rings", "pieces", "custom"];
|
|
16
|
+
/** Cover (800x420, the default) or banner (1200x420, rounded corners): spec cover-cli, "Banner output". */
|
|
17
|
+
export const FORMATS = ["cover", "banner"];
|
|
16
18
|
const CUSTOM_TAGS = ["g", "rect", "circle", "ellipse", "line", "polyline", "polygon", "path", "text"];
|
|
17
19
|
// Attributes a free-form object may use (design §6): geometry, paint, text and transform.
|
|
18
20
|
const CUSTOM_ATTRS = new Set([
|
|
@@ -153,10 +155,27 @@ export const validateCoverSpec = (spec) => {
|
|
|
153
155
|
}
|
|
154
156
|
if (!isPlainObject(spec.rings))
|
|
155
157
|
out.push({ path: "rings", message: "missing or not an object (expected cx, cy and an optional radius)" });
|
|
156
|
-
else
|
|
157
|
-
|
|
158
|
+
else {
|
|
159
|
+
const ringProblems = checkParams(rings.schema, spec.rings, "rings").filter((problem) => problem.path !== "rings.color" && problem.path !== "rings.count");
|
|
160
|
+
out.push(...ringProblems);
|
|
161
|
+
// The piece takes any finite centre; a cover spec keeps 0.1.0's ±10000, which bounds a banner's ring count.
|
|
162
|
+
for (const axis of ["cx", "cy"]) {
|
|
163
|
+
const value = spec.rings[axis];
|
|
164
|
+
if (ringProblems.some((p) => p.path === `rings.${axis}`) || typeof value !== "number")
|
|
165
|
+
continue;
|
|
166
|
+
if (value < -MAX_SIZE)
|
|
167
|
+
out.push({ path: `rings.${axis}`, message: `expected a number at least -${MAX_SIZE}, found ${value}` });
|
|
168
|
+
else if (value > MAX_SIZE)
|
|
169
|
+
out.push({ path: `rings.${axis}`, message: `expected a number at most ${MAX_SIZE}, found ${value}` });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
158
172
|
if (isPlainObject(spec.rings) && Object.hasOwn(spec.rings, "color"))
|
|
159
173
|
out.push({ path: "rings.color", message: "unknown parameter: the ring colour comes from the background" });
|
|
174
|
+
if (isPlainObject(spec.rings) && Object.hasOwn(spec.rings, "count"))
|
|
175
|
+
out.push({ path: "rings.count", message: "unknown parameter: the ring count comes from the format" });
|
|
176
|
+
if (spec.format !== undefined && (typeof spec.format !== "string" || !FORMATS.includes(spec.format))) {
|
|
177
|
+
out.push({ path: "format", message: `expected one of ${FORMATS.join(", ")}` });
|
|
178
|
+
}
|
|
160
179
|
if (!Array.isArray(spec.pieces))
|
|
161
180
|
out.push({ path: "pieces", message: "missing or not an array" });
|
|
162
181
|
else
|
|
@@ -186,6 +205,16 @@ export const validateCoverSpec = (spec) => {
|
|
|
186
205
|
const reported = new Set(own.map((problem) => problem.path));
|
|
187
206
|
out.push(...related.map((problem) => ({ path: `${at}.${problem.path}`, message: problem.message })).filter((problem) => !reported.has(problem.path)));
|
|
188
207
|
}
|
|
208
|
+
// A valid entry is drawn once: an overflow is a spec problem at the entry, not a crash.
|
|
209
|
+
if (own.length === 0 && !(definition.relations?.(params) ?? []).length) {
|
|
210
|
+
try {
|
|
211
|
+
PIECES[piece](params);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error instanceof RangeError && error.message.endsWith(": the drawing overflows"))
|
|
215
|
+
out.push({ path: at, message: "the drawing overflows" });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
189
218
|
});
|
|
190
219
|
if (spec.custom !== undefined) {
|
|
191
220
|
if (Array.isArray(spec.custom))
|
|
@@ -232,12 +261,25 @@ const resolveCustom = (node) => {
|
|
|
232
261
|
attrs.stroke = "none";
|
|
233
262
|
return el(node.tag, attrs, ...(node.children ?? []).map((child) => (typeof child === "string" ? child : resolveCustom(child))));
|
|
234
263
|
};
|
|
235
|
-
/**
|
|
264
|
+
/** Rings a banner draws: out to the frame's farthest corner (spec cover-cli, "Banner rings reach the corners"). */
|
|
265
|
+
export const bannerRingCount = (cx, cy, radius = tokens.cover.ringRadius) => {
|
|
266
|
+
const { width, height } = tokens.banner;
|
|
267
|
+
const far = Math.max(...[[0, 0], [width, 0], [0, height], [width, height]].map(([x, y]) => Math.hypot(x - cx, y - cy)));
|
|
268
|
+
return Math.max(1, Math.ceil((far - radius) / tokens.cover.ringStep) + 1);
|
|
269
|
+
};
|
|
270
|
+
/** The cover's (or banner's) node tree. The spec must be valid (validateCoverSpec returned no problem). */
|
|
236
271
|
export const coverTree = (spec) => {
|
|
237
|
-
const
|
|
272
|
+
const banner = spec.format === "banner";
|
|
273
|
+
const { width, height } = banner ? tokens.banner : tokens.cover;
|
|
274
|
+
// A banner's corners are rounded; a cover keeps today's square frame, attribute for attribute.
|
|
275
|
+
const corner = banner ? { rx: tokens.banner.radius } : {};
|
|
238
276
|
const pair = tokens.coverBackground[spec.background];
|
|
239
277
|
const drawnPieces = spec.pieces.map(({ piece, ...params }) => PIECES[piece](params));
|
|
240
278
|
const ringPath = `coverBackground.${spec.background}.ring`;
|
|
279
|
+
const ringCount = banner ? { count: bannerRingCount(spec.rings.cx, spec.rings.cy, spec.rings.radius) } : {};
|
|
280
|
+
const frame = `url(#${spec.id}-frame)`;
|
|
281
|
+
const outlined = g({ stroke: tokens.color.ink, "stroke-width": tokens.outline.block, "stroke-linejoin": "round" }, ...drawnPieces.flatMap((d) => d.body), spec.custom ? resolveCustom(spec.custom) : null);
|
|
282
|
+
const labels = g({ "font-size": tokens.cover.labelSize, "font-weight": tokens.cover.labelWeight, fill: tokens.color.ink, "text-anchor": "middle" }, ...drawnPieces.flatMap((d) => d.text));
|
|
241
283
|
return el("svg", {
|
|
242
284
|
xmlns: "http://www.w3.org/2000/svg",
|
|
243
285
|
width,
|
|
@@ -246,5 +288,7 @@ export const coverTree = (spec) => {
|
|
|
246
288
|
role: "img",
|
|
247
289
|
"aria-labelledby": `${spec.id}-title ${spec.id}-desc`,
|
|
248
290
|
"font-family": tokens.font.sans,
|
|
249
|
-
}, el("title", { id: `${spec.id}-title` }, spec.title), el("desc", { id: `${spec.id}-desc` }, spec.description), el("defs", {}, el("clipPath", { id: `${spec.id}-frame` }, el("rect", { width, height }))), el("rect", { width, height, fill: pair.fill }), g({ "clip-path":
|
|
291
|
+
}, el("title", { id: `${spec.id}-title` }, spec.title), el("desc", { id: `${spec.id}-desc` }, spec.description), el("defs", {}, el("clipPath", { id: `${spec.id}-frame` }, el("rect", { width, height, ...corner }))), el("rect", { width, height, ...corner, fill: pair.fill }), g({ "clip-path": frame }, ...rings({ ...spec.rings, color: ringPath, ...ringCount }).body),
|
|
292
|
+
// On a banner, pieces and labels are clipped too, so nothing pokes past a rounded corner.
|
|
293
|
+
...(banner ? [g({ "clip-path": frame }, outlined, labels)] : [outlined, labels]));
|
|
250
294
|
};
|
|
@@ -5,5 +5,5 @@ export { COLOUR_TOKENS, DATA_COLOUR, MAX_ITEMS, paint, MAX_SIZE, checkParams, co
|
|
|
5
5
|
export { definePiece, type Piece } from "./piece.ts";
|
|
6
6
|
export * from "./library.ts";
|
|
7
7
|
export { EIP_ICONS, EIP_ICON_NAMES, EIP_ICON_HEIGHT, EIP_ICON_WIDTH, type EipIcon } from "./eip-icons.ts";
|
|
8
|
-
export { SAFE_ID, backgroundNames, comparePaths, coverTree, validateCoverSpec, type CoverPiece, type CoverSpec, type CustomNode } from "./cover.ts";
|
|
8
|
+
export { FORMATS, SAFE_ID, backgroundNames, bannerRingCount, comparePaths, coverTree, validateCoverSpec, type CoverFormat, type CoverPiece, type CoverSpec, type CustomNode } from "./cover.ts";
|
|
9
9
|
export { tokens, type Tokens } from "../../tokens/generated/tokens.ts";
|
package/dist/src/pieces/index.js
CHANGED
|
@@ -10,5 +10,5 @@ export { COLOUR_TOKENS, DATA_COLOUR, MAX_ITEMS, paint, MAX_SIZE, checkParams, co
|
|
|
10
10
|
export { definePiece } from "./piece.js";
|
|
11
11
|
export * from "./library.js";
|
|
12
12
|
export { EIP_ICONS, EIP_ICON_NAMES, EIP_ICON_HEIGHT, EIP_ICON_WIDTH } from "./eip-icons.js";
|
|
13
|
-
export { SAFE_ID, backgroundNames, comparePaths, coverTree, validateCoverSpec } from "./cover.js";
|
|
13
|
+
export { FORMATS, SAFE_ID, backgroundNames, bannerRingCount, comparePaths, coverTree, validateCoverSpec } from "./cover.js";
|
|
14
14
|
export { tokens } from "../../tokens/generated/tokens.js";
|
|
@@ -10,13 +10,16 @@ export type PillParams = {
|
|
|
10
10
|
/** Pill label centred on (x, y), usually overlapping the top edge of what it names. */
|
|
11
11
|
export declare const pill: import("./piece.ts").Piece<PillParams>;
|
|
12
12
|
export declare const RING_COUNT = 14;
|
|
13
|
+
/** Most rings one call draws: enough for a banner's farthest corner from any centre a cover spec accepts (548). */
|
|
14
|
+
export declare const MAX_RINGS = 1000;
|
|
13
15
|
export type RingsParams = {
|
|
14
16
|
cx: number;
|
|
15
17
|
cy: number;
|
|
16
18
|
radius?: number;
|
|
17
19
|
color?: string;
|
|
20
|
+
count?: number;
|
|
18
21
|
};
|
|
19
|
-
/** Concentric rings around (cx, cy): 14
|
|
22
|
+
/** Concentric rings around (cx, cy): `count` rings (14 by default), step and width from the cover tokens. */
|
|
20
23
|
export declare const rings: import("./piece.ts").Piece<RingsParams>;
|
|
21
24
|
export type SheetRow = {
|
|
22
25
|
lead?: "none" | "checkbox" | "checked" | "question";
|
|
@@ -23,9 +23,11 @@ export const pill = definePiece("pill", { x: pos(), y: pos(), text: label(), fil
|
|
|
23
23
|
});
|
|
24
24
|
// ---------- background ----------
|
|
25
25
|
export const RING_COUNT = 14;
|
|
26
|
-
/**
|
|
27
|
-
export const
|
|
28
|
-
|
|
26
|
+
/** Most rings one call draws: enough for a banner's farthest corner from any centre a cover spec accepts (548). */
|
|
27
|
+
export const MAX_RINGS = 1000;
|
|
28
|
+
/** Concentric rings around (cx, cy): `count` rings (14 by default), step and width from the cover tokens. */
|
|
29
|
+
export const rings = definePiece("rings", { cx: pos(), cy: pos(), radius: size(), color: col(), count: count(1, MAX_RINGS) }, ({ cx, cy, radius = tokens.cover.ringRadius, color = "color.ring", count: rings = RING_COUNT }) => drawn([
|
|
30
|
+
g({ fill: "none", stroke: colour(color, "color"), "stroke-width": tokens.cover.ringWidth }, ...Array.from({ length: rings }, (_, i) => el("circle", { cx, cy, r: radius + i * tokens.cover.ringStep }))),
|
|
29
31
|
]));
|
|
30
32
|
const rowSchema = {
|
|
31
33
|
lead: { kind: "enum", values: ["none", "checkbox", "checked", "question"] },
|
|
@@ -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
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
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]+$/;
|
package/dist/src/pieces/piece.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/* Generated from tokens/source.json by scripts/tokens.ts (npm run tokens). Do not edit by hand. */
|
|
2
2
|
:root {
|
|
3
|
+
--ds-banner-height: 420px;
|
|
4
|
+
--ds-banner-radius: 20px;
|
|
5
|
+
--ds-banner-width: 1200px;
|
|
3
6
|
--ds-color-backend: #FF6B6B;
|
|
4
7
|
--ds-color-blocked: #FF8A3D;
|
|
5
8
|
--ds-color-caller: #8FB2FF;
|