@brunosouzas/design-system 0.1.1 → 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 +11 -2
- package/dist/src/pieces/cover.d.ts +7 -1
- package/dist/src/pieces/cover.js +27 -5
- 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/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
|
```
|
|
@@ -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
|
{
|
|
@@ -173,6 +173,15 @@ A cover is a JSON spec kept with its article, outside this repository: drafts ar
|
|
|
173
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.
|
|
174
174
|
- The three published covers that proved the pieces are the test fixtures in `test/fixtures/covers/`.
|
|
175
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
|
+
|
|
176
185
|
### A new piece
|
|
177
186
|
|
|
178
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
|
@@ -12,7 +12,9 @@ import { PIECE_NAMES, PIECES, rings } from "./library.js";
|
|
|
12
12
|
import { el, g } from "./node.js";
|
|
13
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([
|
|
@@ -154,7 +156,7 @@ export const validateCoverSpec = (spec) => {
|
|
|
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
158
|
else {
|
|
157
|
-
const ringProblems = checkParams(rings.schema, spec.rings, "rings").filter((problem) => problem.path !== "rings.color");
|
|
159
|
+
const ringProblems = checkParams(rings.schema, spec.rings, "rings").filter((problem) => problem.path !== "rings.color" && problem.path !== "rings.count");
|
|
158
160
|
out.push(...ringProblems);
|
|
159
161
|
// The piece takes any finite centre; a cover spec keeps 0.1.0's ±10000, which bounds a banner's ring count.
|
|
160
162
|
for (const axis of ["cx", "cy"]) {
|
|
@@ -169,6 +171,11 @@ export const validateCoverSpec = (spec) => {
|
|
|
169
171
|
}
|
|
170
172
|
if (isPlainObject(spec.rings) && Object.hasOwn(spec.rings, "color"))
|
|
171
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
|
+
}
|
|
172
179
|
if (!Array.isArray(spec.pieces))
|
|
173
180
|
out.push({ path: "pieces", message: "missing or not an array" });
|
|
174
181
|
else
|
|
@@ -254,12 +261,25 @@ const resolveCustom = (node) => {
|
|
|
254
261
|
attrs.stroke = "none";
|
|
255
262
|
return el(node.tag, attrs, ...(node.children ?? []).map((child) => (typeof child === "string" ? child : resolveCustom(child))));
|
|
256
263
|
};
|
|
257
|
-
/**
|
|
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). */
|
|
258
271
|
export const coverTree = (spec) => {
|
|
259
|
-
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 } : {};
|
|
260
276
|
const pair = tokens.coverBackground[spec.background];
|
|
261
277
|
const drawnPieces = spec.pieces.map(({ piece, ...params }) => PIECES[piece](params));
|
|
262
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));
|
|
263
283
|
return el("svg", {
|
|
264
284
|
xmlns: "http://www.w3.org/2000/svg",
|
|
265
285
|
width,
|
|
@@ -268,5 +288,7 @@ export const coverTree = (spec) => {
|
|
|
268
288
|
role: "img",
|
|
269
289
|
"aria-labelledby": `${spec.id}-title ${spec.id}-desc`,
|
|
270
290
|
"font-family": tokens.font.sans,
|
|
271
|
-
}, 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]));
|
|
272
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"] },
|
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;
|