sheleg-design-skill 1.48.1 → 1.49.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 +76 -0
- package/README.md +5 -4
- package/bin/cli.js +5 -2
- package/cursor/rules/sheleg-design.mdc +6 -1
- package/kits/rimlight/.design-sync/config.json +14 -0
- package/kits/rimlight/.design-sync/conventions.md +62 -0
- package/kits/rimlight/README.md +39 -0
- package/kits/rimlight/package.json +29 -0
- package/kits/rimlight/src/Act.md +19 -0
- package/kits/rimlight/src/Act.tsx +21 -0
- package/kits/rimlight/src/Button.md +17 -0
- package/kits/rimlight/src/Button.tsx +33 -0
- package/kits/rimlight/src/Card.md +14 -0
- package/kits/rimlight/src/Card.tsx +24 -0
- package/kits/rimlight/src/Chip.md +12 -0
- package/kits/rimlight/src/Chip.tsx +25 -0
- package/kits/rimlight/src/Heading.md +14 -0
- package/kits/rimlight/src/Heading.tsx +19 -0
- package/kits/rimlight/src/Label.md +16 -0
- package/kits/rimlight/src/Label.tsx +18 -0
- package/kits/rimlight/src/LitButton.md +22 -0
- package/kits/rimlight/src/LitButton.tsx +26 -0
- package/kits/rimlight/src/Rule.md +13 -0
- package/kits/rimlight/src/Rule.tsx +18 -0
- package/kits/rimlight/src/Stat.md +13 -0
- package/kits/rimlight/src/Stat.tsx +17 -0
- package/kits/rimlight/src/Tile.md +16 -0
- package/kits/rimlight/src/Tile.tsx +22 -0
- package/kits/rimlight/src/index.ts +23 -0
- package/kits/rimlight/src/styles.css +546 -0
- package/kits/rimlight/tsconfig.json +15 -0
- package/package.json +2 -2
- package/plugins/sheleg-design/.claude-plugin/plugin.json +2 -2
- package/plugins/sheleg-design/commands/sheleg-design.md +2 -2
- package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/FIGMA_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 +4 -3
- package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +3 -3
- package/plugins/sheleg-design/skills/sheleg-design/styles/STYLE_PACK_TEMPLATE.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/rimlight.md +342 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/showroom.md +1 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tenor.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/rimlight.css +306 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,82 @@ follow [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.49.0] - 2026-08-24
|
|
10
|
+
|
|
11
|
+
### Rimlight, the thirty-fourth pack — elevation made of light
|
|
12
|
+
|
|
13
|
+
Extracted from `peppermint.global/services/web-design`, a design studio's service page.
|
|
14
|
+
The site is Webflow: one shared stylesheet of 230,354 bytes declaring 82 custom
|
|
15
|
+
properties, and an interaction engine that ships on the page and drives exactly **one**
|
|
16
|
+
element. Every value was read off **computed styles on the live page** through CDP at
|
|
17
|
+
1440×900 — 487 visible of 562 elements on a 12,410px page — and at a device-emulated
|
|
18
|
+
390×844.
|
|
19
|
+
|
|
20
|
+
**The signature is a light, not a shadow.** The primary control wears a **sixteen-layer**
|
|
21
|
+
`box-shadow`: six layers lit — a white rim, a teal spill, a cyan throw, two insets that
|
|
22
|
+
keep it from reading as a sticker, and a mint bloom at 44px of blur offset −14.78px in x
|
|
23
|
+
— and **ten pre-declared at alpha 0**, every offset already in place so a direction can
|
|
24
|
+
be lit by moving alpha alone with no relayout. The light sits below and to the left. On a
|
|
25
|
+
page otherwise made of ink, one blue and two greys, that bloom is the only chromatic
|
|
26
|
+
event, so the most saturated thing on the screen is also the thing you are meant to click.
|
|
27
|
+
No pack in the library had elevation made of coloured light.
|
|
28
|
+
|
|
29
|
+
**Reading the render mattered twice, in opposite directions**, which is exactly what the
|
|
30
|
+
template's new rule 7 was written for a release earlier. The uppercase micro-labels *look*
|
|
31
|
+
generously tracked and are not — `letter-spacing` computes to `normal` on every one, and
|
|
32
|
+
the openness is Source Code Pro's own advance width, so a pack matched from a screenshot
|
|
33
|
+
ships a tracking value that does not exist. And the button's elevation *looks* like one
|
|
34
|
+
soft shadow; sixteen layers only resolve under `getComputedStyle`.
|
|
35
|
+
|
|
36
|
+
**The palette is split by field, and that is the pack's central law.** Every one of the
|
|
37
|
+
reference's five secondary hues measures 6.5–10.5:1 on the dark act and **1.65–2.65:1 on
|
|
38
|
+
the white page**. They did not fail — they were designed for the dark, and the light field
|
|
39
|
+
is where they must be held back. So the hues live in `[data-surface="dark"]`, which is a
|
|
40
|
+
**surface variant and not a theme twin**: one section is dark, the document never inverts,
|
|
41
|
+
and the four statuses are remapped rather than inherited because the light set measures
|
|
42
|
+
1.1–1.5:1 there.
|
|
43
|
+
|
|
44
|
+
**The blue is two tokens because it does two jobs.** `--accent` is 3.03:1 on `--bg` —
|
|
45
|
+
below the 4.5:1 body floor and above the 3:1 large-text one — so it is declared
|
|
46
|
+
`@role non-text` with its exemption stated: legal on a word only at ≥ 24px, which is where
|
|
47
|
+
the reference spends it, at 86px in the headline's first phrase. Anything smaller takes
|
|
48
|
+
`--accent-ink` at 5.16:1.
|
|
49
|
+
|
|
50
|
+
**Four corrections travel with the pack.** Secondary copy is `#9d9fa3` at 2.65:1 on the
|
|
51
|
+
page. The tertiary grey `#767676` passes the page at 4.54:1 and fails the act separator at
|
|
52
|
+
4.05:1. The reference's secondary set is a palette and not a scheme — eight of its
|
|
53
|
+
twenty-one pairs are too close to separate under dichromacy — so the four status roles were
|
|
54
|
+
derived from its own hues rather than adopted from its names. And 26 of 37 visible
|
|
55
|
+
interactive elements at 390 are under 44px.
|
|
56
|
+
|
|
57
|
+
**Two doctrine corrections the gates caught, both worth recording.** The pack prescribed
|
|
58
|
+
its press at the reference's own 200ms, which sits outside the doctrine's 100–160ms band,
|
|
59
|
+
so `--dur-press` takes the band's ceiling instead of the nearest measured value — the one
|
|
60
|
+
motion value here that is not measured, and it says so. And the pack bans bold while the
|
|
61
|
+
token layer shipped no base rule for it: the UA supplies `strong { font-weight: 700 }`
|
|
62
|
+
whether a pack does or not, so a ban with no rule is invisible. The layer now sets
|
|
63
|
+
`strong, b { font-weight: var(--weight-label) }` — 500, the heaviest weight this pack has.
|
|
64
|
+
|
|
65
|
+
**Rendering the kit caught a defect the gates could not**, for the third release running.
|
|
66
|
+
The light control variant rendered *dark* inside the dark act: it was built on
|
|
67
|
+
`--surface`, which `[data-surface="dark"]` remaps to `#242424`, so a control that is white
|
|
68
|
+
by definition followed the field and the rig lit nothing. `--lit-light-fill` does not
|
|
69
|
+
follow the field. A gate reads structure, not layout.
|
|
70
|
+
|
|
71
|
+
`MOTION_INTENSITY` is pinned at **2** — the lowest ceiling in the library beside
|
|
72
|
+
`bulletin`. 432 of 487 elements compute `transition-duration: 0s`, `animation-timeline`
|
|
73
|
+
appears zero times, and the rig is a **static** light: animating it is the pack's first
|
|
74
|
+
ban and the fastest way to turn it into a toy.
|
|
75
|
+
|
|
76
|
+
The fork against `showroom` is written from both ends: both are white and both spend a big
|
|
77
|
+
layered elevation on one object, and the distinction is that `showroom` lights the
|
|
78
|
+
application in a neutral shadow dropped from above while this lights a control in coloured
|
|
79
|
+
light thrown from the side — and this hero carries no product at all. `ADR-0001` gains a
|
|
80
|
+
further application, with `halation`, `limelight` and `luminaire` weighed and rejected.
|
|
81
|
+
|
|
82
|
+
Gates: 4626 → **4754**, 2681 → **2784**, 692 → **706**. Both palette ceilings held for the
|
|
83
|
+
second consecutive release; `computed_at_least` rises 572 → 600.
|
|
84
|
+
|
|
9
85
|
## [1.48.1] - 2026-08-24
|
|
10
86
|
|
|
11
87
|
### The assembly rule is written down, and the render step earned its place within the hour
|
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 **thirty-
|
|
14
|
+
decides whether to animate before it decides how, and **thirty-four 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
|
|
|
@@ -53,6 +53,7 @@ into the cinematic layer, and says so in its own *Motion flavor* section.
|
|
|
53
53
|
| `babylove` | white with one orange in six steps and nothing else declared — seven tokens over Tailwind's defaults, two card levels nested at 16 and 8px, no shadow and no dark theme | SEO and AI-visibility SaaS with a long time-to-value — the minimal-system answer to the same brief `outrank` answers maximally |
|
|
54
54
|
| `patchbay` | near-black under a faint 40px grid, one mint-cyan doing every functional job, elevation made of 8% hairlines with no shadow anywhere, and a live architecture diagram whose cords carry SMIL particles | engines, message buses, pipelines, schedulers and open-source front doors — anything whose argument is an architecture rather than a promise |
|
|
55
55
|
| `nameplate` | a cool near-white slab under a page that is **square on 87% of its elements** — 1,091 of 1,251 measured — where the one round shape is reserved for a white 1px-bordered pill carrying somebody else's publication name **as type rather than as a logo**, one family with the body at **weight 500**, and two uppercase registers tracked 0.06em and 0.175em | **pages whose argument is that named third parties will vouch for you** — press and media placement, PR distribution, trust marks and badges, certification, review aggregation and "as featured in" surfaces (standalone) |
|
|
56
|
+
| `rimlight` | a white field with a cool grey act separator and one near-black act, a grotesque for every sentence and a **monospace for every piece of chrome**, square on 84% of its elements, tracked negative at every size and with **no bold anywhere** — and an elevation made of **coloured light**: a sixteen-layer rig, six layers lit and ten held at alpha 0, thrown from below-left onto one control per viewport | **a studio's own front door and the pages that sell what it makes** — design and engineering agencies, product studios, service and case-study pages (standalone) |
|
|
56
57
|
| `outrank` | white field with one violet that carries text in both directions, a 5px light ring standing in for a button shadow, and two type families split by how long a thing is read | SEO and growth SaaS, agent-run back-office — the pack for a product that ships a landing and a dashboard at once |
|
|
57
58
|
| `orchard` | warm oat field of rounded slabs, sage brand + one candy-orange action, rounded geometric display, soft-3D pills built from inset light | friendly consumer biotech, DTC wellness, testing kits & supplements |
|
|
58
59
|
| `field-notes` | warm green-cast paper ruled by hairlines, one rust accent, a hero that dissolves into the page instead of ending, numbered mono eyebrows, crop marks, colour that encodes how a claim is known | open-source & developer tools sold on auditability — code intelligence, provenance, evals, agent memory |
|
|
@@ -153,7 +154,7 @@ skills.
|
|
|
153
154
|
| `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 |
|
|
154
155
|
| `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 |
|
|
155
156
|
| `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 |
|
|
156
|
-
| `styles/*.md` | The thirty-
|
|
157
|
+
| `styles/*.md` | The thirty-four style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
|
|
157
158
|
| `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) |
|
|
158
159
|
| `styles/STYLE_PACK_TEMPLATE.md` | The pack contract as a skeleton, so a new style is authored against the same headings rather than improvised |
|
|
159
160
|
|
|
@@ -224,7 +225,7 @@ cd ./ds-workbench && npm install && npm run build
|
|
|
224
225
|
then `/design-sync` in that directory, from Claude Code. Three layers cross: the
|
|
225
226
|
pack's **bans** as the design system's own README, `styles.css` built from
|
|
226
227
|
`tokens/<pack>.css` verbatim, and the components — a six-name spine that is
|
|
227
|
-
identical in all thirty-
|
|
228
|
+
identical in all thirty-four kits, so switching packs swaps identity rather than API,
|
|
228
229
|
plus each pack's signature parts. **Motion does not cross**, exactly as it does
|
|
229
230
|
not cross into Figma: a kit is the static half of a pack, and saying so is what
|
|
230
231
|
stops an agent inventing motion to fill the silence.
|
|
@@ -270,7 +271,7 @@ a pack's four widened sections used to make two gates *quieter* and still green.
|
|
|
270
271
|
One honest limit: the npx installer is checked by asserting its runtime bundle
|
|
271
272
|
walker exists, not by reading a file list — it has none by design. What proves
|
|
272
273
|
it ships the right files is CI, which installs the bundle through **both**
|
|
273
|
-
installers and `diff -r`s the result against the source, then builds all thirty-
|
|
274
|
+
installers and `diff -r`s the result against the source, then builds all thirty-four
|
|
274
275
|
kits.
|
|
275
276
|
|
|
276
277
|
`test/scenarios.md` (T1–T30) is the behavioral harness: fresh subagents given a
|
package/bin/cli.js
CHANGED
|
@@ -234,14 +234,17 @@ ${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/ thirty-
|
|
237
|
+
styles/ thirty-four 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), patchbay (dark live
|
|
241
241
|
schematic), nameplate (a cool near-white slab under a
|
|
242
242
|
square page, the one round shape reserved for a white
|
|
243
243
|
bordered pill carrying somebody else's publication name
|
|
244
|
-
as type),
|
|
244
|
+
as type), rimlight (a white field and one near-black
|
|
245
|
+
act, a monospace carrying every label and button, and
|
|
246
|
+
an elevation made of coloured light rather than a
|
|
247
|
+
shadow), babylove (white +
|
|
245
248
|
one orange, seven tokens over Tailwind defaults),
|
|
246
249
|
outrank (white +
|
|
247
250
|
one violet, a landing and a dashboard in one brand),
|
|
@@ -107,7 +107,12 @@ the one round shape reserved for a white 1px-bordered pill carrying somebody
|
|
|
107
107
|
else's publication name as type, one family with the body at weight 500, and two
|
|
108
108
|
uppercase registers tracked 0.06em and 0.175em, for pages whose argument is that
|
|
109
109
|
named third parties will vouch for you: press placement, trust marks,
|
|
110
|
-
certification and review aggregation
|
|
110
|
+
certification and review aggregation;
|
|
111
|
+
rimlight — a white field with a cool grey act separator and one near-black act, a
|
|
112
|
+
grotesque for every sentence and a monospace for every piece of chrome, square on 84%
|
|
113
|
+
of its elements, tracked negative at every size and with no bold anywhere, whose only
|
|
114
|
+
elevation is a sixteen-layer coloured light rig thrown from below-left onto one
|
|
115
|
+
control per viewport, for a studio's front door and its service and case-study pages);
|
|
111
116
|
otherwise follow the contract below (self-contained on purpose).
|
|
112
117
|
|
|
113
118
|
## Whether to animate at all — before how
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pkg": "@sheleg-design/rimlight",
|
|
3
|
+
"globalName": "ShelegRimlight",
|
|
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,62 @@
|
|
|
1
|
+
# Rimlight — the contract this design system ships under
|
|
2
|
+
|
|
3
|
+
**Register.** Choose Rimlight for **a studio's own front door and the pages that sell
|
|
4
|
+
what it makes**: design and engineering agencies, product studios, service and
|
|
5
|
+
case-study pages, B2B surfaces whose argument is craft rather than a feature list. A
|
|
6
|
+
white field, a cool grey act separator, one near-black act, and coloured light where
|
|
7
|
+
other systems put a shadow. Build every screen against `var(--…)` and never a literal.
|
|
8
|
+
|
|
9
|
+
**Elevation is a light, and it belongs to one control.** `--glow-rig` is sixteen
|
|
10
|
+
layers — six lit and ten held at alpha 0, thrown from below and to the left. Exactly
|
|
11
|
+
one control per viewport wears it. Two lit controls and the light stops meaning *this
|
|
12
|
+
one*.
|
|
13
|
+
|
|
14
|
+
**The rig is static.** It does not change on hover, press or focus: the fill moves one
|
|
15
|
+
step and the light stays. Pulsing, rotating or hover-growing it is the first ban here,
|
|
16
|
+
and it is the single change that turns this pack into a novelty.
|
|
17
|
+
|
|
18
|
+
**The only other shadows are the tile's.** `--shadow-tile` on the 80px icon tile and
|
|
19
|
+
`--shadow-tile-inner` on its 68px image. Nothing else carries one — 483 of the
|
|
20
|
+
reference's 487 visible elements compute `box-shadow: none`. A card has no shadow and
|
|
21
|
+
no border; it is separated by the field it sits on.
|
|
22
|
+
|
|
23
|
+
**The monospace is the chrome.** Every nav item, button and label is Source Code Pro;
|
|
24
|
+
every sentence is Archivo. That split is how a reader tells a control from a statement
|
|
25
|
+
without reading either, and it is not decorative.
|
|
26
|
+
|
|
27
|
+
**Do not track the monospace.** `letter-spacing` computes to `normal` on every label
|
|
28
|
+
in the reference — the openness is the face's own advance width. Adding tracking is
|
|
29
|
+
the trap this pack sets for anyone matching it from a screenshot.
|
|
30
|
+
|
|
31
|
+
**No bold anywhere.** 500 is the heaviest weight and it belongs to the mono label; the
|
|
32
|
+
display runs at 400 and the lede at 300. The display is loud because it is 86px.
|
|
33
|
+
|
|
34
|
+
**Tracking is negative at every size and tightens as the display shrinks** —
|
|
35
|
+
`--track-display` (−0.02em) at 86px, `--track-display-narrow` (−0.04em) at the 40px
|
|
36
|
+
narrow headline.
|
|
37
|
+
|
|
38
|
+
**The blue is two tokens because it does two jobs.** `--accent` is 3.03:1 on `--bg`:
|
|
39
|
+
legal on a word only at WCAG large text, which at weight 400 means ≥ 24px. Anything
|
|
40
|
+
smaller takes `--accent-ink`. A 16px link in `--accent` is a word below the floor and
|
|
41
|
+
is the commonest way to break this pack while believing you matched it.
|
|
42
|
+
|
|
43
|
+
**The colours live in the dark act.** Every one of the reference's five secondary hues
|
|
44
|
+
is 6.5–10.5:1 on `#1b1b1b` and 1.65–2.65:1 on white. They were designed for the dark;
|
|
45
|
+
on a light field they are not text.
|
|
46
|
+
|
|
47
|
+
**`[data-surface="dark"]` is a section, never the document.** There is no toggle and
|
|
48
|
+
no second palette for the page — one band is dark and the rest stays light. Putting it
|
|
49
|
+
on `:root` inverts a page that was never designed to invert, and the light field's four
|
|
50
|
+
statuses measure 1.1–1.5:1 if they arrive unremapped.
|
|
51
|
+
|
|
52
|
+
**Status is never by colour alone** — every state takes a glyph or a label beside its
|
|
53
|
+
colour. The four roles are derived rather than adopted: the reference's own secondary
|
|
54
|
+
set is a palette, not a scheme, with eight of its twenty-one pairs too close to
|
|
55
|
+
separate under dichromacy.
|
|
56
|
+
|
|
57
|
+
**Every control clears `--tap-min` (44px).** A correction: 26 of the reference's 37
|
|
58
|
+
visible interactive elements at 390px are shorter than that.
|
|
59
|
+
|
|
60
|
+
**Nothing travels in space.** No parallax, no scrub, no `animation-timeline`, no press
|
|
61
|
+
translation. `MOTION_INTENSITY` above 2 has nothing legal to buy, and reduced motion
|
|
62
|
+
collapses every duration to zero.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @sheleg-design/rimlight
|
|
2
|
+
|
|
3
|
+
The React reference kit for the SHELEG **Rimlight** style pack — a white field, one
|
|
4
|
+
near-black act, a monospace carrying every piece of chrome, and an elevation made of
|
|
5
|
+
coloured light rather than shadow.
|
|
6
|
+
|
|
7
|
+
It is generated from the pack, not authored beside it: `src/styles.css` opens with
|
|
8
|
+
`styles/tokens/rimlight.css` byte for byte, and the rules the design agent must obey
|
|
9
|
+
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 types
|
|
18
|
+
in every SHELEG kit, so switching packs swaps identity rather than API.
|
|
19
|
+
|
|
20
|
+
## This pack's own
|
|
21
|
+
|
|
22
|
+
`LitButton` (the sixteen-layer rig, one per viewport), `Tile` (the 80px icon square
|
|
23
|
+
that opens a section, and one of only two objects allowed an ordinary shadow), `Label`
|
|
24
|
+
(the monospace chrome — untracked, always) and `Act` (a full-bleed section, including
|
|
25
|
+
the dark one, which is a surface variant and never a document theme).
|
|
26
|
+
|
|
27
|
+
## The three rules that carry the kit
|
|
28
|
+
|
|
29
|
+
1. **One lit control per viewport.** The rig is the only chromatic event on a page
|
|
30
|
+
otherwise made of ink, one blue and two greys — so the most saturated thing on the
|
|
31
|
+
screen is also the thing you are meant to click. Two of them and that stops working.
|
|
32
|
+
2. **The light does not move.** It is deliberately absent from `.rl-lit`'s transition
|
|
33
|
+
list. A static light reads as craft; an animated one reads as a toy.
|
|
34
|
+
3. **The monospace is chrome and the grotesque is prose.** Never mix the two roles, and
|
|
35
|
+
never track the monospace.
|
|
36
|
+
|
|
37
|
+
**Motion does not cross into a design tool.** A kit is the static half of a pack: this
|
|
38
|
+
one is unusually literal about that, because the pack's whole motion budget is a 0.3s
|
|
39
|
+
colour transition.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sheleg-design/rimlight",
|
|
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,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A full-bleed section on one of the pack's three fields. `field="dark"` sets
|
|
6
|
+
`data-surface="dark"`, which remaps the palette **for that band only**.
|
|
7
|
+
|
|
8
|
+
**It is not a dark mode.** The reference has no toggle and no second palette for the
|
|
9
|
+
document: one section carries the dark treatment and the rest of the page stays light.
|
|
10
|
+
Putting `data-surface="dark"` on `:root` inverts a page that was never designed to
|
|
11
|
+
invert.
|
|
12
|
+
|
|
13
|
+
The dark act is where the reference's colours finally work — every one of its
|
|
14
|
+
secondary hues is 6.5–10.5:1 there against 1.65–2.65:1 on white.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<Act>…</Act>
|
|
18
|
+
<Act field="dark">…</Act>
|
|
19
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ActProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** `page` is white, `field` the cool grey separator, `dark` the near-black act. */
|
|
6
|
+
field?: 'page' | 'field' | 'dark';
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A full-bleed section. `dark` sets data-surface, which remaps the palette for the
|
|
11
|
+
* band only — it is a surface variant, never a document theme. */
|
|
12
|
+
export function Act({ children, field = 'page', className }: ActProps) {
|
|
13
|
+
return (
|
|
14
|
+
<section
|
|
15
|
+
className={['rl-act', `rl-act--${field}`, className].filter(Boolean).join(' ')}
|
|
16
|
+
data-surface={field === 'dark' ? 'dark' : undefined}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</section>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Actions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The ordinary control: transparent with a `1px solid --ink-faint` border at `--r-pill`
|
|
6
|
+
(160px), its label the monospace at 16–18px/500 uppercase, height `--control-h` (48px)
|
|
7
|
+
or `--control-h-lg` (60px).
|
|
8
|
+
|
|
9
|
+
`primary` fills with `--ink` and takes `--on-ink`. **It does not wear the rig** — that
|
|
10
|
+
belongs to `LitButton`, one per viewport. Hover moves the border to `--ink` and never
|
|
11
|
+
adds a fill; nothing on this page travels in space.
|
|
12
|
+
|
|
13
|
+
Every size honours `--tap-min` (44px) as a floor.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Button variant="secondary">View project</Button>
|
|
17
|
+
```
|
|
@@ -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={['rl-btn', `rl-btn--${variant}`, `rl-btn--${size}`, className]
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join(' ')}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`--surface` on `--field`, or `--field` on `--bg`, at `--r-sm` (12px), with **no border
|
|
6
|
+
and no shadow**. Inside a dark `Act` it becomes `#242424` on `#1b1b1b` — separated by
|
|
7
|
+
tone in both cases, never by a line.
|
|
8
|
+
|
|
9
|
+
483 of the reference's 487 visible elements carry no shadow at all. A card is not one
|
|
10
|
+
of the four that do.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<Card title="Discovery" meta="01">…</Card>
|
|
14
|
+
```
|
|
@@ -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={['rl-card', className].filter(Boolean).join(' ')}>
|
|
15
|
+
{head && (
|
|
16
|
+
<div className="rl-card__head">
|
|
17
|
+
{title !== undefined && <h3 className="rl-card__title">{title}</h3>}
|
|
18
|
+
{meta !== undefined && <span className="rl-card__meta">{meta}</span>}
|
|
19
|
+
</div>
|
|
20
|
+
)}
|
|
21
|
+
<div className="rl-card__body">{children}</div>
|
|
22
|
+
</section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A small tag at `--r-sm`, `--field` fill, label in the monospace at `--t-xs`. `accent`
|
|
6
|
+
tints with `--accent-wash` and takes `--accent-ink` for the word — **never `--accent`
|
|
7
|
+
itself**, which is 3.03:1 and legal only above 24px.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<Chip>SaaS</Chip>
|
|
11
|
+
<Chip tone="accent" selected>DevTools</Chip>
|
|
12
|
+
```
|
|
@@ -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
|
+
'rl-chip',
|
|
15
|
+
`rl-chip--${tone}`,
|
|
16
|
+
selected ? 'rl-chip--selected' : undefined,
|
|
17
|
+
className,
|
|
18
|
+
]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(' ')}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Three levels in Archivo at `--weight-body` (400) — never bold — with
|
|
6
|
+
`--track-display` (−0.02em): 1 is the hero at `--t-hero` (86px, and 40px at 390 where
|
|
7
|
+
the tracking tightens to −0.04em), 2 is the section at `--t-section` (60px), 3 a card
|
|
8
|
+
title at `--t-card` (32px).
|
|
9
|
+
|
|
10
|
+
The display is loud because it is large, not because it is heavy.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<Heading level={1}>Web design for SaaS</Heading>
|
|
14
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HeadingProps {
|
|
4
|
+
/** 1 = page title (28px), 2 = section (20px), 3 = card title (15px). */
|
|
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={['rl-heading', `rl-heading--${level}`, className].filter(Boolean).join(' ')}
|
|
15
|
+
>
|
|
16
|
+
{children}
|
|
17
|
+
</Tag>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The monospace, uppercase, at `--t-label` (16px) and `--weight-label` (500). It carries
|
|
6
|
+
every nav item, every button and every section eyebrow — the grotesque carries only
|
|
7
|
+
sentences, and that split is the pack's most legible rule.
|
|
8
|
+
|
|
9
|
+
**Do not track it.** `letter-spacing` computes to `normal` on every label in the
|
|
10
|
+
reference; the openness is Source Code Pro's own advance width. Adding tracking is the
|
|
11
|
+
trap this pack sets for anyone matching it from a screenshot.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Label>Services</Label>
|
|
15
|
+
<Label role="eyebrow">How we measure success</Label>
|
|
16
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface LabelProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** `chrome` is nav and button furniture; `eyebrow` opens a section. */
|
|
6
|
+
role?: 'chrome' | 'eyebrow';
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Every label in this pack is the monospace. It is how a reader tells a control
|
|
11
|
+
* from a statement without reading either. */
|
|
12
|
+
export function Label({ children, role = 'chrome', className }: LabelProps) {
|
|
13
|
+
return (
|
|
14
|
+
<span className={['rl-label', `rl-label--${role}`, className].filter(Boolean).join(' ')}>
|
|
15
|
+
{children}
|
|
16
|
+
</span>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Actions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The pack's signature and its whole idea: a near-black pill at `--r-glow` (100px)
|
|
6
|
+
wearing `--glow-rig` — sixteen shadow layers, six lit and ten held at alpha 0, thrown
|
|
7
|
+
from below and to the left. `surface="light"` swaps to `--glow-rig-light`, where the
|
|
8
|
+
rim goes black and the inset goes white.
|
|
9
|
+
|
|
10
|
+
**One per viewport.** Two lit controls and the light stops meaning *this one*.
|
|
11
|
+
|
|
12
|
+
**The rig does not change on hover.** It is a static light; the fill moves one step
|
|
13
|
+
and the rig stays. Pulsing, rotating or growing it is the pack's first ban, and it is
|
|
14
|
+
the fastest way to turn this pack into a novelty. Disabled drops the rig entirely.
|
|
15
|
+
|
|
16
|
+
The label is the monospace at 18px/500 uppercase with `letter-spacing: normal` — the
|
|
17
|
+
openness is the face's own width, not tracking.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<LitButton>Book a call</LitButton>
|
|
21
|
+
<LitButton surface="light">Request a quote</LitButton>
|
|
22
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface LitButtonProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** `dark` is the near-black control on a light field; `light` is its inverse.
|
|
6
|
+
* Both wear the same rig — only the rim and the inset swap. */
|
|
7
|
+
surface?: 'dark' | 'light';
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The pack's signature, and there is ONE per viewport. The rig is a static light:
|
|
14
|
+
* it does not change on hover, and animating it is the pack's first ban. */
|
|
15
|
+
export function LitButton({ children, surface = 'dark', disabled = false, onClick, className }: LitButtonProps) {
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
className={['rl-lit', `rl-lit--${surface}`, className].filter(Boolean).join(' ')}
|
|
20
|
+
disabled={disabled}
|
|
21
|
+
onClick={onClick}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</button>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`hairline` is a 1px `--ink-faint` line — the only line the pack draws. `strong` takes
|
|
6
|
+
`--ink-quiet`.
|
|
7
|
+
|
|
8
|
+
There is no other border width here, and a rule is never a substitute for the field
|
|
9
|
+
change that normally separates two acts.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<Rule />
|
|
13
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RuleProps {
|
|
2
|
+
tone?: 'hairline' | 'strong';
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function Rule({ tone = 'hairline', className }: RuleProps) {
|
|
7
|
+
return (
|
|
8
|
+
<hr
|
|
9
|
+
className={[
|
|
10
|
+
'rl-rule',
|
|
11
|
+
tone === 'strong' ? 'rl-rule--strong' : undefined,
|
|
12
|
+
className,
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(' ')}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|