sheleg-design-skill 1.12.0 → 1.13.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +6 -5
  3. package/bin/cli.js +4 -3
  4. package/cursor/rules/sheleg-design.mdc +4 -1
  5. package/kits/scoreboard/.design-sync/config.json +14 -0
  6. package/kits/scoreboard/.design-sync/conventions.md +59 -0
  7. package/kits/scoreboard/README.md +16 -0
  8. package/kits/scoreboard/package.json +29 -0
  9. package/kits/scoreboard/src/Button.md +5 -0
  10. package/kits/scoreboard/src/Button.tsx +33 -0
  11. package/kits/scoreboard/src/Card.md +5 -0
  12. package/kits/scoreboard/src/Card.tsx +24 -0
  13. package/kits/scoreboard/src/Chip.md +5 -0
  14. package/kits/scoreboard/src/Chip.tsx +25 -0
  15. package/kits/scoreboard/src/Heading.md +5 -0
  16. package/kits/scoreboard/src/Heading.tsx +19 -0
  17. package/kits/scoreboard/src/Ledger.md +5 -0
  18. package/kits/scoreboard/src/Ledger.tsx +45 -0
  19. package/kits/scoreboard/src/LedgerRow.md +5 -0
  20. package/kits/scoreboard/src/LedgerRow.tsx +28 -0
  21. package/kits/scoreboard/src/Rule.md +5 -0
  22. package/kits/scoreboard/src/Rule.tsx +18 -0
  23. package/kits/scoreboard/src/Stat.md +5 -0
  24. package/kits/scoreboard/src/Stat.tsx +17 -0
  25. package/kits/scoreboard/src/StatusChip.md +5 -0
  26. package/kits/scoreboard/src/StatusChip.tsx +27 -0
  27. package/kits/scoreboard/src/TickHeading.md +5 -0
  28. package/kits/scoreboard/src/TickHeading.tsx +23 -0
  29. package/kits/scoreboard/src/index.ts +23 -0
  30. package/kits/scoreboard/src/styles.css +332 -0
  31. package/kits/scoreboard/tsconfig.json +15 -0
  32. package/package.json +2 -2
  33. package/plugins/sheleg-design/.claude-plugin/plugin.json +2 -2
  34. package/plugins/sheleg-design/commands/sheleg-design.md +2 -2
  35. package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +1 -1
  36. package/plugins/sheleg-design/skills/sheleg-design/MOBILE_SURFACES.md +2 -2
  37. package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +5 -4
  38. package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +2 -2
  39. package/plugins/sheleg-design/skills/sheleg-design/styles/field-notes.md +9 -0
  40. package/plugins/sheleg-design/skills/sheleg-design/styles/scoreboard.md +355 -0
  41. package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/scoreboard.css +229 -0
  42. package/plugins/sheleg-design/skills/sheleg-design/styles/workbench.md +7 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,71 @@ All notable changes to this project are documented in this file. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
5
5
  follow [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.13.0] - 2026-08-12
8
+
9
+ A thirteenth style pack, and it is the first one in the library whose accent is
10
+ forbidden from carrying a word.
11
+
12
+ ### Added
13
+
14
+ - **`scoreboard`** — the thirteenth pack, extracted from
15
+ <https://www.get-ryze.ai/> on 2026-08-12 off its shipped stylesheet and the
16
+ markup of three pages. Warm paper (`#FAF9F5`), a warm near-black ink
17
+ (`#221D16`, 15.88:1), radii of two and three pixels, an **ink** primary button,
18
+ one hot orange (`#FF4801`) used only as a mark, and a dark ledger of
19
+ dotted-leader rows whose numbers are set in an aliased pixel face. Widened
20
+ contract — all thirteen headings — with `styles/tokens/scoreboard.css` and a
21
+ full reference kit in `kits/scoreboard/`.
22
+ - **The accent measures 3.23:1 and the pack says so at the top.** Above the 3:1
23
+ floor for a non-text mark, below the one for a word. The reference obeys this
24
+ without ever stating it: across three pages its orange is a 3×18px tick, a
25
+ `::marker`, a focus ring, a selection colour, a link underline and one
26
+ oversized chevron — and its primary button is ink. The pack turns that
27
+ observation into a ban, which is the only reason a page in it can carry a
28
+ colour that loud.
29
+ - **Two status sets rather than one filtered set.** The reference paints status
30
+ only on its dark panels; those values measure 1.6–2.6:1 against warm paper. The
31
+ paper set is selected from deeper steps of the Tailwind ramp the reference's own
32
+ stylesheet ships, and the measured on-dark set is kept beside it under
33
+ `--*-on-dark`. Every declaration says which of the two kinds of claim it is.
34
+ - **`TickHeading`, `Ledger`, `LedgerRow` and `StatusChip`** in the kit, on the
35
+ same six-component spine as every other kit. `StatusChip` takes `label` as a
36
+ **required** prop: the paper statuses cluster (the accent and `--warn` separate
37
+ by 6.3 under protanopia), so status in this pack is a chip with its word in it.
38
+
39
+ ### Fixed
40
+
41
+ - **Three corrections to the reference, recorded rather than silently applied.**
42
+ Its positive-delta colour `#00D492` is set at 11px on white — 1.84:1, an
43
+ invisible success state — and is confined here to the dark panel where it
44
+ measures 10.21:1. Its primary button transitions over 500ms, past the 300ms
45
+ ceiling in `MOTION_DOCTRINE.md` §3, and the pack pins `--dur-fast` at .16s. Its
46
+ scan line animates `top`; the pack rebuilds it on `transform`.
47
+ - **`validate.py --self-test` printed FAILED and exited 0.** `main()` returned
48
+ the self-test's status and `__main__` called it bare, so the code was dropped
49
+ on the floor and `npm run selftest` stayed green through a self-test that had
50
+ failed — found because this release's count change broke a plant fixture and
51
+ the suite passed anyway. The argv handling directly above it exists to close
52
+ this exact class one layer up and never reached the exit code. Verified by
53
+ breaking a fixture in a copy of the tree: 0 before, 1 after.
54
+ - **A plant fixture pinned to a literal that changes every release.** The
55
+ stale-count plant searched for `**twelve locked style packs**`; the first time
56
+ the library grew it mutated nothing and stopped testing the check it exists
57
+ for. It now reads whatever number the README claims and makes that wrong.
58
+ - **Reciprocal forks with `field-notes` and `workbench`.** From a distance
59
+ `field-notes` *is* this pack — warm paper, one orange-red accent, hairline
60
+ rules — and the distinction is what the small type does: mono numerals make
61
+ evidence auditable, pixel numerals make results countable. Both neighbours now
62
+ carry the fork back, so an agent arriving at either one first still learns it
63
+ exists.
64
+
65
+ ## [1.12.1] - 2026-08-11
66
+
67
+ - **The reference-sweep heading named one server for a section about two.** It
68
+ read `Optional — real-world references (Lazyweb MCP)` after Mobbin joined it,
69
+ so an agent skimming headings would conclude Mobbin was not there. Headings are
70
+ a discovery surface, not decoration.
71
+
7
72
  ## [1.12.0] - 2026-08-11
8
73
 
9
74
  Mobile becomes a register the skill can name, and a second reference sweep joins
package/README.md CHANGED
@@ -11,7 +11,7 @@ problem — invented colors, six accent hues, dark mode retrofitted later.
11
11
 
12
12
  This skill is the taste layer. It gives a coding agent **one motion
13
13
  methodology** for cinematic, scroll-driven pages, **a motion doctrine** that
14
- decides whether to animate before it decides how, and **twelve locked style
14
+ decides whether to animate before it decides how, and **thirteen locked style
15
15
  packs** with ready-made design tokens, so what it builds reads as one system
16
16
  instead of a pile of effects.
17
17
 
@@ -56,6 +56,7 @@ into the cinematic layer, and says so in its own *Motion flavor* section.
56
56
  | `blueprint` | white drawing stock, 32px grid, ruled column edges, corner registration marks, one electric blue, zero radius anywhere | infrastructure sold on precision — vector databases, search, storage and query engines |
57
57
  | `prism` | one static iridescent wash with a hard bottom edge, heavy grotesque display over mono body copy, cyan as a fill only | an open-source project's front door, where the first action is a command |
58
58
  | `maquette` | near-black table, cream ink and cream axonometric models, mono block labels, pale aqua that works as text, one offset shadow | enterprise data infrastructure sold to an architecture buyer |
59
+ | `scoreboard` | warm paper and warm near-black ink, 2–3px radii, an ink primary button, one hot orange that only ever marks, and a dark ledger of dotted-leader rows whose numbers are set in an aliased pixel face | products whose argument is an accumulating number — ads and SEO operators, growth tools, revenue dashboards sold on results |
59
60
  | `cyclorama` | a pale field cycling through six pastel stops on a 32s loop under near-black ink that never moves with it, a monospaced typewriter serif over mono, one orange used only as a fill, a particle organ that holds then redeploys, no shadows anywhere | enterprise AI transformation, applied-AI services, technical consultancies — where what is sold is a change of state and there is no screenshot worth showing |
60
61
 
61
62
  Each pack locks palette, type, texture, motion tokens, signature motifs and
@@ -126,12 +127,12 @@ skills.
126
127
  |---|---|
127
128
  | `SKILL.md` | The agent-facing skill: discovery triggers, the principles, how to apply them, quick-reference rules, common mistakes |
128
129
  | `SHELEG_DESIGN.md` | The full reference: architecture, layer-by-layer mechanics with code, the exact morph math, the DOM↔WebGL projection bridge, a build-from-scratch recipe, and why each piece works |
129
- | `SURFACE_COMPOSITION.md` | Two decisions the pack layer does not make: the six depth layers of a scene, read before writing CSS for a cinematic page; and the handoff to `dataviz`, read before drawing a chart in any pack — token names are not uniform across the twelve and an undefined custom property fails silently |
130
+ | `SURFACE_COMPOSITION.md` | Two decisions the pack layer does not make: the six depth layers of a scene, read before writing CSS for a cinematic page; and the handoff to `dataviz`, read before drawing a chart in any pack — token names are not uniform across the thirteen and an undefined custom property fails silently |
130
131
  | `MOTION_DOCTRINE.md` | Whether to animate at all, before how: the frequency table that kills motion on high-repetition paths, the easing tree and the `ease-in` ban, the duration ceiling, the forbidden forms, and the reduced-motion contract. `SKILL.md` marks it required before any animation |
131
132
  | `DESIGN_SYNC_BRIDGE.md` | The Claude Design contract: what a pack sends to claude.ai/design and in what shape, the rule for each of the four reference types, and the border motion does not cross |
132
133
  | `FIGMA_BRIDGE.md` | The design↔code contract: how a pack's tokens map onto Figma variable collections and modes, how to implement a design without importing raw values, and what cannot cross the border |
133
134
  | `AI_PRODUCT_PATTERNS.md` | The surfaces a model drives: the five states of a call, streaming instead of spinners, latency, provenance and uncertainty, agent confirmations, and the bans that keep it honest |
134
- | `styles/*.md` | The twelve style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
135
+ | `styles/*.md` | The thirteen style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
135
136
  | `styles/tokens/*.css` | The ready-made token layer per pack, copied verbatim instead of transcribed (`workbench` and `field-notes` each ship a light `:root` plus a `data-theme="dark"` twin) |
136
137
  | `styles/STYLE_PACK_TEMPLATE.md` | The pack contract as a skeleton, so a new style is authored against the same headings rather than improvised |
137
138
 
@@ -202,7 +203,7 @@ cd ./ds-workbench && npm install && npm run build
202
203
  then `/design-sync` in that directory, from Claude Code. Three layers cross: the
203
204
  pack's **bans** as the design system's own README, `styles.css` built from
204
205
  `tokens/<pack>.css` verbatim, and the components — a six-name spine that is
205
- identical in all twelve kits, so switching packs swaps identity rather than API,
206
+ identical in all thirteen kits, so switching packs swaps identity rather than API,
206
207
  plus each pack's signature parts. **Motion does not cross**, exactly as it does
207
208
  not cross into Figma: a kit is the static half of a pack, and saying so is what
208
209
  stops an agent inventing motion to fill the silence.
@@ -248,7 +249,7 @@ a pack's four widened sections used to make two gates *quieter* and still green.
248
249
  One honest limit: the npx installer is checked by asserting its runtime bundle
249
250
  walker exists, not by reading a file list — it has none by design. What proves
250
251
  it ships the right files is CI, which installs the bundle through **both**
251
- installers and `diff -r`s the result against the source, then builds all twelve
252
+ installers and `diff -r`s the result against the source, then builds all thirteen
252
253
  kits.
253
254
 
254
255
  `test/scenarios.md` (T1–T19) is the behavioral harness: fresh subagents given a
package/bin/cli.js CHANGED
@@ -234,7 +234,7 @@ ${c("bold", "What it installs")}
234
234
  DESIGN_SYNC_BRIDGE.md the Claude Design contract (what a pack sends, and
235
235
  what does not cross)
236
236
  AI_PRODUCT_PATTERNS.md chat / agent / streaming surfaces (honest state)
237
- styles/ twelve style packs — instrument-console (dark console),
237
+ styles/ thirteen style packs — instrument-console (dark console),
238
238
  editorial-luxury (warm editorial), workbench (light/dark
239
239
  product UI), briefing-room (dark 16:9 presentation deck),
240
240
  atrium (warm cream consumer health), orchard (friendly
@@ -242,7 +242,8 @@ ${c("bold", "What it installs")}
242
242
  cyclorama (a pastel field on a 32s cycle), showroom
243
243
  (the product as the exhibit), blueprint (a drawing sheet,
244
244
  zero radius), prism (one iridescent wash over mono body),
245
- maquette (cream axonometric models on a dark table) —
245
+ maquette (cream axonometric models on a dark table),
246
+ scoreboard (warm paper, pixel numerals, a dark ledger) —
246
247
  plus a ready-made token CSS per pack and
247
248
  STYLE_PACK_TEMPLATE.md for authoring more
248
249
  `);
@@ -324,7 +325,7 @@ function main() {
324
325
  ` ${c("dim", "SKILL.md")} the agent skill\n` +
325
326
  ` ${c("dim", "SHELEG_DESIGN.md")} the full reference\n` +
326
327
  ` ${c("dim", "MOTION_DOCTRINE.md")} whether to animate at all — read before any animation\n` +
327
- ` ${c("dim", "styles/")} style packs + token CSS (instrument-console / editorial-luxury / workbench / briefing-room / atrium / orchard / field-notes / cyclorama / showroom / blueprint / prism / maquette)\n\n` +
328
+ ` ${c("dim", "styles/")} style packs + token CSS (instrument-console / editorial-luxury / workbench / briefing-room / atrium / orchard / field-notes / cyclorama / showroom / blueprint / prism / maquette / scoreboard)\n\n` +
328
329
  `Your Cursor / Claude agent can now discover the skill and build\n` +
329
330
  `cinematic, scroll-driven pages — or style product UI (dashboards, admin,\n` +
330
331
  `internal tools) from a standalone pack — on its principles.\n\n` +
@@ -27,7 +27,10 @@ under a seven-layer shadow; blueprint — a drawing sheet for infrastructure
27
27
  sold on precision: grid, ruled columns, registration marks, one electric
28
28
  blue and zero radius; prism — an open-source project's front door, one
29
29
  static iridescent wash over mono body copy; maquette — enterprise data
30
- infrastructure as a cream axonometric model on a near-black table);
30
+ infrastructure as a cream axonometric model on a near-black table;
31
+ scoreboard — warm paper and a dark ledger of dotted-leader rows whose
32
+ numbers are set in an aliased pixel face, for a product whose argument is
33
+ an accumulating number);
31
34
  otherwise follow the contract below (self-contained on purpose).
32
35
 
33
36
  ## Whether to animate at all — before how
@@ -0,0 +1,14 @@
1
+ {
2
+ "pkg": "@sheleg-design/scoreboard",
3
+ "globalName": "ShelegScoreboard",
4
+ "shape": "package",
5
+ "buildCmd": "npm run build",
6
+ "srcDir": "src",
7
+ "tsconfig": "tsconfig.json",
8
+ "cssEntry": "src/styles.css",
9
+ "docsDir": "src",
10
+ "readmeHeader": ".design-sync/conventions.md",
11
+ "guidelinesGlob": [
12
+ "guidelines/*.md"
13
+ ]
14
+ }
@@ -0,0 +1,59 @@
1
+ # Scoreboard — the contract this design system ships under
2
+
3
+ **Register.** Choose Scoreboard for a product whose argument is **an
4
+ accumulating number**: performance marketing and ads operators, SEO and growth
5
+ tools, revenue dashboards sold on results, agency-replacement services. The page
6
+ is a running tally — a dated column of figures the product already produced, with
7
+ the label on the left, the number on the right, and a line of dots between them.
8
+ The fork people get wrong is against `field-notes`, which shares the warm paper
9
+ and the orange-red accent: there the numerals are mono and make evidence
10
+ *auditable*; here they are pixel and make results *countable*. *How do you know
11
+ that?* goes there. *How much, and since when?* stays here. Build every screen
12
+ against `var(--…)` and never a literal.
13
+
14
+ **The accent is a mark, not a voice.** `--accent` `#FF4801` measures 3.23:1 on
15
+ the paper — above the 3:1 floor for a non-text mark, below the one for a word.
16
+ It is the 3×18px tick, the list marker, the focus ring and the link underline.
17
+ Body text, headings and button fills set in it are the fastest way to break this
18
+ system.
19
+
20
+ **The action is ink.** `--action` `#0A0A0A` with a white label is the primary
21
+ button. This is measured off the reference, and it is why the accent survives:
22
+ the loudest colour on the page never competes with the thing you are meant to
23
+ click. A view with an orange CTA has two primaries and no accent.
24
+
25
+ **Status is never by colour alone.** The paper statuses cluster — the accent and
26
+ `--warn` separate by only 6.3 under protanopia, `--danger` and `--warn` by 12.6
27
+ at full colour. Every status is therefore a chip containing its word, which is
28
+ why `StatusChip` takes `label` as a required prop rather than an optional one.
29
+
30
+ **There are two status sets, not one.** On paper use `--good` / `--warn` /
31
+ `--danger` / `--info`; on a `data-surface="panel"` element use the measured
32
+ `--*-on-dark` values. The dark set measures 1.6–2.6:1 against the paper and the
33
+ paper set disappears on the panel. `StatusChip` takes `onPanel` for exactly this.
34
+
35
+ **Radii are two and three pixels.** Across the reference, 107 of 143 radius
36
+ utilities are 2px or 3px. At 8px everywhere this becomes a generic product page
37
+ with an orange tick on it. When containers nest, an inner radius is the outer
38
+ minus the padding between them.
39
+
40
+ **Bans** (verbatim from the pack):
41
+
42
+ - The accent as body text, a heading, or a button fill.
43
+ - A second ledger on the page, a ledger with a rounded marketing number in it,
44
+ or a ledger with no date under it.
45
+ - Antialiased pixel type. Without smoothing off, the numerals are a novelty face.
46
+ - A bare status dot, and any status carried by colour with no word beside it.
47
+ - The paper status set on the dark band, or the dark set on paper.
48
+ - Radii above 8px, and any radius on the ledger's own rows.
49
+ - A spinner where a number will land. The skeleton is the row.
50
+ - Fluid `clamp()` display type; `transition: all`; a second accent; a gradient
51
+ anywhere except the hero's wash.
52
+
53
+ **The body size is 15px and the numeral column is fixed.** Press Start 2P has no
54
+ currency width worth trusting: budget the column in pixels (80px, 70px below the
55
+ medium breakpoint) and right-align it.
56
+
57
+ Motion is not part of this design system and must not be invented: a kit is the
58
+ static half of a pack, and anything that moves — the entrance, the scan line, the
59
+ count-up — stays behind in the pack.
@@ -0,0 +1,16 @@
1
+ # @sheleg-design/scoreboard
2
+
3
+ The React reference kit for the SHELEG **Scoreboard** style pack — warm paper, an
4
+ orange that only ever marks, and a ledger of dotted-leader rows whose numbers are
5
+ set in an aliased pixel face.
6
+
7
+ It is generated from the pack, not authored beside it: `src/styles.css` opens
8
+ with `styles/tokens/scoreboard.css` byte for byte, and the rules the design agent
9
+ must obey are in [`.design-sync/conventions.md`](./.design-sync/conventions.md).
10
+
11
+ ```bash
12
+ npm install && npm run build
13
+ ```
14
+
15
+ Then run `/design-sync` in Claude Code from this directory to push it to
16
+ claude.ai/design.
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@sheleg-design/scoreboard",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json"
21
+ },
22
+ "peerDependencies": {
23
+ "react": ">=18"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^5.6.0",
27
+ "@types/react": "^18.3.0"
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Actions
3
+ ---
4
+
5
+ `primary` is the **ink** fill, not the accent — this pack's loudest colour never competes with the thing you are meant to click. `secondary` is the 4%-ink ghost. Nothing scales on hover: the fill changes and the button presses by one pixel.
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface ButtonProps {
4
+ /** `primary` is the pack's default fill — at most one accent action per view. */
5
+ variant?: 'primary' | 'secondary' | 'ghost';
6
+ size?: 'sm' | 'md' | 'lg';
7
+ disabled?: boolean;
8
+ onClick?: () => void;
9
+ children: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function Button({
14
+ variant = 'primary',
15
+ size = 'md',
16
+ disabled = false,
17
+ onClick,
18
+ children,
19
+ className,
20
+ }: ButtonProps) {
21
+ return (
22
+ <button
23
+ type="button"
24
+ className={['sb-btn', `sb-btn--${variant}`, `sb-btn--${size}`, className]
25
+ .filter(Boolean)
26
+ .join(' ')}
27
+ disabled={disabled}
28
+ onClick={onClick}
29
+ >
30
+ {children}
31
+ </button>
32
+ );
33
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Surfaces
3
+ ---
4
+
5
+ A `--surface` panel with a 1px `--line-weak` and a hairline under it, at `--radius-sm` — three pixels, not eight. This pack divides with rules; `--shadow-card` is reserved for the one report surface a section is built around.
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface CardProps {
4
+ title?: string;
5
+ /** Right-aligned metadata on the title row: a count, an id, a timestamp. */
6
+ meta?: string;
7
+ children: ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function Card({ title, meta, children, className }: CardProps) {
12
+ const head = title !== undefined || meta !== undefined;
13
+ return (
14
+ <section className={['sb-card', className].filter(Boolean).join(' ')}>
15
+ {head && (
16
+ <div className="sb-card__head">
17
+ {title !== undefined && <h3 className="sb-card__title">{title}</h3>}
18
+ {meta !== undefined && <span className="sb-card__meta">{meta}</span>}
19
+ </div>
20
+ )}
21
+ <div className="sb-card__body">{children}</div>
22
+ </section>
23
+ );
24
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Data
3
+ ---
4
+
5
+ A 2px-radius tag at 11px uppercase. `accent` uses `--accent-wash` behind `--accent-hover`, never the raw accent as text. For anything that carries a state, use `StatusChip` instead — it requires the word.
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface ChipProps {
4
+ children: ReactNode;
5
+ selected?: boolean;
6
+ tone?: 'neutral' | 'accent';
7
+ className?: string;
8
+ }
9
+
10
+ export function Chip({ children, selected = false, tone = 'neutral', className }: ChipProps) {
11
+ return (
12
+ <span
13
+ className={[
14
+ 'sb-chip',
15
+ `sb-chip--${tone}`,
16
+ selected ? 'sb-chip--selected' : undefined,
17
+ className,
18
+ ]
19
+ .filter(Boolean)
20
+ .join(' ')}
21
+ >
22
+ {children}
23
+ </span>
24
+ );
25
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Foundations
3
+ ---
4
+
5
+ Satoshi at 600–700 with the tracking coupled to the size: −0.03em at display, −0.02em at section. Section headings normally arrive through `TickHeading`, which puts the accent tick in front of them.
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface HeadingProps {
4
+ /** 1 = page title, 2 = section, 3 = sub-head. */
5
+ level?: 1 | 2 | 3;
6
+ children: ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ export function Heading({ level = 2, children, className }: HeadingProps) {
11
+ const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
12
+ return (
13
+ <Tag
14
+ className={['sb-heading', `sb-heading--${level}`, className].filter(Boolean).join(' ')}
15
+ >
16
+ {children}
17
+ </Tag>
18
+ );
19
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Signature
3
+ ---
4
+
5
+ The signature element: a dark panel of dotted-leader rows under a bulleted group label, with the date the figures were last true at its foot. One per page. `status` renders beside the live square and is a sentence — the square is never shown alone.
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface LedgerProps {
4
+ /** The group label: uppercase, +0.14em, with a square bullet ahead of it. */
5
+ label?: string;
6
+ /** The date the figures were last true. A ledger with no date is a poster. */
7
+ updated?: string;
8
+ /** Rendered beside the live square, e.g. "All systems ok". Never a bare dot. */
9
+ status?: string;
10
+ children: ReactNode;
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * The pack's signature element: a dark panel of dotted-leader rows. The scan
16
+ * line that crosses it on the live site is motion and stays in the pack — a kit
17
+ * is the static half.
18
+ */
19
+ export function Ledger({ label, updated, status, children, className }: LedgerProps) {
20
+ return (
21
+ <section
22
+ data-surface="panel"
23
+ className={['sb-ledger', className].filter(Boolean).join(' ')}
24
+ >
25
+ {label !== undefined && (
26
+ <p className="sb-ledger__label">
27
+ <span className="sb-ledger__bullet" aria-hidden="true" />
28
+ {label}
29
+ </p>
30
+ )}
31
+ <div className="sb-ledger__rows">{children}</div>
32
+ {(updated !== undefined || status !== undefined) && (
33
+ <div className="sb-ledger__foot">
34
+ {updated !== undefined && <div>Last updated: {updated}</div>}
35
+ {status !== undefined && (
36
+ <div className="sb-ledger__status">
37
+ <span className="sb-ledger__live" aria-hidden="true" />
38
+ {status}
39
+ </div>
40
+ )}
41
+ </div>
42
+ )}
43
+ </section>
44
+ );
45
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Signature
3
+ ---
4
+
5
+ Label, dotted leader, pixel numeral on a fixed 80px column (70px below the medium breakpoint). The leader absorbs every width change so the row never wraps. `pending` keeps the row and puts an em dash where the number will land — a skeleton, never a spinner.
@@ -0,0 +1,28 @@
1
+ export interface LedgerRowProps {
2
+ /** Left column, two lines at most. It does not wrap past that. */
3
+ label: string;
4
+ /** The figure. Set in the pixel face, right-aligned on a fixed column. */
5
+ value: string;
6
+ /** No figure yet: the row survives and an em dash holds the column. */
7
+ pending?: boolean;
8
+ className?: string;
9
+ }
10
+
11
+ /**
12
+ * Label, a dotted leader, and a pixel numeral on a fixed column. A row that
13
+ * wraps is not a row — the leader absorbs every width change instead.
14
+ */
15
+ export function LedgerRow({ label, value, pending = false, className }: LedgerRowProps) {
16
+ return (
17
+ <div className={['sb-row', className].filter(Boolean).join(' ')}>
18
+ <span className="sb-row__label">{label}</span>
19
+ <span className="sb-row__leader" aria-hidden="true" />
20
+ <span className={['sb-row__value', pending ? 'sb-row__value--pending' : undefined]
21
+ .filter(Boolean)
22
+ .join(' ')}
23
+ >
24
+ {pending ? '—' : value}
25
+ </span>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Foundations
3
+ ---
4
+
5
+ The pack's elevation model in one element. `hairline` is `--line-weak`, `strong` is `--line`; a card border and a section divider are the same rule at two weights.
@@ -0,0 +1,18 @@
1
+ export interface RuleProps {
2
+ tone?: 'hairline' | 'strong';
3
+ className?: string;
4
+ }
5
+
6
+ export function Rule({ tone = 'hairline', className }: RuleProps) {
7
+ return (
8
+ <hr
9
+ className={[
10
+ 'sb-rule',
11
+ tone === 'strong' ? 'sb-rule--strong' : undefined,
12
+ className,
13
+ ]
14
+ .filter(Boolean)
15
+ .join(' ')}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Data
3
+ ---
4
+
5
+ A figure in `--ink-strong` above its label. Use it for a single number in the flow of a page; four or more figures that belong together are a `Ledger`, not four Stats. Give `source` whenever the number came from somewhere checkable.
@@ -0,0 +1,17 @@
1
+ export interface StatProps {
2
+ value: string;
3
+ label: string;
4
+ /** Where the figure came from — a query, a window, a dashboard. */
5
+ source?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export function Stat({ value, label, source, className }: StatProps) {
10
+ return (
11
+ <div className={['sb-stat', className].filter(Boolean).join(' ')}>
12
+ <span className="sb-stat__label">{label}</span>
13
+ <span className="sb-stat__value">{value}</span>
14
+ {source !== undefined && <span className="sb-stat__source">{source}</span>}
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Data
3
+ ---
4
+
5
+ `label` is required, not optional: the paper statuses cluster (the accent and `--warn` separate by 6.3 under protanopia), so a status in this pack is a chip with its word inside. `onPanel` swaps to the measured on-dark set, which is a different palette rather than the same one lightened.
@@ -0,0 +1,27 @@
1
+ export type Status = 'good' | 'warn' | 'danger' | 'info';
2
+
3
+ export interface StatusChipProps {
4
+ status: Status;
5
+ /** Required, not optional. Status is never by colour alone in this pack. */
6
+ label: string;
7
+ /** On a dark panel the measured on-dark set applies instead of the paper one. */
8
+ onPanel?: boolean;
9
+ className?: string;
10
+ }
11
+
12
+ export function StatusChip({ status, label, onPanel = false, className }: StatusChipProps) {
13
+ return (
14
+ <span
15
+ className={[
16
+ 'sb-status',
17
+ `sb-status--${status}`,
18
+ onPanel ? 'sb-status--on-panel' : undefined,
19
+ className,
20
+ ]
21
+ .filter(Boolean)
22
+ .join(' ')}
23
+ >
24
+ {label}
25
+ </span>
26
+ );
27
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ category: Signature
3
+ ---
4
+
5
+ The 3×18px accent bar, a 10px gap, then the heading. The most recognisable thing in the pack and one of the few places the raw `--accent` is correct at full strength: a mark is not read, so its 3.23:1 against the paper is measured against the 3:1 non-text floor rather than the text one.
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface TickHeadingProps {
4
+ /** 2 = section, 3 = sub-head. A tick never precedes the page title. */
5
+ level?: 2 | 3;
6
+ children: ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * A 3x18px accent bar, a 10px gap, then the heading. The pack's most
12
+ * recognisable motif and the one place the raw accent is correct at full
13
+ * strength — it is a mark, not a word.
14
+ */
15
+ export function TickHeading({ level = 2, children, className }: TickHeadingProps) {
16
+ const Tag = `h${level}` as 'h2' | 'h3';
17
+ return (
18
+ <div className={['sb-tick-heading', className].filter(Boolean).join(' ')}>
19
+ <span className="sb-tick-heading__tick" aria-hidden="true" />
20
+ <Tag className={['sb-heading', `sb-heading--${level}`].join(' ')}>{children}</Tag>
21
+ </div>
22
+ );
23
+ }