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.
Files changed (48) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +5 -4
  3. package/bin/cli.js +5 -3
  4. package/cursor/rules/sheleg-design.mdc +4 -1
  5. package/kits/pigeonhole/.design-sync/config.json +14 -0
  6. package/kits/pigeonhole/.design-sync/conventions.md +59 -0
  7. package/kits/pigeonhole/README.md +53 -0
  8. package/kits/pigeonhole/package.json +29 -0
  9. package/kits/pigeonhole/src/Button.md +20 -0
  10. package/kits/pigeonhole/src/Button.tsx +33 -0
  11. package/kits/pigeonhole/src/Card.md +16 -0
  12. package/kits/pigeonhole/src/Card.tsx +24 -0
  13. package/kits/pigeonhole/src/CategoryChip.md +44 -0
  14. package/kits/pigeonhole/src/CategoryChip.tsx +42 -0
  15. package/kits/pigeonhole/src/Chip.md +16 -0
  16. package/kits/pigeonhole/src/Chip.tsx +25 -0
  17. package/kits/pigeonhole/src/FaqList.md +11 -0
  18. package/kits/pigeonhole/src/FaqList.tsx +30 -0
  19. package/kits/pigeonhole/src/Heading.md +17 -0
  20. package/kits/pigeonhole/src/Heading.tsx +19 -0
  21. package/kits/pigeonhole/src/LabelledRow.md +27 -0
  22. package/kits/pigeonhole/src/LabelledRow.tsx +54 -0
  23. package/kits/pigeonhole/src/Rule.md +12 -0
  24. package/kits/pigeonhole/src/Rule.tsx +18 -0
  25. package/kits/pigeonhole/src/Stat.md +11 -0
  26. package/kits/pigeonhole/src/Stat.tsx +17 -0
  27. package/kits/pigeonhole/src/WashCard.md +17 -0
  28. package/kits/pigeonhole/src/WashCard.tsx +27 -0
  29. package/kits/pigeonhole/src/index.ts +23 -0
  30. package/kits/pigeonhole/src/styles.css +860 -0
  31. package/kits/pigeonhole/tsconfig.json +15 -0
  32. package/kits/showroom/src/styles.css +11 -0
  33. package/package.json +2 -2
  34. package/plugins/sheleg-design/.claude-plugin/plugin.json +2 -2
  35. package/plugins/sheleg-design/commands/sheleg-design.md +1 -1
  36. package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +1 -1
  37. package/plugins/sheleg-design/skills/sheleg-design/MOBILE_SURFACES.md +1 -1
  38. package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +8 -5
  39. package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +3 -3
  40. package/plugins/sheleg-design/skills/sheleg-design/styles/STYLE_PACK_TEMPLATE.md +30 -3
  41. package/plugins/sheleg-design/skills/sheleg-design/styles/cyclorama.md +7 -0
  42. package/plugins/sheleg-design/skills/sheleg-design/styles/manpage.md +4 -1
  43. package/plugins/sheleg-design/skills/sheleg-design/styles/orchard.md +7 -0
  44. package/plugins/sheleg-design/skills/sheleg-design/styles/pigeonhole.md +503 -0
  45. package/plugins/sheleg-design/skills/sheleg-design/styles/showroom.md +10 -1
  46. package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/pigeonhole.css +327 -0
  47. package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/showroom.css +11 -0
  48. 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 **fifteen locked style
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 fifteen style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
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 fifteen kits, so switching packs swaps identity rather than API,
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 fifteen
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/ fifteen style packs — instrument-console (dark console),
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
+ }