sheleg-design-skill 1.42.0 → 1.44.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 +107 -0
- package/README.md +6 -5
- package/bin/cli.js +5 -2
- package/cursor/rules/sheleg-design.mdc +5 -1
- package/kits/bulletin/.design-sync/config.json +14 -0
- package/kits/bulletin/.design-sync/conventions.md +55 -0
- package/kits/bulletin/README.md +38 -0
- package/kits/bulletin/package.json +29 -0
- package/kits/bulletin/src/Band.md +17 -0
- package/kits/bulletin/src/Band.tsx +19 -0
- package/kits/bulletin/src/Button.md +20 -0
- package/kits/bulletin/src/Button.tsx +33 -0
- package/kits/bulletin/src/Card.md +18 -0
- package/kits/bulletin/src/Card.tsx +24 -0
- package/kits/bulletin/src/Chip.md +16 -0
- package/kits/bulletin/src/Chip.tsx +25 -0
- package/kits/bulletin/src/Heading.md +17 -0
- package/kits/bulletin/src/Heading.tsx +19 -0
- package/kits/bulletin/src/Panel.md +15 -0
- package/kits/bulletin/src/Panel.tsx +11 -0
- package/kits/bulletin/src/Rail.md +16 -0
- package/kits/bulletin/src/Rail.tsx +28 -0
- package/kits/bulletin/src/Rule.md +15 -0
- package/kits/bulletin/src/Rule.tsx +18 -0
- package/kits/bulletin/src/Skeleton.md +15 -0
- package/kits/bulletin/src/Skeleton.tsx +19 -0
- package/kits/bulletin/src/Stat.md +15 -0
- package/kits/bulletin/src/Stat.tsx +17 -0
- package/kits/bulletin/src/StatusDot.md +17 -0
- package/kits/bulletin/src/StatusDot.tsx +17 -0
- package/kits/bulletin/src/index.ts +25 -0
- package/kits/bulletin/src/styles.css +671 -0
- package/kits/bulletin/tsconfig.json +15 -0
- package/kits/editorial-luxury/src/styles.css +26 -0
- package/kits/instrument-console/src/styles.css +33 -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 +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/MOTION_DOCTRINE.md +6 -2
- package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +5 -4
- package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +4 -4
- package/plugins/sheleg-design/skills/sheleg-design/styles/bulletin.md +355 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/instrument-console.md +11 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/orchard.md +6 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/pigeonhole.md +4 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/bulletin.css +279 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/editorial-luxury.css +26 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/instrument-console.css +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,113 @@ 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.44.0] - 2026-08-19
|
|
8
|
+
|
|
9
|
+
### Degrade to calm gained an observable
|
|
10
|
+
|
|
11
|
+
The promise had none. `sloplint.py` asserted only that the **string**
|
|
12
|
+
`prefers-reduced-motion` appeared in the doctrine — that the rule was mentioned, not that any
|
|
13
|
+
shipped artifact obeyed it. Measured: **2 of 29 token layers shipped no reduce branch**,
|
|
14
|
+
including the pack that mandates a particle field.
|
|
15
|
+
|
|
16
|
+
`validate_reduced_motion()` asserts two things per layer: a layer declaring `--dur-*` or
|
|
17
|
+
`--ease-*` ships a `prefers-reduced-motion` branch, **and that branch collapses at least one
|
|
18
|
+
time-valued property to ≤1ms**. The second half is the row — doctrine §9 says motion collapses
|
|
19
|
+
"to static or instant, not to slower", so a branch that merely names the query satisfies a grep
|
|
20
|
+
and changes nothing a reader can feel. Durations are matched **by value, not by name**, so
|
|
21
|
+
`paperclip`'s `--t-*` and `instrument-console`'s `--motion-ease` are not read as empty.
|
|
22
|
+
|
|
23
|
+
`editorial-luxury` and `instrument-console` fixed. Where a motif cannot be reached from a custom
|
|
24
|
+
property — a rAF particle field is not stoppable by a duration — the still is now stated for each:
|
|
25
|
+
field to one static frame, spotlight to every section lit, instruments fully drawn. 29 of 29
|
|
26
|
+
layers now carry a branch that bites. B-040 closed.
|
|
27
|
+
|
|
28
|
+
### A count hyphenated onto its noun was invisible to the gate that counts it
|
|
29
|
+
|
|
30
|
+
`docs/DOCMAP.md` said "a fourteen-kit build matrix" against **29** kits, and the counted-claims
|
|
31
|
+
guard ran green over that string for weeks: `COUNTED` never matched a number spelled as a word and
|
|
32
|
+
joined to its noun by a hyphen. A contributor had already routed around the hole rather than
|
|
33
|
+
report it.
|
|
34
|
+
|
|
35
|
+
The separator is now `[ -]`, **captured and back-referenced**, so a compound is read in one
|
|
36
|
+
alphabet or the other and a half-and-half form is not silently accepted; the plural is required
|
|
37
|
+
for the spaced form only, because a hyphenated count is a singular modifier by grammar.
|
|
38
|
+
Blast radius measured old-pattern-against-new on one tree: the policed set 34 → 36 spans with
|
|
39
|
+
**zero false positives**, the whole tree 333 → 384.
|
|
40
|
+
|
|
41
|
+
The coordination claim is **derived** rather than reworded — DOCMAP now points at the generated
|
|
42
|
+
`AGENT_SYNC.md`, and a new check holds the derivation from both ends so the page, the config and
|
|
43
|
+
the document cannot disagree.
|
|
44
|
+
|
|
45
|
+
### Plants
|
|
46
|
+
|
|
47
|
+
Self-test 16 → 19, each asserting its **message** rather than merely the red — every token layer
|
|
48
|
+
is copied into its kit byte for byte, so any edit trips the kit-drift check and a plant would
|
|
49
|
+
otherwise report "caught" with the intended check never having run.
|
|
50
|
+
|
|
51
|
+
## [1.43.0] - 2026-08-17
|
|
52
|
+
|
|
53
|
+
**The twenty-ninth pack, and the first one whose signature is a defect its reference
|
|
54
|
+
ships.** `bulletin` is extracted from [socialchamp.com](https://www.socialchamp.com) by
|
|
55
|
+
enumerating all 748 URLs in its page sitemap, fetching every one, and reading the 58
|
|
56
|
+
distinct stylesheets they resolve to plus the shared layer its theme ships. What it
|
|
57
|
+
takes is the *drawn* elevation — a 1px ink outline over a hard zero-blur ink offset, 185
|
|
58
|
+
of them against roughly 50 blurred shadows across the whole site — and the press that
|
|
59
|
+
goes with it: a control travels exactly as far as its offset shrinks, so the ink
|
|
60
|
+
displaced is constant.
|
|
61
|
+
|
|
62
|
+
The reference's primary CTA is white on `#ff6900` at 2.89:1, on every one of those 748
|
|
63
|
+
pages. That is under WCAG AA for body text and under the large-text floor as well, so no
|
|
64
|
+
type size rescues it; the pack keeps the measured hue and darkens it in oklab until white
|
|
65
|
+
clears AA. Two more corrections travel with it, and all three are in the pack's Gotchas
|
|
66
|
+
with their numbers.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- **`bulletin`**, from [socialchamp.com](https://www.socialchamp.com) — warm cream paper
|
|
71
|
+
(`#fcfaf4`) cut by three flat pastel bands, one ink (`#464646`) doing four jobs at once
|
|
72
|
+
(text, outline, offset and the dark band), an orange that fills and marks but never
|
|
73
|
+
carries a word, a display face at **800 inside controls above the headline's 700**, and
|
|
74
|
+
**zero tracking at every size** — `letter-spacing` appears seven times in 58
|
|
75
|
+
stylesheets and never on a heading. Widened contract, all thirteen headings.
|
|
76
|
+
- **`styles/tokens/bulletin.css`** — the ready-made token layer, with a
|
|
77
|
+
`[data-surface="ink"]` block for the dark band measured off the reference's own dark
|
|
78
|
+
footer, where the outline and the offset both invert to white. Every value is marked
|
|
79
|
+
MEASURED, SELECTED or DERIVED at its declaration.
|
|
80
|
+
- **`kits/bulletin`** — the twenty-ninth reference kit: the six-name spine plus
|
|
81
|
+
`StatusDot`, `Rail` (the row of outlined circles that says *many*), `Band`, `Panel` and
|
|
82
|
+
`Skeleton`. Its `styles.css` opens with the token layer byte for byte.
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- **`MOTION_DOCTRINE.md` — a fourth standalone pack pins its own ceiling.** `bulletin`
|
|
87
|
+
caps `MOTION_INTENSITY` at **3**, the lowest in the library, because the reference's
|
|
88
|
+
entire measured motion budget across 748 pages is an entrance fade, a 0.12s press and a
|
|
89
|
+
0.3s hover: no scroll clock, no parallax, no scrub, no pinning. Its depth is drawn
|
|
90
|
+
rather than animated, and animating the offset is what flattens it.
|
|
91
|
+
- **Every count that names the library moves to twenty-nine** — the pack tables in
|
|
92
|
+
`SKILL.md` and `README.md`, both manifests, the CLI banner, the Cursor rule, the slash
|
|
93
|
+
command's by-name fast path, and the core-contract remainder (seven core, twenty-two
|
|
94
|
+
widened).
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **The rail collapsed to a column, and only rendering it showed that.**
|
|
99
|
+
`container-type: inline-size` applies inline-size containment, so an element stops
|
|
100
|
+
taking its width from its contents — on a shrink-to-fit box (a grid item under
|
|
101
|
+
`justify-items: center`, a flex item, an inline-block) `.bl-rail` went to near-zero
|
|
102
|
+
width, stacked one circle per line and fired its own narrow branch, hiding every
|
|
103
|
+
platform name. All three gates were green over it, because a gate reads structure and
|
|
104
|
+
not layout. `width: 100%` is the fix and the reason is now a comment beside it.
|
|
105
|
+
Rendered and confirmed at 1440×1000, including the press: the primary's offset goes
|
|
106
|
+
3px → 1px as the control travels 2px, while the secondary beside it keeps its 3px.
|
|
107
|
+
- **Three stale counts in `SURFACE_COMPOSITION.md` that no gate could see.** It said the
|
|
108
|
+
token names were not uniform "across the twenty-one" when twenty-nine packs ship, and
|
|
109
|
+
that the accent is `--accent` "in eighteen" when it is twenty-seven — `--brand` in
|
|
110
|
+
`field-notes` and `--cta` in `orchard` are still the only two exceptions, counted. The
|
|
111
|
+
third was a count of packs carrying an `@role non-text:` colour, which is now stated as
|
|
112
|
+
sixteen token layers because that is what the grep returns.
|
|
113
|
+
|
|
7
114
|
## [1.42.0] - 2026-08-17
|
|
8
115
|
|
|
9
116
|
**The twenty-eighth pack, and the first one measured for its tempo rather than its
|
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 **twenty-
|
|
14
|
+
decides whether to animate before it decides how, and **twenty-nine 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
|
|
|
@@ -72,6 +72,7 @@ into the cinematic layer, and says so in its own *Motion flavor* section.
|
|
|
72
72
|
| `almanac` | **oatmeal paper rather than white**, seams at 2px and 4px with **no 1px anywhere**, a 104px display at weight 500 with a line-height below one that locks its lines into a block, uppercase mono tags notched through the edges of drawn boxes, and one object per page floating on four stacked shadow stops | **pages that assert a category** — a manifesto, a company saying what this kind of thing is, a product whose argument is editorial rather than functional (standalone) |
|
|
73
73
|
| `vitrine` | a white field drawn **entirely in hairlines**, a serif display over a sans body, an ink primary so the accent stays free to mark what can be read, a grey panel that groups without lifting, and **one framed record** with a 1px inset highlight carrying the page's evidence | **the front door of a product sold on trust** — B2B software under evaluation, security and compliance surfaces, specification and comparison pages (standalone) |
|
|
74
74
|
| `proscenium` | a white field carrying two cool acts and **one deep indigo act at the middle**, ink that is an indigo rather than a grey, an electric violet that fills a control staying **nearly square at 4px** against cards at 16, one family at nine weights, and a framed product panel the fold cuts off | **product-led marketing front doors whose argument is a demonstration** — SaaS home pages, launch and tour pages, any page with six or more acts that needs a repeated beat (standalone) |
|
|
75
|
+
| `bulletin` | warm cream paper cut by **flat pastel bands**, every card and control a 1px ink outline standing on a **hard zero-blur ink offset it travels into when pressed** — 185 of them against 50 blurred shadows in the reference — a display face at 800 inside controls above the headline's 700, and **no tracking at any size** | **front doors whose argument is breadth** — a tool doing many things across many channels for many clients, sold cheerfully to a small team or an agency: social and content platforms, scheduling and inbox products, all-in-one SMB SaaS (standalone) |
|
|
75
76
|
| `ledger` | warm cream paper where elevation is a **1px hairline at 12% ink** and no card casts a shadow, radii of 7.5/10/15/20 nested concentrically, an **ink** primary button, and a terracotta accent forbidden from filling any control — it labels, as a 10px monospace uppercase kicker — over 32px data rows, with a seal on every card stating how its number is known | the console of a product that answers questions **about data** — AI analysts, BI surfaces, query workspaces, agents that read a warehouse and write back a figure |
|
|
76
77
|
|
|
77
78
|
Each pack locks palette, type, texture, motion tokens, signature motifs and
|
|
@@ -147,7 +148,7 @@ skills.
|
|
|
147
148
|
| `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 |
|
|
148
149
|
| `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 |
|
|
149
150
|
| `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 |
|
|
150
|
-
| `styles/*.md` | The twenty-
|
|
151
|
+
| `styles/*.md` | The twenty-nine style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
|
|
151
152
|
| `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) |
|
|
152
153
|
| `styles/STYLE_PACK_TEMPLATE.md` | The pack contract as a skeleton, so a new style is authored against the same headings rather than improvised |
|
|
153
154
|
|
|
@@ -218,7 +219,7 @@ cd ./ds-workbench && npm install && npm run build
|
|
|
218
219
|
then `/design-sync` in that directory, from Claude Code. Three layers cross: the
|
|
219
220
|
pack's **bans** as the design system's own README, `styles.css` built from
|
|
220
221
|
`tokens/<pack>.css` verbatim, and the components — a six-name spine that is
|
|
221
|
-
identical in all twenty-
|
|
222
|
+
identical in all twenty-nine kits, so switching packs swaps identity rather than API,
|
|
222
223
|
plus each pack's signature parts. **Motion does not cross**, exactly as it does
|
|
223
224
|
not cross into Figma: a kit is the static half of a pack, and saying so is what
|
|
224
225
|
stops an agent inventing motion to fill the silence.
|
|
@@ -264,10 +265,10 @@ a pack's four widened sections used to make two gates *quieter* and still green.
|
|
|
264
265
|
One honest limit: the npx installer is checked by asserting its runtime bundle
|
|
265
266
|
walker exists, not by reading a file list — it has none by design. What proves
|
|
266
267
|
it ships the right files is CI, which installs the bundle through **both**
|
|
267
|
-
installers and `diff -r`s the result against the source, then builds all twenty-
|
|
268
|
+
installers and `diff -r`s the result against the source, then builds all twenty-nine
|
|
268
269
|
kits.
|
|
269
270
|
|
|
270
|
-
`test/scenarios.md` (T1–
|
|
271
|
+
`test/scenarios.md` (T1–T30) is the behavioral harness: fresh subagents given a
|
|
271
272
|
task, checking that the skill is discovered, applied and quoted correctly.
|
|
272
273
|
Re-run the affected scenarios after any edit to `SKILL.md`, a pack or the
|
|
273
274
|
reference.
|
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/ twenty-
|
|
237
|
+
styles/ twenty-nine 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
|
|
@@ -280,7 +280,10 @@ ${c("bold", "What it installs")}
|
|
|
280
280
|
proscenium (a white field with two cool acts and one deep
|
|
281
281
|
indigo act at the middle, a violet filling a control that
|
|
282
282
|
stays nearly square at 4px against cards at 16, and a
|
|
283
|
-
framed product panel the fold cuts off)
|
|
283
|
+
framed product panel the fold cuts off),
|
|
284
|
+
bulletin (warm cream paper cut by flat pastel bands,
|
|
285
|
+
every card and control a 1px ink outline on a hard
|
|
286
|
+
zero-blur ink offset it travels into when pressed) —
|
|
284
287
|
plus a ready-made token CSS per pack and
|
|
285
288
|
STYLE_PACK_TEMPLATE.md for authoring more
|
|
286
289
|
`);
|
|
@@ -91,7 +91,11 @@ proscenium — a white field carrying two cool acts and one deep indigo act at t
|
|
|
91
91
|
middle, an electric violet filling a control that stays nearly square at 4px
|
|
92
92
|
against cards at 16, one family at nine weights, and a framed product panel the
|
|
93
93
|
fold cuts off, for product-led marketing front doors whose argument is a
|
|
94
|
-
demonstration
|
|
94
|
+
demonstration;
|
|
95
|
+
bulletin — warm cream paper cut by flat pastel bands, every card and control a
|
|
96
|
+
1px ink outline standing on a hard zero-blur ink offset it travels into when
|
|
97
|
+
pressed, a display face at 800 inside controls and 700 in the headline, and no
|
|
98
|
+
tracking at any size, for front doors whose argument is breadth);
|
|
95
99
|
otherwise follow the contract below (self-contained on purpose).
|
|
96
100
|
|
|
97
101
|
## Whether to animate at all — before how
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pkg": "@sheleg-design/bulletin",
|
|
3
|
+
"globalName": "ShelegBulletin",
|
|
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,55 @@
|
|
|
1
|
+
# Bulletin — the contract this design system ships under
|
|
2
|
+
|
|
3
|
+
**Register.** Choose Bulletin for **front doors whose argument is breadth**: a
|
|
4
|
+
tool that does many things across many channels for many clients, sold
|
|
5
|
+
cheerfully to a small team or an agency — social and content platforms,
|
|
6
|
+
scheduling and inbox products, all-in-one SMB SaaS, marketplaces of small
|
|
7
|
+
features. Warm cream paper, flat pastel bands, and every object drawn rather
|
|
8
|
+
than tinted. Build every screen against `var(--…)` and never a literal.
|
|
9
|
+
|
|
10
|
+
**Elevation is a hard offset and it never blurs.** A 1px `--line` outline plus a
|
|
11
|
+
zero-blur ink shadow — `--shadow-1` 2px for a chip, `--shadow-2` 3px for a
|
|
12
|
+
control, `--shadow-3` 4px for a card, `--shadow-4` for the one framed panel per
|
|
13
|
+
page. One soft shadow anywhere and the pack is gone. An offset without an
|
|
14
|
+
outline reads as a rendering fault.
|
|
15
|
+
|
|
16
|
+
**The press.** A control translates by `--press-travel` exactly as its offset
|
|
17
|
+
shrinks to `--shadow-press`, so the ink displaced stays constant; `:active`
|
|
18
|
+
travels one more pixel and the offset goes to nothing. Hover and
|
|
19
|
+
`:focus-visible` share the rule, so a keyboard user sees it. A **surface** does
|
|
20
|
+
the opposite: its offset grows to `--shadow-3-wide`. One move per kind of
|
|
21
|
+
object.
|
|
22
|
+
|
|
23
|
+
**The accent rule.** There is exactly one accent (`--accent`) and it is a fill
|
|
24
|
+
and a mark, never a word — at 2.77:1 on the paper it sits below the non-text
|
|
25
|
+
floor. The filled control uses `--action`, a derived value: the reference sets
|
|
26
|
+
white on the measured orange at 2.89:1, which is under AA and under the
|
|
27
|
+
large-text floor, so no type size rescues it. Links use `--link` with an
|
|
28
|
+
underline. At most one accent fill per view.
|
|
29
|
+
|
|
30
|
+
**Status is never by colour alone.** `--good` / `--warn` / `--danger` /
|
|
31
|
+
`--info` are state, never decoration, and every one of them ships beside a word.
|
|
32
|
+
Under deuteranopia `--danger` and `--warn` separate by 0.7; the word is the
|
|
33
|
+
message and the colour reinforces it. The `[data-surface="ink"]` block remaps
|
|
34
|
+
all four — a theme that remaps its ink and leaves its statuses behind paints
|
|
35
|
+
1.3–2.0:1.
|
|
36
|
+
|
|
37
|
+
**Type.** Two families: Bricolage Grotesque for display, DM Sans for body. The
|
|
38
|
+
control label is **heavier than the headline** — 800 against 700 — and that is
|
|
39
|
+
measured, not a flourish. **Tracking is zero at every size.**
|
|
40
|
+
|
|
41
|
+
**Bans** (verbatim from the pack):
|
|
42
|
+
|
|
43
|
+
- No blur on any elevation. No offset without an outline.
|
|
44
|
+
- No second orange fill in one viewport; no accent as a word.
|
|
45
|
+
- No tracking, anywhere.
|
|
46
|
+
- No gradient in a band — the bands are flat.
|
|
47
|
+
- No status by colour alone.
|
|
48
|
+
- No motion beyond the press and a fade: no parallax, no scrub, no sticky
|
|
49
|
+
choreography, no marquee.
|
|
50
|
+
- No white page. White is a card; the field is `--bg`.
|
|
51
|
+
|
|
52
|
+
**Motion does not cross.** A kit is the static half of a pack. The press is
|
|
53
|
+
specified in `styles.css` and described here; nothing else in this package
|
|
54
|
+
animates, and `prefers-reduced-motion` collapses every duration and both travel
|
|
55
|
+
distances to zero at the token layer.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @sheleg-design/bulletin
|
|
2
|
+
|
|
3
|
+
The React reference kit for the SHELEG **Bulletin** style pack — warm cream
|
|
4
|
+
paper cut by flat pastel bands, where every card and control is a 1px ink
|
|
5
|
+
outline standing on a hard zero-blur ink offset it travels into when pressed.
|
|
6
|
+
|
|
7
|
+
It is generated from the pack, not authored beside it: `src/styles.css` opens
|
|
8
|
+
with `styles/tokens/bulletin.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
|
+
## The spine
|
|
16
|
+
|
|
17
|
+
`Button`, `Card`, `Chip`, `Stat`, `Heading`, `Rule` — identical names, props and
|
|
18
|
+
types in every SHELEG kit, so switching packs swaps identity rather than API.
|
|
19
|
+
|
|
20
|
+
## This pack's own
|
|
21
|
+
|
|
22
|
+
`StatusDot` (the word is required, never the colour alone), `Rail` (the row of
|
|
23
|
+
outlined circles that says *many*), `Band` (one flat pastel per act, and the
|
|
24
|
+
`ink` tone that flips the whole block to the dark register), `Panel` (one per
|
|
25
|
+
page, the widest offset in the ramp) and `Skeleton`.
|
|
26
|
+
|
|
27
|
+
## The two rules that carry the kit
|
|
28
|
+
|
|
29
|
+
1. **An outline and an offset always travel together.** A hard shadow under an
|
|
30
|
+
edgeless box reads as a rendering fault rather than as depth.
|
|
31
|
+
2. **A control presses into its offset; a surface grows out of it.** One move
|
|
32
|
+
per kind of object. Give a card the button's press and the grid stops reading
|
|
33
|
+
as a set of objects; give a button the card's growth and the click has no
|
|
34
|
+
feedback.
|
|
35
|
+
|
|
36
|
+
**Motion does not cross into a design tool.** A kit is the static half of a
|
|
37
|
+
pack: the press is described here and in the pack, and it is implemented in
|
|
38
|
+
`styles.css`, but nothing in this package animates on its own.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sheleg-design/bulletin",
|
|
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,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
One flat pastel per act, full-bleed, used to **change subject** rather than to
|
|
6
|
+
decorate. The ink clears AA on every band — 5.76:1 on the lilac, 6.53 on the
|
|
7
|
+
sky, 8.36 on the peach — which is why the bands can be saturated without a
|
|
8
|
+
second text colour.
|
|
9
|
+
|
|
10
|
+
`ink` switches the whole band to the dark register by setting
|
|
11
|
+
`data-surface="ink"`, where the outline and the offset both invert to white. A
|
|
12
|
+
gradient in a band is banned: the reference paints them flat, and a gradient
|
|
13
|
+
reads as a different product.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Band tone="sky"><Heading level={2}>One inbox for every client</Heading></Band>
|
|
17
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface BandProps {
|
|
4
|
+
/** One flat pastel per act. The pack forbids a gradient here. */
|
|
5
|
+
tone?: 'paper' | 'peach' | 'sky' | 'lilac' | 'lemon' | 'ink';
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Band({ tone = 'paper', children, className }: BandProps) {
|
|
11
|
+
return (
|
|
12
|
+
<section
|
|
13
|
+
className={['bl-band', `bl-band--${tone}`, className].filter(Boolean).join(' ')}
|
|
14
|
+
data-surface={tone === 'ink' ? 'ink' : undefined}
|
|
15
|
+
>
|
|
16
|
+
<div className="bl-band__inner">{children}</div>
|
|
17
|
+
</section>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Actions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The press is this pack's signature, and it lives here. A button stands `3px`
|
|
6
|
+
proud of the paper on a hard zero-blur ink offset; hover translates it `2px` and
|
|
7
|
+
shrinks the offset to `1px`; `:active` translates `3px` and removes the offset
|
|
8
|
+
entirely — the control has bottomed out. The ink displaced is constant at every
|
|
9
|
+
step, which is why it reads as movement rather than as a restyle.
|
|
10
|
+
|
|
11
|
+
`primary` fills with `--action`, not `--accent`: the measured orange carries
|
|
12
|
+
white at 2.89:1 and no type size rescues that. `secondary` is the same outline
|
|
13
|
+
and offset over `--surface`. `ghost` keeps neither, for a toolbar row where an
|
|
14
|
+
outline would add a line the eye has to parse.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<Button onClick={start}>Start free</Button>
|
|
18
|
+
<Button variant="secondary" onClick={book}>Book a demo</Button>
|
|
19
|
+
<Button variant="ghost" size="sm" onClick={dismiss}>Not now</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={['bl-btn', `bl-btn--${variant}`, `bl-btn--${size}`, className]
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join(' ')}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A card is `--surface` at `--r-lg` with a 1px `--line` outline and
|
|
6
|
+
`--shadow-3` — the outline and the offset always travel together, because a hard
|
|
7
|
+
shadow under an edgeless box reads as a rendering fault rather than as depth.
|
|
8
|
+
|
|
9
|
+
A card is for a *group*. A list of statements takes a `Rule`, and a single
|
|
10
|
+
figure takes a `Stat` on the bare paper. Hover on a card **grows** the offset to
|
|
11
|
+
`--shadow-3-wide` — the opposite of the button's press, and mixing the two moves
|
|
12
|
+
is the way this pack breaks.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<Card title="Approvals" meta="4 waiting">
|
|
16
|
+
<p>Every post routes through the client before it publishes.</p>
|
|
17
|
+
</Card>
|
|
18
|
+
```
|
|
@@ -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={['bl-card', className].filter(Boolean).join(' ')}>
|
|
15
|
+
{head && (
|
|
16
|
+
<div className="bl-card__head">
|
|
17
|
+
{title !== undefined && <h3 className="bl-card__title">{title}</h3>}
|
|
18
|
+
{meta !== undefined && <span className="bl-card__meta">{meta}</span>}
|
|
19
|
+
</div>
|
|
20
|
+
)}
|
|
21
|
+
<div className="bl-card__body">{children}</div>
|
|
22
|
+
</section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A chip is the small outlined object the pack repeats everywhere: a tag, a
|
|
6
|
+
filter, a platform label. `--r-pill`, a 1px `--line`, and `--shadow-1` at 2px —
|
|
7
|
+
the smallest step of the offset ramp, because the offset grows with the object.
|
|
8
|
+
|
|
9
|
+
`accent` tints with `--accent-wash` rather than filling with `--accent`: the
|
|
10
|
+
accent is a fill for exactly one control per view and a chip is never it.
|
|
11
|
+
`selected` keeps its state after the pointer leaves.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Chip>Instagram</Chip>
|
|
15
|
+
<Chip tone="accent" selected>Agencies</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
|
+
'bl-chip',
|
|
15
|
+
`bl-chip--${tone}`,
|
|
16
|
+
selected ? 'bl-chip--selected' : undefined,
|
|
17
|
+
className,
|
|
18
|
+
]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(' ')}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The type ramp made visible: 1 is the hero at `--t-hero` 84 / weight 700 /
|
|
6
|
+
line-height 1.1, 2 the section at `--t-page` 48 / 1.2, 3 the card title at
|
|
7
|
+
`--t-card` 20 / weight 600 / 1.4.
|
|
8
|
+
|
|
9
|
+
**Tracking is zero at every level.** The reference sets `letter-spacing` seven
|
|
10
|
+
times in 58 stylesheets and never on a heading, so a negative track here is the
|
|
11
|
+
fastest way to stop the pack looking like itself. Level 1 clamps fluidly and the
|
|
12
|
+
clamp is monotonic — the reference's own ramp is not, and that is a correction
|
|
13
|
+
rather than a copy.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Heading level={1}>Post once. Show up everywhere</Heading>
|
|
17
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HeadingProps {
|
|
4
|
+
/** 1 = hero (84px), 2 = section (48px), 3 = card title (20px). */
|
|
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={['bl-heading', `bl-heading--${level}`, className].filter(Boolean).join(' ')}
|
|
15
|
+
>
|
|
16
|
+
{children}
|
|
17
|
+
</Tag>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Signature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
**One per page.** The framed block at `--r-2xl` with `--shadow-4` — the widest
|
|
6
|
+
offset in the ramp — and 70px of padding, holding the page's densest claim: the
|
|
7
|
+
comparison, the pricing table, the screenshot that has to be believed.
|
|
8
|
+
|
|
9
|
+
A second panel and neither is the panel. Everything else on the page is a card
|
|
10
|
+
at `--shadow-3` or a chip at `--shadow-1`, and the step between them is what
|
|
11
|
+
tells the reader which block matters.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Panel><PricingTable /></Panel>
|
|
15
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface PanelProps {
|
|
4
|
+
/** The page's densest claim. One per page — the pack says so. */
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Panel({ children, className }: PanelProps) {
|
|
10
|
+
return <div className={['bl-panel', className].filter(Boolean).join(' ')}>{children}</div>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Signature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The row of outlined circles that says *many*. Each mark is a 60px chip at
|
|
6
|
+
`--r-pill` with a 1px `--line` and `--shadow-2-wide`; the rail of them is how
|
|
7
|
+
this pack states breadth without a paragraph.
|
|
8
|
+
|
|
9
|
+
It belongs in the first viewport. Putting it below the fold is the most common
|
|
10
|
+
way to build this hero wrong — the rail *is* the argument, and the headline only
|
|
11
|
+
introduces it. The circles carry no text, so `name` is what assistive tech
|
|
12
|
+
reads and what the visible caption renders.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<Rail items={[{ mark: 'in', name: 'LinkedIn' }, { mark: 'ig', name: 'Instagram' }]} />
|
|
16
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface RailItem {
|
|
4
|
+
/** The glyph, mark or one-letter fallback drawn inside the circle. */
|
|
5
|
+
mark: ReactNode;
|
|
6
|
+
/** Read out by assistive tech — the circle carries no text of its own. */
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RailProps {
|
|
11
|
+
items: RailItem[];
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Rail({ items, className }: RailProps) {
|
|
16
|
+
return (
|
|
17
|
+
<ul className={['bl-rail', className].filter(Boolean).join(' ')}>
|
|
18
|
+
{items.map((item) => (
|
|
19
|
+
<li className="bl-rail__item" key={item.name}>
|
|
20
|
+
<span className="bl-rail__mark" aria-hidden="true">
|
|
21
|
+
{item.mark}
|
|
22
|
+
</span>
|
|
23
|
+
<span className="bl-rail__name">{item.name}</span>
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The divider, and the thing to reach for before a second card. `hairline` is 1px
|
|
6
|
+
`--line-weak` for a seam inside a group; `strong` is 1.5px `--line`, the weight
|
|
7
|
+
the reference gives a panel edge.
|
|
8
|
+
|
|
9
|
+
A rule costs no elevation, which is the point: this pack's offsets are a budget
|
|
10
|
+
and a list of statements does not need to spend one.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<Rule />
|
|
14
|
+
<Rule tone="strong" />
|
|
15
|
+
```
|