sheleg-design-skill 1.20.0 → 1.22.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 +130 -0
- package/README.md +5 -4
- package/bin/cli.js +5 -3
- package/cursor/rules/sheleg-design.mdc +4 -1
- package/kits/pigeonhole/.design-sync/config.json +14 -0
- package/kits/pigeonhole/.design-sync/conventions.md +59 -0
- package/kits/pigeonhole/README.md +53 -0
- package/kits/pigeonhole/package.json +29 -0
- package/kits/pigeonhole/src/Button.md +20 -0
- package/kits/pigeonhole/src/Button.tsx +33 -0
- package/kits/pigeonhole/src/Card.md +16 -0
- package/kits/pigeonhole/src/Card.tsx +24 -0
- package/kits/pigeonhole/src/CategoryChip.md +44 -0
- package/kits/pigeonhole/src/CategoryChip.tsx +42 -0
- package/kits/pigeonhole/src/Chip.md +16 -0
- package/kits/pigeonhole/src/Chip.tsx +25 -0
- package/kits/pigeonhole/src/FaqList.md +11 -0
- package/kits/pigeonhole/src/FaqList.tsx +30 -0
- package/kits/pigeonhole/src/Heading.md +17 -0
- package/kits/pigeonhole/src/Heading.tsx +19 -0
- package/kits/pigeonhole/src/LabelledRow.md +27 -0
- package/kits/pigeonhole/src/LabelledRow.tsx +54 -0
- package/kits/pigeonhole/src/Rule.md +12 -0
- package/kits/pigeonhole/src/Rule.tsx +18 -0
- package/kits/pigeonhole/src/Stat.md +11 -0
- package/kits/pigeonhole/src/Stat.tsx +17 -0
- package/kits/pigeonhole/src/WashCard.md +17 -0
- package/kits/pigeonhole/src/WashCard.tsx +27 -0
- package/kits/pigeonhole/src/index.ts +23 -0
- package/kits/pigeonhole/src/styles.css +860 -0
- package/kits/pigeonhole/tsconfig.json +15 -0
- package/kits/showroom/src/styles.css +11 -0
- package/package.json +2 -2
- package/plugins/sheleg-design/.claude-plugin/plugin.json +2 -2
- package/plugins/sheleg-design/commands/sheleg-design.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/MOBILE_SURFACES.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +8 -5
- package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +3 -3
- package/plugins/sheleg-design/skills/sheleg-design/styles/STYLE_PACK_TEMPLATE.md +30 -3
- package/plugins/sheleg-design/skills/sheleg-design/styles/cyclorama.md +7 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/manpage.md +4 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/orchard.md +7 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/pigeonhole.md +503 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/showroom.md +10 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/pigeonhole.css +327 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/showroom.css +11 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/workbench.md +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,136 @@ 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.22.0] - 2026-08-13
|
|
8
|
+
|
|
9
|
+
The palette gate learns two colour forms it had been refusing, and the refusal turns
|
|
10
|
+
out to have been the reason eight token layers restate a token's channels by hand.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`color-mix()` and relative colour are computable, so they are no longer banned.**
|
|
15
|
+
The gate parses `color-mix(in srgb | srgb-linear | oklab | oklch, A p%, B q%)` —
|
|
16
|
+
premultiplied, shorter hue arc — and `rgb(from <colour> r g b / a)`, with `var()`
|
|
17
|
+
now resolved **inside** a value rather than only as a whole value. Verified against
|
|
18
|
+
Chrome 151's own computed values across eleven cases at a **worst ΔE of 0.004**,
|
|
19
|
+
which is the browser's six-digit serialisation rather than a disagreement.
|
|
20
|
+
- **Four self-test plants, two of which prove the new paths are checked rather than
|
|
21
|
+
tolerated.** A `color-mix()` and a relative colour whose ink misses AA must fail on
|
|
22
|
+
the *ratio* — only possible if the parser really computed them. The other two prove
|
|
23
|
+
the refusals still refuse: an unimplemented mix space, and a `calc()` inside a
|
|
24
|
+
channel. Half-implemented CSS maths is worse than an honest refusal.
|
|
25
|
+
- **Rule 5 in the pack skeleton** replaces the ban with the limits and one migration
|
|
26
|
+
rule: relative colour is Baseline 2024, so where a token feeds an
|
|
27
|
+
accessibility-critical property — a focus ring above all — the literal ships first
|
|
28
|
+
and the derived value second, because a dropped declaration on a focus ring is an
|
|
29
|
+
invisible focus indicator.
|
|
30
|
+
- **`docs/audit/2026-08-13-modern-css-audit.md`** — the whole measurement: what the
|
|
31
|
+
library is ahead on (OKLab dichromacy checks, `color-scheme` in 16/16,
|
|
32
|
+
`text-wrap: balance` in 13/16 kits, `tabular-nums` in 10/16), what is a deliberate
|
|
33
|
+
position rather than a gap, and ten findings with counts.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`showroom`'s focus ring now tracks its accent.** `rgba(38, 109, 240, 0.35)` was
|
|
38
|
+
the accent's channels written out by hand; it is now
|
|
39
|
+
`rgb(from var(--accent) r g b / 0.35)`, measured **ΔE 0.00** from the literal, with
|
|
40
|
+
the literal kept as the preceding declaration. Re-tinting `--accent` used to leave
|
|
41
|
+
the ring on the old blue silently — the live mechanism behind B-023/B-024.
|
|
42
|
+
- **A blind spot closed in the same commit that opened its cause.** `themes()` decided
|
|
43
|
+
whether a block was a theme by testing for a `#` or an `oklch(` prefix, so a dark
|
|
44
|
+
theme written in `color-mix()` would have been read as "overrides no colour" and
|
|
45
|
+
skipped entirely. It asks `COLOR_SHAPED` now.
|
|
46
|
+
|
|
47
|
+
### Notes
|
|
48
|
+
|
|
49
|
+
- **The migration itself is not in this release.** 42 declarations across eight token
|
|
50
|
+
layers are ΔE 0.00 from a token in their own file and could migrate with no visible
|
|
51
|
+
change; 13 more sit within ΔE 2 without equalling one, and *near is not drift* — a
|
|
52
|
+
white at 80% beside an off-white field may be deliberately white. The first set is
|
|
53
|
+
B-027 and needs a per-property support decision; the second is B-028 and needs its
|
|
54
|
+
author, not a script.
|
|
55
|
+
- Also filed: B-029 (container queries in the kits — 0 of 16, against 7 viewport
|
|
56
|
+
blocks), B-030 (`text-box-trim`, measure in `ch`, metric-matched fallbacks,
|
|
57
|
+
`@property`, fluid spacing), B-031 (a DTCG export for the Figma seam).
|
|
58
|
+
|
|
59
|
+
## [1.21.0] - 2026-08-12
|
|
60
|
+
|
|
61
|
+
A sixteenth style pack, whose eleven pastel hues are a filing scheme rather than a
|
|
62
|
+
mood — and a taxonomy that fails its own contrast floor eight times out of nine.
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **`pigeonhole`** — the sixteenth pack, extracted from `getinboxzero.com` off the
|
|
67
|
+
server-rendered HTML of `/` (399,558 bytes), its two shipped stylesheets
|
|
68
|
+
(599,990 bytes, 152 custom properties) and then off **computed styles on the
|
|
69
|
+
live page** at 1440×900, 768×1024 and 390×844 — 912 rendered elements. A white
|
|
70
|
+
field ruled by hairlines, one blue that only ever appears as a two-stop
|
|
71
|
+
gradient, a display face that never passes weight 400, one italic word in the
|
|
72
|
+
headline, and nine categories in which a hue *is* the category, drawn from an
|
|
73
|
+
eleven-ramp pastel system. For products
|
|
74
|
+
whose job is to sort the reader's incoming mess into named categories — email
|
|
75
|
+
triage, ticket routing, digests, organisers, CRM inboxes. Widened contract, a
|
|
76
|
+
light-only token layer, a full reference kit, and reciprocal forks written into
|
|
77
|
+
`cyclorama`, `showroom`, `orchard`, `workbench` and `manpage`.
|
|
78
|
+
- **The signature element is a chip with two layers.** The outer carries the
|
|
79
|
+
deeper tint pair at radius 8px, the inner the paler pair at 7px with one pixel
|
|
80
|
+
between them — the one place in the reference where radius-by-subtraction
|
|
81
|
+
happens to hold exactly. `CategoryChip`'s label word is a **required** prop, not
|
|
82
|
+
an optional one, and the reason is measured: see below.
|
|
83
|
+
- **Nine category inks, eight of them derived.** The reference paints its chip
|
|
84
|
+
inks on tints of their own hue and eight of nine fail WCAG AA against those very
|
|
85
|
+
tints — `#49d1fa` at 1.53:1, `#d8a40c` at 1.65:1, `#e65707` at 2.71:1, `#17a34a`
|
|
86
|
+
at 2.72:1, `#c942b2` at 2.79:1, `#c94244` at 3.09:1, `#124dff` at 3.89:1,
|
|
87
|
+
`#6410ff` at 4.28:1. Only the neutral `#525252` clears it. Each is re-derived
|
|
88
|
+
along OKLab lightness with hue and chroma held, and each derivation is marked at
|
|
89
|
+
its declaration.
|
|
90
|
+
- **The label word is mandatory, and the number says why.** Darkening those inks
|
|
91
|
+
to clear 4.5:1 compresses them: the worst deuteranopic pair (Marketing against
|
|
92
|
+
Notification) falls from ΔE 4.42 to **1.24**, far under the palette gate's hard
|
|
93
|
+
floor of 10. Eleven hues cannot be simultaneously AA-compliant and mutually
|
|
94
|
+
distinguishable to a dichromatic reader, so the hue is declared a *redundant*
|
|
95
|
+
channel and the category tokens sit deliberately outside the gate's semantic
|
|
96
|
+
peer set — with the reason written at the declaration, so a future widening
|
|
97
|
+
reads it rather than assuming an oversight.
|
|
98
|
+
- **`LabelledRow`, `WashCard` and `FaqList`** join the six-component spine. A
|
|
99
|
+
wash card's shadow is mixed toward its own hue rather than toward black, which
|
|
100
|
+
is why the page reads coloured while the field stays white.
|
|
101
|
+
|
|
102
|
+
### Fixed
|
|
103
|
+
|
|
104
|
+
- **The primary button's ramp is reversed against the reference.** White on its
|
|
105
|
+
upper gradient stop measures 5.04:1 and on its lower stop 3.29:1 — the label
|
|
106
|
+
passes at the top of the button and fails at the bottom of the same button. The
|
|
107
|
+
pack and the kit run the gradient light-to-dark so the worst case is the passing
|
|
108
|
+
colour.
|
|
109
|
+
- **The focused CTA gets a ring.** The reference computes `outline-style: none` on
|
|
110
|
+
its focused primary button with no compensating shadow. The kit ships 2px of
|
|
111
|
+
`--accent-strong` at 2px offset.
|
|
112
|
+
- **The lede ink.** `#848484` at 3.74:1, painted at 18px regular, replaced by the
|
|
113
|
+
reference's own `#6b7280` at 4.83:1.
|
|
114
|
+
- **Two counts that reached no check** are corrected in passing:
|
|
115
|
+
`SURFACE_COMPOSITION.md`'s accent-role tally (thirteen → fourteen, recounted
|
|
116
|
+
from the token layers rather than incremented) and the kit-count claims in
|
|
117
|
+
`README.md`. The class stays open on the board as B-016.
|
|
118
|
+
- **B-015 is closed.** `.tmp-fp-hero.png` leaves the tree and `.gitignore` gains
|
|
119
|
+
the `.tmp*` rule that would have stopped it entering.
|
|
120
|
+
|
|
121
|
+
### Notes
|
|
122
|
+
|
|
123
|
+
- **Two claims from the screenshots were refuted by the DOM and are recorded
|
|
124
|
+
rather than dropped.** There is no rotation anywhere on the page — zero
|
|
125
|
+
elements carry a rotation term in `transform` or the individual `rotate`
|
|
126
|
+
property, at three viewports — so a scattered, tilted pile is explicitly not
|
|
127
|
+
this pack. And the before/after diptych is not built from DOM rows: the words
|
|
128
|
+
`Before` and `After` appear zero times in the served HTML and zero times in the
|
|
129
|
+
live DOM after a full scroll pass. The section is raster art, and the pack
|
|
130
|
+
specifies it as art direction with an aspect ratio rather than as a component.
|
|
131
|
+
- **No dark theme.** The reference's stylesheet carries a `.dark` block, but it
|
|
132
|
+
belongs to the application's stock shadcn slate theme and nothing on the
|
|
133
|
+
marketing page consumes it. Shipping an undocumented dark twin is a defect this
|
|
134
|
+
library already carries once (board B-018), so the pack is light only and says
|
|
135
|
+
so.
|
|
136
|
+
|
|
7
137
|
## [1.20.0] - 2026-08-12
|
|
8
138
|
|
|
9
139
|
A fifteenth style pack, whose display typeface costs zero bytes — and three
|
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 **sixteen 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
|
|
|
@@ -60,6 +60,7 @@ into the cinematic layer, and says so in its own *Motion flavor* section.
|
|
|
60
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 |
|
|
61
61
|
| `datasheet` | an off-white spec sheet ruled with dashed page guides, a live instrument built from hairline cells at radius zero, one vivid orange, Inter over JetBrains Mono, concentric radii from 16 to 2, and a dark alarm state the instrument enters when it detects the reader is hiding | B2B SaaS whose product is a verdict about the visitor, the request or the device — fraud and bot detection, device intelligence, identity and verification, API products sold on their payload |
|
|
62
62
|
| `manpage` | cream paper under the reader's own system monospace — zero webfont bytes for the display face — a 48px display that never grows louder, a 576px argument column, coral label chips that are real `<h2>`s, `└` tree glyphs in their own grid column, and one dark code frame as the focal point | developer products whose buyer reads code — APIs, SDKs, CLIs, MCP servers, developer infrastructure, where the honest hero is the call itself |
|
|
63
|
+
| `pigeonhole` | a white field ruled by hairlines, a display face that never passes weight 400 with exactly one italic word, and **nine categories in which a hue is the category**, drawn from an eleven-ramp pastel system — each rendered as a two-layer chip, 8px outside and 7px inside, whose label word is mandatory because the hue cannot carry the meaning alone | products whose job is to sort the reader's incoming mess into named categories — email triage, ticket routing, notification digests, file organisers, CRM inboxes |
|
|
63
64
|
|
|
64
65
|
Each pack locks palette, type, texture, motion tokens, signature motifs and
|
|
65
66
|
bans — and ships a `tokens/<pack>.css` to copy verbatim, so the agent never
|
|
@@ -134,7 +135,7 @@ skills.
|
|
|
134
135
|
| `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 |
|
|
135
136
|
| `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 |
|
|
136
137
|
| `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 |
|
|
137
|
-
| `styles/*.md` | The
|
|
138
|
+
| `styles/*.md` | The sixteen style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
|
|
138
139
|
| `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) |
|
|
139
140
|
| `styles/STYLE_PACK_TEMPLATE.md` | The pack contract as a skeleton, so a new style is authored against the same headings rather than improvised |
|
|
140
141
|
|
|
@@ -205,7 +206,7 @@ cd ./ds-workbench && npm install && npm run build
|
|
|
205
206
|
then `/design-sync` in that directory, from Claude Code. Three layers cross: the
|
|
206
207
|
pack's **bans** as the design system's own README, `styles.css` built from
|
|
207
208
|
`tokens/<pack>.css` verbatim, and the components — a six-name spine that is
|
|
208
|
-
identical in all
|
|
209
|
+
identical in all sixteen kits, so switching packs swaps identity rather than API,
|
|
209
210
|
plus each pack's signature parts. **Motion does not cross**, exactly as it does
|
|
210
211
|
not cross into Figma: a kit is the static half of a pack, and saying so is what
|
|
211
212
|
stops an agent inventing motion to fill the silence.
|
|
@@ -251,7 +252,7 @@ a pack's four widened sections used to make two gates *quieter* and still green.
|
|
|
251
252
|
One honest limit: the npx installer is checked by asserting its runtime bundle
|
|
252
253
|
walker exists, not by reading a file list — it has none by design. What proves
|
|
253
254
|
it ships the right files is CI, which installs the bundle through **both**
|
|
254
|
-
installers and `diff -r`s the result against the source, then builds all
|
|
255
|
+
installers and `diff -r`s the result against the source, then builds all sixteen
|
|
255
256
|
kits.
|
|
256
257
|
|
|
257
258
|
`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/ sixteen 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
|
|
@@ -247,7 +247,9 @@ ${c("bold", "What it installs")}
|
|
|
247
247
|
datasheet (an off-white spec sheet whose live instrument
|
|
248
248
|
goes dark when it catches the reader hiding),
|
|
249
249
|
manpage (a developer landing page set in the reader's
|
|
250
|
-
own system monospace, cream paper, coral label chips)
|
|
250
|
+
own system monospace, cream paper, coral label chips),
|
|
251
|
+
pigeonhole (a white sorting wall whose nine pastel
|
|
252
|
+
categories are a filing scheme, one hue each) —
|
|
251
253
|
plus a ready-made token CSS per pack and
|
|
252
254
|
STYLE_PACK_TEMPLATE.md for authoring more
|
|
253
255
|
`);
|
|
@@ -329,7 +331,7 @@ function main() {
|
|
|
329
331
|
` ${c("dim", "SKILL.md")} the agent skill\n` +
|
|
330
332
|
` ${c("dim", "SHELEG_DESIGN.md")} the full reference\n` +
|
|
331
333
|
` ${c("dim", "MOTION_DOCTRINE.md")} whether to animate at all — read before any animation\n` +
|
|
332
|
-
` ${c("dim", "styles/")} style packs + token CSS (instrument-console / editorial-luxury / workbench / briefing-room / atrium / orchard / field-notes / cyclorama / showroom / blueprint / prism / maquette / scoreboard / datasheet / manpage)\n\n` +
|
|
334
|
+
` ${c("dim", "styles/")} style packs + token CSS (instrument-console / editorial-luxury / workbench / briefing-room / atrium / orchard / field-notes / cyclorama / showroom / blueprint / prism / maquette / scoreboard / datasheet / manpage / pigeonhole)\n\n` +
|
|
333
335
|
`Your Cursor / Claude agent can now discover the skill and build\n` +
|
|
334
336
|
`cinematic, scroll-driven pages — or style product UI (dashboards, admin,\n` +
|
|
335
337
|
`internal tools) from a standalone pack — on its principles.\n\n` +
|
|
@@ -36,7 +36,10 @@ which re-skins itself dark when it detects the reader is hiding, for B2B
|
|
|
36
36
|
SaaS whose product is a verdict about the visitor or the device;
|
|
37
37
|
manpage — a developer landing page set in the reader's own system
|
|
38
38
|
monospace on cream paper, coral label chips that are real headings and a
|
|
39
|
-
dark code frame as the argument, for APIs, SDKs and CLIs
|
|
39
|
+
dark code frame as the argument, for APIs, SDKs and CLIs; pigeonhole — a
|
|
40
|
+
white sorting wall whose nine pastel categories are a taxonomy rather than
|
|
41
|
+
a mood, each a two-layer chip whose label word is mandatory, for products
|
|
42
|
+
that file the reader's incoming mess into named categories);
|
|
40
43
|
otherwise follow the contract below (self-contained on purpose).
|
|
41
44
|
|
|
42
45
|
## Whether to animate at all — before how
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pkg": "@sheleg-design/pigeonhole",
|
|
3
|
+
"globalName": "ShelegPigeonhole",
|
|
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
|
+
# Pigeonhole — the design contract
|
|
2
|
+
|
|
3
|
+
The reference is <https://www.getinboxzero.com/>, measured 2026-08-12 off its
|
|
4
|
+
computed styles at 1440×900, 768×1024 and 390×844. The full pack is
|
|
5
|
+
`styles/pigeonhole.md`; this file is what a design agent must not get wrong.
|
|
6
|
+
|
|
7
|
+
## The one thing this pack is
|
|
8
|
+
|
|
9
|
+
**The label system is the design system.** A white wall, hairlines instead of
|
|
10
|
+
borders, a display face that never passes weight 400, and nine pastel categories in
|
|
11
|
+
which a hue *is* the category — each rendered as a two-layer chip: the deeper tint
|
|
12
|
+
pair outside at 8px, the paler pair inside at 7px, one pixel between them.
|
|
13
|
+
|
|
14
|
+
## Non-negotiable
|
|
15
|
+
|
|
16
|
+
- **A category chip always carries its word.** The component's type requires it.
|
|
17
|
+
Darkened to clear WCAG AA, the worst deuteranopic pair among the nine inks sits
|
|
18
|
+
**1.24 ΔE** apart — under the palette gate's hard floor of 10 — so hue is a
|
|
19
|
+
redundant channel and the label is the real one. Status, likewise, is never by
|
|
20
|
+
colour alone.
|
|
21
|
+
- **Weight 400 for the display, always.** Level 1 is
|
|
22
|
+
`clamp(34px, 5.6vw, 60px)` on line-height exactly 1. A 600 display is a
|
|
23
|
+
different pack.
|
|
24
|
+
- **One italic word per page**, inside the headline, same face, same weight.
|
|
25
|
+
- **The primary button's gradient runs light-to-dark.** White on the reference's
|
|
26
|
+
lower stop measures 3.29:1 and on its upper stop 5.04:1; the kit reverses the
|
|
27
|
+
ramp so the label's worst case passes.
|
|
28
|
+
- **`:focus-visible` is 2px `--accent-strong` at 2px offset.** The reference ships
|
|
29
|
+
`outline-style: none` on its focused primary CTA. Do not copy that.
|
|
30
|
+
- **A tinted card's shadow is tinted to its own hue**, never to black.
|
|
31
|
+
- **Elevation is a hairline.** One soft shadow (`--shadow-card`) is a hint, not a
|
|
32
|
+
hierarchy; `--shadow-hero` belongs to the product frame alone.
|
|
33
|
+
|
|
34
|
+
## Banned
|
|
35
|
+
|
|
36
|
+
- A chip with no label word. A hue-only category. A status told by colour alone.
|
|
37
|
+
- A bolder display, or a second accent hue.
|
|
38
|
+
- Any scroll clock: no scrubbing, no parallax, no sticky nav, no
|
|
39
|
+
`animation-timeline`. The reference has none of these, measured.
|
|
40
|
+
- **Rotation.** Zero rotated elements at three viewports. A scattered, tilted pile
|
|
41
|
+
is not this pack, however much a screenshot suggests it.
|
|
42
|
+
- `<details>` for an answer a crawler should read — the FAQ is a `<dl>`.
|
|
43
|
+
- A dark theme. None was measured, and inventing one invents values.
|
|
44
|
+
- Black shadows under tinted cards.
|
|
45
|
+
|
|
46
|
+
## Art direction, not components
|
|
47
|
+
|
|
48
|
+
The page's set pieces are **raster art** on the reference: the chaos-to-order
|
|
49
|
+
diptych at 1152×703 (≈1.64:1), the hero frame at 1150×631, the next section at
|
|
50
|
+
1152×539. Compose them as art with a subject, an aspect ratio and a treatment.
|
|
51
|
+
`LabelledRow` is the component; the pile is a picture.
|
|
52
|
+
|
|
53
|
+
## Motion
|
|
54
|
+
|
|
55
|
+
Entrance and hover only. The one motion worth copying is the headline's
|
|
56
|
+
**word-by-word blur-in** — each word its own element, entering on opacity and
|
|
57
|
+
`filter: blur()` at `--stagger-word`, settling at `blur(0px)`. `--ease-overshoot`
|
|
58
|
+
is spent on exactly one element, as the reference spends it. Everything collapses
|
|
59
|
+
under `prefers-reduced-motion`, which the reference honours globally.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @sheleg-design/pigeonhole
|
|
2
|
+
|
|
3
|
+
The React reference kit for the SHELEG **Pigeonhole** style pack — a white wall
|
|
4
|
+
ruled by hairlines, a display face that never passes weight 400, one italic word
|
|
5
|
+
in the headline, and nine pastel categories in which a hue *is* the category.
|
|
6
|
+
|
|
7
|
+
It is generated from the pack, not authored beside it: `src/styles.css` opens with
|
|
8
|
+
`styles/tokens/pigeonhole.css` byte for byte, and the rules the design agent must
|
|
9
|
+
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.
|
|
17
|
+
|
|
18
|
+
## The spine, and this pack's four
|
|
19
|
+
|
|
20
|
+
`Button`, `Card`, `Chip`, `Stat`, `Heading` and `Rule` are identical in name, props
|
|
21
|
+
and types across every SHELEG kit — switching packs swaps identity, not API.
|
|
22
|
+
|
|
23
|
+
The four that are this pack's own:
|
|
24
|
+
|
|
25
|
+
| Component | What it is |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `CategoryChip` | the two-layer chip — deeper tint outside at 8px, paler inside at 7px — and the **signature element**. Its label word is required by its type |
|
|
28
|
+
| `LabelledRow` | one row of the product, labelled: the atom the pack is built from |
|
|
29
|
+
| `WashCard` | a feature card in its category's palest pair, its shadow tinted to its own hue |
|
|
30
|
+
| `FaqList` | a `<dl>` whose answers are always visible, so a machine can quote them |
|
|
31
|
+
|
|
32
|
+
## Why the label word is a required prop
|
|
33
|
+
|
|
34
|
+
Nine category inks were measured on the reference and eight of them fail 4.5:1
|
|
35
|
+
against the very tints it paints them on — `#49d1fa` at 1.53:1 is the worst. The
|
|
36
|
+
kit ships derived inks that clear the floor, and derivation has a cost: pushing
|
|
37
|
+
lightness down compresses the hues in OKLab, so the worst deuteranopic pair
|
|
38
|
+
(Marketing against Notification) ends up **1.24 ΔE** apart, well under the palette
|
|
39
|
+
gate's hard floor of 10.
|
|
40
|
+
|
|
41
|
+
Nine hues cannot be simultaneously AA-compliant and mutually distinguishable to a
|
|
42
|
+
dichromatic reader — and they were not distinguishable before the derivation either:
|
|
43
|
+
the reference's own worst pair measures 4.42 ΔE against the same floor of 10. The
|
|
44
|
+
derivation makes it worse rather than causing it. So the hue is the second channel
|
|
45
|
+
and the word is the first, which is why `children` on `CategoryChip` is not
|
|
46
|
+
optional.
|
|
47
|
+
|
|
48
|
+
## What this kit deliberately does not contain
|
|
49
|
+
|
|
50
|
+
The page's set pieces are **raster art** on the reference — the chaos-to-order
|
|
51
|
+
diptych at 1152×703, the hero frame at 1150×631 — so they are art direction rather
|
|
52
|
+
than components. And nothing here rotates: the measured page has zero rotated
|
|
53
|
+
elements at three viewports.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sheleg-design/pigeonhole",
|
|
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,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Actions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The primary is the only accent fill on a view, and its gradient runs
|
|
6
|
+
**light-to-dark** — `--accent-2` at the top into `--accent` at the foot. The
|
|
7
|
+
reference runs it the other way, where its white label clears 5.04:1 against the
|
|
8
|
+
upper stop and 3.29:1 against the lower one; reversing the stops puts the label's
|
|
9
|
+
worst case on the passing colour and changes nothing a reader would notice.
|
|
10
|
+
|
|
11
|
+
Its shadow is tinted to the accent (`--shadow-cta`), never to black. `:hover`
|
|
12
|
+
lifts the shadow and translates 1px; `:focus-visible` draws a 2px
|
|
13
|
+
`--accent-strong` ring at 2px offset — the reference computes `outline-style:
|
|
14
|
+
none` here and adds no compensating shadow, so this ring is the kit's correction
|
|
15
|
+
rather than its copy.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Button>Get started</Button>
|
|
19
|
+
<Button variant="secondary">Talk to sales</Button>
|
|
20
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ButtonProps {
|
|
4
|
+
/** `primary` is the accent fill — at most one 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={['pg-btn', `pg-btn--${variant}`, `pg-btn--${size}`, className]
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join(' ')}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The neutral panel: `--surface` at `--radius-panel` (32px), a `--rule` hairline,
|
|
6
|
+
and the one soft shadow this pack spends on 40 elements. Elevation here is a
|
|
7
|
+
hairline, not a stack — the seven-layer framing shadow belongs to `showroom`.
|
|
8
|
+
|
|
9
|
+
For a feature card that belongs to a category, use `WashCard`: it fills with that
|
|
10
|
+
hue's palest pair and tints its own shadow to match.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<Card title="Bulk unsubscribe" meta="2 min">
|
|
14
|
+
Every list you never read, in one screen.
|
|
15
|
+
</Card>
|
|
16
|
+
```
|
|
@@ -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={['pg-card', className].filter(Boolean).join(' ')}>
|
|
15
|
+
{head && (
|
|
16
|
+
<div className="pg-card__head">
|
|
17
|
+
{title !== undefined && <h3 className="pg-card__title">{title}</h3>}
|
|
18
|
+
{meta !== undefined && <span className="pg-card__meta">{meta}</span>}
|
|
19
|
+
</div>
|
|
20
|
+
)}
|
|
21
|
+
<div className="pg-card__body">{children}</div>
|
|
22
|
+
</section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Signature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
**The pack's signature element.** Two nested chips: the outer carries the deeper
|
|
6
|
+
tint pair at `--radius-chip` (8px), the inner the paler pair at
|
|
7
|
+
`--radius-chip-inner` (7px) with 1px of padding between them — exactly the step
|
|
8
|
+
the reference uses, and the one place in it where radius-by-subtraction happens
|
|
9
|
+
to hold.
|
|
10
|
+
|
|
11
|
+
`children` is **required**, and that is a design decision enforced by the type.
|
|
12
|
+
Neither set of inks clears the bar: as the reference paints them, the worst
|
|
13
|
+
deuteranopic pair (Marketing against Notification) is **4.42 ΔE** against the
|
|
14
|
+
palette gate's hard floor of 10, so these hues were never distinguishable to that
|
|
15
|
+
reader. Deriving them to clear WCAG AA makes it worse rather than causing it — the
|
|
16
|
+
same pair falls to **1.24**. Nine hues cannot be simultaneously AA-compliant and
|
|
17
|
+
mutually distinguishable to a dichromatic reader, so the word carries the category
|
|
18
|
+
and the hue reinforces it. A chip without its label is not a quieter chip, it is an
|
|
19
|
+
unreadable one.
|
|
20
|
+
|
|
21
|
+
Each ink clears 4.5:1 against the deepest tint of its own ramp. All nine are
|
|
22
|
+
derived from the reference's values, which fail that floor in eight cases out of
|
|
23
|
+
nine; the numbers are in the pack's Gotchas.
|
|
24
|
+
|
|
25
|
+
## Selected, for categories that are filters
|
|
26
|
+
|
|
27
|
+
A chip is a label, so it has no pressed state — but where the product's categories
|
|
28
|
+
*are* filters, the reader needs to know which one is on. Add `pg-cat--selected` (or
|
|
29
|
+
`aria-pressed="true"`, which the stylesheet matches): the inner layer takes its
|
|
30
|
+
category's `-150` step instead of `-50` and the ink marks a 2px inset. That inset is
|
|
31
|
+
the one border a chip may carry.
|
|
32
|
+
|
|
33
|
+
## Two of the nine do not render two layers
|
|
34
|
+
|
|
35
|
+
`--cat-cold-150` and `--cat-cold-100` are the same measured literal, and
|
|
36
|
+
`--cat-step-50` is the page white — so on Cold Email and on STEP the 8px→7px step
|
|
37
|
+
is invisible against `--surface`. Both are faithful to the reference. Where those
|
|
38
|
+
two must read as two layers, put them on `--surface-2`.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<CategoryChip category="reply">To Reply</CategoryChip>
|
|
42
|
+
<CategoryChip category="newsletter">Newsletter</CategoryChip>
|
|
43
|
+
<CategoryChip category="marketing" className="pg-cat--selected">Marketing</CategoryChip>
|
|
44
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** The nine categories measured on the reference. `step` is the neutral. */
|
|
4
|
+
export type Category =
|
|
5
|
+
| 'reply'
|
|
6
|
+
| 'newsletter'
|
|
7
|
+
| 'marketing'
|
|
8
|
+
| 'calendar'
|
|
9
|
+
| 'notification'
|
|
10
|
+
| 'cold'
|
|
11
|
+
| 'team'
|
|
12
|
+
| 'urgent'
|
|
13
|
+
| 'step';
|
|
14
|
+
|
|
15
|
+
export interface CategoryChipProps {
|
|
16
|
+
category: Category;
|
|
17
|
+
/**
|
|
18
|
+
* The label word, and it is required rather than optional on purpose: the hue
|
|
19
|
+
* alone cannot carry the category. The reference's own inks leave the worst
|
|
20
|
+
* deuteranopic pair 4.42 ΔE apart against a floor of 10, and deriving them to
|
|
21
|
+
* clear WCAG AA drops that pair to 1.24 — so a reader with deuteranopia cannot
|
|
22
|
+
* tell Marketing from Notification by colour either way. The word is the
|
|
23
|
+
* channel; the hue reinforces it.
|
|
24
|
+
*/
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The pack's signature element: two nested chips. The outer carries the deeper
|
|
31
|
+
* tint pair at radius 8px, the inner the paler pair at 7px, one pixel inside its
|
|
32
|
+
* parent — the step the reference itself uses.
|
|
33
|
+
*/
|
|
34
|
+
export function CategoryChip({ category, children, className }: CategoryChipProps) {
|
|
35
|
+
return (
|
|
36
|
+
<span
|
|
37
|
+
className={['pg-cat', `pg-cat--${category}`, className].filter(Boolean).join(' ')}
|
|
38
|
+
>
|
|
39
|
+
<span className="pg-cat__inner">{children}</span>
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Data
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The generic tag. For a **category**, use `CategoryChip` instead — that one is the
|
|
6
|
+
two-layer construction this pack is remembered by, and its label word is required
|
|
7
|
+
by its type.
|
|
8
|
+
|
|
9
|
+
`neutral` is `--surface-2` under `--ink-soft`. `accent` is `--accent-wash` under
|
|
10
|
+
`--accent-strong` at 5.04:1 on the field. `selected` adds an `--accent-edge`
|
|
11
|
+
border rather than a second fill.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Chip>Open source</Chip>
|
|
15
|
+
<Chip tone="accent" selected>Free for 7 days</Chip>
|
|
16
|
+
```
|
|
@@ -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
|
+
'pg-chip',
|
|
15
|
+
`pg-chip--${tone}`,
|
|
16
|
+
selected ? 'pg-chip--selected' : undefined,
|
|
17
|
+
className,
|
|
18
|
+
]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(' ')}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Data
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A `<dl>`, always open. There is no `collapsed` prop on purpose: the reference
|
|
6
|
+
ships seven `dt`/`dd` pairs in served HTML and no `<details>`, and an answer a
|
|
7
|
+
crawler cannot read without running JavaScript is an answer that is not there.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<FaqList entries={[{ q: 'Which providers?', a: 'Gmail and Outlook.' }]} />
|
|
11
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FaqEntry {
|
|
4
|
+
q: string;
|
|
5
|
+
a: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FaqListProps {
|
|
9
|
+
entries: FaqEntry[];
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A definition list, always open. There is deliberately no `collapsed` prop: the
|
|
15
|
+
* reference ships seven `dt`/`dd` pairs in served HTML and no `<details>`, and an
|
|
16
|
+
* answer a crawler cannot read without running JavaScript is an answer that is
|
|
17
|
+
* not there.
|
|
18
|
+
*/
|
|
19
|
+
export function FaqList({ entries, className }: FaqListProps) {
|
|
20
|
+
return (
|
|
21
|
+
<dl className={['pg-faq', className].filter(Boolean).join(' ')}>
|
|
22
|
+
{entries.map(({ q, a }) => (
|
|
23
|
+
<div className="pg-faq__row" key={q}>
|
|
24
|
+
<dt className="pg-faq__q">{q}</dt>
|
|
25
|
+
<dd className="pg-faq__a">{a}</dd>
|
|
26
|
+
</div>
|
|
27
|
+
))}
|
|
28
|
+
</dl>
|
|
29
|
+
);
|
|
30
|
+
}
|