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.
- package/CHANGELOG.md +65 -0
- package/README.md +6 -5
- package/bin/cli.js +4 -3
- package/cursor/rules/sheleg-design.mdc +4 -1
- package/kits/scoreboard/.design-sync/config.json +14 -0
- package/kits/scoreboard/.design-sync/conventions.md +59 -0
- package/kits/scoreboard/README.md +16 -0
- package/kits/scoreboard/package.json +29 -0
- package/kits/scoreboard/src/Button.md +5 -0
- package/kits/scoreboard/src/Button.tsx +33 -0
- package/kits/scoreboard/src/Card.md +5 -0
- package/kits/scoreboard/src/Card.tsx +24 -0
- package/kits/scoreboard/src/Chip.md +5 -0
- package/kits/scoreboard/src/Chip.tsx +25 -0
- package/kits/scoreboard/src/Heading.md +5 -0
- package/kits/scoreboard/src/Heading.tsx +19 -0
- package/kits/scoreboard/src/Ledger.md +5 -0
- package/kits/scoreboard/src/Ledger.tsx +45 -0
- package/kits/scoreboard/src/LedgerRow.md +5 -0
- package/kits/scoreboard/src/LedgerRow.tsx +28 -0
- package/kits/scoreboard/src/Rule.md +5 -0
- package/kits/scoreboard/src/Rule.tsx +18 -0
- package/kits/scoreboard/src/Stat.md +5 -0
- package/kits/scoreboard/src/Stat.tsx +17 -0
- package/kits/scoreboard/src/StatusChip.md +5 -0
- package/kits/scoreboard/src/StatusChip.tsx +27 -0
- package/kits/scoreboard/src/TickHeading.md +5 -0
- package/kits/scoreboard/src/TickHeading.tsx +23 -0
- package/kits/scoreboard/src/index.ts +23 -0
- package/kits/scoreboard/src/styles.css +332 -0
- package/kits/scoreboard/tsconfig.json +15 -0
- package/package.json +2 -2
- package/plugins/sheleg-design/.claude-plugin/plugin.json +2 -2
- package/plugins/sheleg-design/commands/sheleg-design.md +2 -2
- package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/MOBILE_SURFACES.md +2 -2
- package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +5 -4
- package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +2 -2
- package/plugins/sheleg-design/skills/sheleg-design/styles/field-notes.md +9 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/scoreboard.md +355 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/scoreboard.css +229 -0
- 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 **
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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,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,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,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,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,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
|
+
}
|