sheleg-design-skill 1.41.0 → 1.42.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 +51 -0
- package/README.md +5 -4
- package/bin/cli.js +6 -2
- package/cursor/rules/sheleg-design.mdc +6 -1
- package/kits/proscenium/.design-sync/config.json +14 -0
- package/kits/proscenium/.design-sync/conventions.md +50 -0
- package/kits/proscenium/README.md +27 -0
- package/kits/proscenium/package.json +29 -0
- package/kits/proscenium/src/Button.md +23 -0
- package/kits/proscenium/src/Button.tsx +33 -0
- package/kits/proscenium/src/Card.md +26 -0
- package/kits/proscenium/src/Card.tsx +24 -0
- package/kits/proscenium/src/Chip.md +18 -0
- package/kits/proscenium/src/Chip.tsx +25 -0
- package/kits/proscenium/src/Frame.md +23 -0
- package/kits/proscenium/src/Frame.tsx +25 -0
- package/kits/proscenium/src/Heading.md +20 -0
- package/kits/proscenium/src/Heading.tsx +19 -0
- package/kits/proscenium/src/Rule.md +16 -0
- package/kits/proscenium/src/Rule.tsx +18 -0
- package/kits/proscenium/src/Skeleton.md +17 -0
- package/kits/proscenium/src/Skeleton.tsx +17 -0
- package/kits/proscenium/src/Stage.md +22 -0
- package/kits/proscenium/src/Stage.tsx +15 -0
- package/kits/proscenium/src/Stat.md +17 -0
- package/kits/proscenium/src/Stat.tsx +17 -0
- package/kits/proscenium/src/StatusDot.md +20 -0
- package/kits/proscenium/src/StatusDot.tsx +19 -0
- package/kits/proscenium/src/index.ts +23 -0
- package/kits/proscenium/src/styles.css +589 -0
- package/kits/proscenium/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 +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/MOBILE_SURFACES.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +5 -4
- package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/proscenium.md +312 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/showroom.md +9 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/proscenium.css +225 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ 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.42.0] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
**The twenty-eighth pack, and the first one measured for its tempo rather than its
|
|
10
|
+
surface.** `proscenium` is extracted from [mailmodo.com](https://www.mailmodo.com/), read
|
|
11
|
+
off live computed styles in a headless Chrome at 1440×1000 and at 390, 768 and 1024 for
|
|
12
|
+
the ramp. What it takes from the reference is the *cadence* — two acts, then the same call
|
|
13
|
+
to action, again, with one dark act at the middle — which is why it forks against
|
|
14
|
+
`showroom` on tempo rather than on look.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`proscenium`**, from [mailmodo.com](https://www.mailmodo.com/) — a white field
|
|
19
|
+
carrying two cool acts and **one deep indigo act at the middle** (`#07061d` to `#2a0b78`
|
|
20
|
+
at 86.41%, stops measured), ink that is an indigo rather than a grey, an electric violet
|
|
21
|
+
at `#5a45fe` filling a control that stays **nearly square at 4px against cards at 16**,
|
|
22
|
+
one family at nine weights, and a framed product panel the fold cuts off. Widened
|
|
23
|
+
contract, addressable origin, every stated ratio recomputed by the palette gate.
|
|
24
|
+
- **A reference kit**, with the six-component spine plus `StatusDot`, `Skeleton`, `Frame`
|
|
25
|
+
and this pack's own `Stage` — the one dark act, which the kit's stylesheet teaches to
|
|
26
|
+
strip elevation from any card standing inside it.
|
|
27
|
+
|
|
28
|
+
### Three values the reference has and this pack declines, each marked at its declaration
|
|
29
|
+
|
|
30
|
+
- **The rem base.** Mailmodo steps the root font size by viewport — 10px at 390 and 768,
|
|
31
|
+
11px at 1024, 13px at 1440 — and lets every rem follow. It is coherent and it overrides
|
|
32
|
+
the reader's own text-size preference, so the pack takes the *endpoints* (27px→62px on
|
|
33
|
+
the display, 28px→62px on an act heading) and ships them as clamp slopes with a rem term
|
|
34
|
+
in the sum.
|
|
35
|
+
- **The heading ink.** The reference sets headings to pure black against an indigo body
|
|
36
|
+
ink. The move is worth keeping and the literal is not: pure black as a field or an ink is
|
|
37
|
+
banned library-wide as an unfinished default and the slop lint fails on it, so the pack
|
|
38
|
+
ships `#05041c` — 20.18:1 on the field against black's 21.00.
|
|
39
|
+
- **The amber.** `#9e7613` is 4.15:1 on white, under AA for body text. The hue is the
|
|
40
|
+
reference's; the step down to `#8a6510` (5.32 / 4.91 / 4.68) is the pack's.
|
|
41
|
+
|
|
42
|
+
`--ok`, `--danger` and the entire dark register are pack decisions rather than
|
|
43
|
+
measurements — the reference paints no success and no error state and has no dark mode. The
|
|
44
|
+
dark register is derived from the one dark thing the reference does have: the stage act's
|
|
45
|
+
own two stops carry the field and the panel.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **A self-test plant that had stopped finding its target.** The core-contract remainder
|
|
50
|
+
fixture matched `The other \w+ answer all four`; at the twenty-eighth pack the remainder
|
|
51
|
+
became "twenty-one", which a bare `\w+` cannot match, so the plant changed nothing and
|
|
52
|
+
the self-test still reported it as caught. A fixture that cannot find its own target is a
|
|
53
|
+
hole in the gate, and it opens on exactly the release the plant exists to catch.
|
|
54
|
+
|
|
55
|
+
### Ratchet
|
|
56
|
+
|
|
57
|
+
`test/floors.json` raised 2422/1287/504 → 3222/1811/615.
|
|
7
58
|
## [1.41.0] - 2026-08-17
|
|
8
59
|
|
|
9
60
|
**A style pack is a token layer and a set of rules. It does not ship a button** — and until
|
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-eight 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
|
|
|
@@ -71,6 +71,7 @@ into the cinematic layer, and says so in its own *Motion flavor* section.
|
|
|
71
71
|
| `notation` | a near-white page drawn **entirely in hairlines instead of cards**, radii of 2 and 4px, a slab serif held at weight 300 against a monospace, **no bold anywhere**, an ink primary that leaves the accent free to mark what can be read, and one chamfered corner per page | **developer and technical products sold on restraint** — open source front pages, workspaces for people who dislike being sold to, documentation homes (standalone) |
|
|
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
|
+
| `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) |
|
|
74
75
|
| `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 |
|
|
75
76
|
|
|
76
77
|
Each pack locks palette, type, texture, motion tokens, signature motifs and
|
|
@@ -146,7 +147,7 @@ skills.
|
|
|
146
147
|
| `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 |
|
|
147
148
|
| `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 |
|
|
148
149
|
| `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 |
|
|
149
|
-
| `styles/*.md` | The twenty-
|
|
150
|
+
| `styles/*.md` | The twenty-eight style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
|
|
150
151
|
| `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) |
|
|
151
152
|
| `styles/STYLE_PACK_TEMPLATE.md` | The pack contract as a skeleton, so a new style is authored against the same headings rather than improvised |
|
|
152
153
|
|
|
@@ -217,7 +218,7 @@ cd ./ds-workbench && npm install && npm run build
|
|
|
217
218
|
then `/design-sync` in that directory, from Claude Code. Three layers cross: the
|
|
218
219
|
pack's **bans** as the design system's own README, `styles.css` built from
|
|
219
220
|
`tokens/<pack>.css` verbatim, and the components — a six-name spine that is
|
|
220
|
-
identical in all twenty-
|
|
221
|
+
identical in all twenty-eight kits, so switching packs swaps identity rather than API,
|
|
221
222
|
plus each pack's signature parts. **Motion does not cross**, exactly as it does
|
|
222
223
|
not cross into Figma: a kit is the static half of a pack, and saying so is what
|
|
223
224
|
stops an agent inventing motion to fill the silence.
|
|
@@ -263,7 +264,7 @@ a pack's four widened sections used to make two gates *quieter* and still green.
|
|
|
263
264
|
One honest limit: the npx installer is checked by asserting its runtime bundle
|
|
264
265
|
walker exists, not by reading a file list — it has none by design. What proves
|
|
265
266
|
it ships the right files is CI, which installs the bundle through **both**
|
|
266
|
-
installers and `diff -r`s the result against the source, then builds all twenty-
|
|
267
|
+
installers and `diff -r`s the result against the source, then builds all twenty-eight
|
|
267
268
|
kits.
|
|
268
269
|
|
|
269
270
|
`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/ twenty-
|
|
237
|
+
styles/ twenty-eight 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
|
|
@@ -276,7 +276,11 @@ ${c("bold", "What it installs")}
|
|
|
276
276
|
almanac (oatmeal paper at 2px with no 1px anywhere and a
|
|
277
277
|
display set below a line-height of one), vitrine (a white
|
|
278
278
|
hairline field with a serif display, an ink primary, and
|
|
279
|
-
one framed record carrying the evidence)
|
|
279
|
+
one framed record carrying the evidence),
|
|
280
|
+
proscenium (a white field with two cool acts and one deep
|
|
281
|
+
indigo act at the middle, a violet filling a control that
|
|
282
|
+
stays nearly square at 4px against cards at 16, and a
|
|
283
|
+
framed product panel the fold cuts off) —
|
|
280
284
|
plus a ready-made token CSS per pack and
|
|
281
285
|
STYLE_PACK_TEMPLATE.md for authoring more
|
|
282
286
|
`);
|
|
@@ -86,7 +86,12 @@ four stacked shadow stops, for pages that assert a category;
|
|
|
86
86
|
vitrine — a white field drawn entirely in hairlines, a serif display over a sans
|
|
87
87
|
body, an ink primary, a grey panel that groups without lifting, and one framed
|
|
88
88
|
record with a 1px inset highlight carrying the page's evidence, for the front
|
|
89
|
-
door of a product sold on trust
|
|
89
|
+
door of a product sold on trust;
|
|
90
|
+
proscenium — a white field carrying two cool acts and one deep indigo act at the
|
|
91
|
+
middle, an electric violet filling a control that stays nearly square at 4px
|
|
92
|
+
against cards at 16, one family at nine weights, and a framed product panel the
|
|
93
|
+
fold cuts off, for product-led marketing front doors whose argument is a
|
|
94
|
+
demonstration);
|
|
90
95
|
otherwise follow the contract below (self-contained on purpose).
|
|
91
96
|
|
|
92
97
|
## Whether to animate at all — before how
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pkg": "@sheleg-design/proscenium",
|
|
3
|
+
"globalName": "ShelegProscenium",
|
|
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,50 @@
|
|
|
1
|
+
# Proscenium — the contract this design system ships under
|
|
2
|
+
|
|
3
|
+
**Register.** Choose Proscenium for **product-led marketing front doors whose
|
|
4
|
+
argument is a demonstration**: SaaS home pages, launch and tour pages, any page with
|
|
5
|
+
six or more acts that needs a repeated beat. Light is the default register and dark
|
|
6
|
+
is a first-class twin; both come from the same tokens, so build every screen against
|
|
7
|
+
`var(--…)` and never a literal.
|
|
8
|
+
|
|
9
|
+
**The page is a sequence of acts on a fixed beat.** Two acts, then the same call to
|
|
10
|
+
action, again. The reader is never more than two acts from the next one, and the
|
|
11
|
+
cadence — not a divider or a colour — is what keeps a long page legible.
|
|
12
|
+
|
|
13
|
+
**One dark act, at the middle.** `--stage` is a measured gradient and the pack allows
|
|
14
|
+
exactly one block of it per page. A second makes the page read as a section list, and
|
|
15
|
+
it is the most likely drift because the block is the easiest thing on the page to
|
|
16
|
+
like.
|
|
17
|
+
|
|
18
|
+
**The control stays nearly square.** Radius 4 on buttons against 16 on cards, both
|
|
19
|
+
measured. Closing that gap is the single fastest way to make a page in this pack look
|
|
20
|
+
like every other generated landing page.
|
|
21
|
+
|
|
22
|
+
**The product is on screen before any claim is made**, inside a frame the fold cuts
|
|
23
|
+
off. `Frame cropped` is the signature: the panel runs off the viewport rather than
|
|
24
|
+
sitting complete inside it.
|
|
25
|
+
|
|
26
|
+
**Three shadows, three jobs.** `--shadow-card` (94px of blur, violet-tinted) carries
|
|
27
|
+
an argument card; `--shadow-hair` (a hard 1px 2px, no blur) carries a container card;
|
|
28
|
+
`--shadow-control` belongs to the primary button. A control never takes the card's
|
|
29
|
+
shadow and a card never takes the control's. Inside the dark act there is no shadow
|
|
30
|
+
at all — at 9% on a gradient it reads as dirt.
|
|
31
|
+
|
|
32
|
+
**Status is never by colour alone.** Every state is a dot or an icon **plus a word**,
|
|
33
|
+
and `StatusDot` makes the label a required prop so the rule cannot be skipped by
|
|
34
|
+
omission.
|
|
35
|
+
|
|
36
|
+
**Two status colours and the whole dark register are pack decisions, not
|
|
37
|
+
measurements.** The reference paints no success and no error state and has no dark
|
|
38
|
+
mode; the token layer marks each substitution at its declaration. Read them as
|
|
39
|
+
decisions, and re-check them if the reference ever ships the real thing.
|
|
40
|
+
|
|
41
|
+
**Bans** (verbatim from the pack):
|
|
42
|
+
|
|
43
|
+
- No second dark act; no scroll clock, no scrub, no parallax.
|
|
44
|
+
- No status by colour alone.
|
|
45
|
+
- No card shadow on a control, and no control shadow on a card.
|
|
46
|
+
- No closing the 4/16 radius gap.
|
|
47
|
+
- No second family, and no swapping Inter for a system stack "for now".
|
|
48
|
+
- No logo wall and no invented counter — a page with no real logos builds its proof
|
|
49
|
+
rail from product facts and leaves no hole where one would go.
|
|
50
|
+
- No bloom under running text; no fixed CTA bar on a phone.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @sheleg-design/proscenium
|
|
2
|
+
|
|
3
|
+
The React reference kit for the SHELEG **Proscenium** style pack — a white field
|
|
4
|
+
carrying two cool acts and one deep indigo act at the middle (light default, dark
|
|
5
|
+
twin), an electric violet filling a control that stays nearly square at 4px against
|
|
6
|
+
cards at 16, and a framed product panel the fold cuts off.
|
|
7
|
+
|
|
8
|
+
It is generated from the pack, not authored beside it: `src/styles.css` opens with
|
|
9
|
+
`styles/tokens/proscenium.css` byte for byte, and the rules the design agent must obey
|
|
10
|
+
are in [`.design-sync/conventions.md`](./.design-sync/conventions.md).
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install && npm run build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## The spine
|
|
17
|
+
|
|
18
|
+
`Button`, `Card`, `Chip`, `Stat`, `Heading`, `Rule` — identical names, props and
|
|
19
|
+
types in every SHELEG kit. Switching packs swaps identity, not API.
|
|
20
|
+
|
|
21
|
+
## This pack's own
|
|
22
|
+
|
|
23
|
+
`Frame` is the signature element — the proscenium arch, and `cropped` is the prop
|
|
24
|
+
that carries it. `Stage` is the one dark act, and the pack allows exactly one per
|
|
25
|
+
page. `StatusDot` requires its label, because this pack states status is never by
|
|
26
|
+
colour alone. `Skeleton` is static. See
|
|
27
|
+
[`styles/proscenium.md`](../../plugins/sheleg-design/skills/sheleg-design/styles/proscenium.md).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sheleg-design/proscenium",
|
|
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,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Actions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Min-height 44, radius **4**, weight 600, label at 16 — and the radius is the
|
|
6
|
+
point. The reference's control is nearly square while its cards sit at 16, and
|
|
7
|
+
that gap is what keeps a roomy page from reading as soft. Rounding this to match
|
|
8
|
+
the card is the single most likely edit to make a page in this pack generic.
|
|
9
|
+
|
|
10
|
+
`primary` fills with the accent, carries `--shadow-control`, and states its
|
|
11
|
+
label colour on the rule rather than inheriting it, because an anchor reset that
|
|
12
|
+
says `color: inherit` will otherwise win the label and paint ink on accent.
|
|
13
|
+
`secondary` is the 1px `--edge` bordered one with no shadow; `ghost` carries no
|
|
14
|
+
fill and takes `--accent-deep` for its label.
|
|
15
|
+
|
|
16
|
+
**Active flattens the shadow instead of moving the control.** Nothing translates
|
|
17
|
+
and nothing scales — a button that pops on click has left this pack.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Button onClick={book}>Book a walkthrough</Button>
|
|
21
|
+
<Button variant="secondary" onClick={tour}>See it in action</Button>
|
|
22
|
+
<Button variant="ghost" size="sm" onClick={dismiss}>Not now</Button>
|
|
23
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ButtonProps {
|
|
4
|
+
/** `primary` is the accent fill — the pack allows 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={['ps-btn', `ps-btn--${variant}`, `ps-btn--${size}`, className]
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join(' ')}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Radius 16, and **two kinds that are not interchangeable.** The default is the
|
|
6
|
+
container card: `--panel`, a 1px `--border`, and `--shadow-hair` — the measured
|
|
7
|
+
hard `1px 2px 0` with no blur at all. `--argument` drops the border and takes
|
|
8
|
+
`--shadow-card`, which is 94px of blur at 4px of offset in a violet-tinted
|
|
9
|
+
black. That one is for the card carrying the act's argument, not for every card
|
|
10
|
+
in a grid.
|
|
11
|
+
|
|
12
|
+
**A card is for a group.** A list of statements takes a seam and no box; boxing
|
|
13
|
+
prose is the fastest way to make this pack look like a template.
|
|
14
|
+
|
|
15
|
+
**Inside the dark act the shadow is gone**, not softened: `--shadow-card` at 9%
|
|
16
|
+
is invisible on the gradient and reads as dirt. The stylesheet already swaps a
|
|
17
|
+
card inside `.ps-stage` to `--stage-panel` with no shadow.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Card title="Managed accounts" meta="4 held">
|
|
21
|
+
<AccountRows />
|
|
22
|
+
</Card>
|
|
23
|
+
<Card className="ps-card--argument" title="Know who holds every account">
|
|
24
|
+
<AccessTable />
|
|
25
|
+
</Card>
|
|
26
|
+
```
|
|
@@ -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, a window, a state. */
|
|
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={['ps-card', className].filter(Boolean).join(' ')}>
|
|
15
|
+
{head && (
|
|
16
|
+
<div className="ps-card__head">
|
|
17
|
+
{title !== undefined && <h3 className="ps-card__title">{title}</h3>}
|
|
18
|
+
{meta !== undefined && <span className="ps-card__meta">{meta}</span>}
|
|
19
|
+
</div>
|
|
20
|
+
)}
|
|
21
|
+
<div className="ps-card__body">{children}</div>
|
|
22
|
+
</section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A pill at `--r-pill`, 13px, weight 500. `neutral` sits on `--panel` inside a
|
|
6
|
+
`--border-strong` hairline; `accent` takes `--accent-weak` with `--accent-deep`
|
|
7
|
+
as its word.
|
|
8
|
+
|
|
9
|
+
**Radius 16 on a chip is a mistake, not a variant.** A 28px-tall chip at
|
|
10
|
+
`--r-card` is a lozenge — the pack names this one explicitly because it is what
|
|
11
|
+
happens when a card token gets reused for a control.
|
|
12
|
+
|
|
13
|
+
`selected` keeps its state after the pointer leaves; hover does not.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Chip>Client / Acme · 10:42</Chip>
|
|
17
|
+
<Chip tone="accent" selected>Needs attention</Chip>
|
|
18
|
+
```
|
|
@@ -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
|
+
'ps-chip',
|
|
15
|
+
`ps-chip--${tone}`,
|
|
16
|
+
selected ? 'ps-chip--selected' : undefined,
|
|
17
|
+
className,
|
|
18
|
+
]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(' ')}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Signature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
**The proscenium arch** — a translucent fill, a 1px `--frame-edge` hairline and
|
|
6
|
+
an inset white glow around a product view. Measured off the reference's hero
|
|
7
|
+
container, `--shadow-frame` and `--frame-fill` together.
|
|
8
|
+
|
|
9
|
+
`cropped` is the part that carries the pack. The panel is meant to be **cut off
|
|
10
|
+
by the fold**: it drops its bottom padding, its bottom border and its bottom
|
|
11
|
+
radii so the product runs off the viewport rather than sitting complete inside
|
|
12
|
+
it. A page in this pack is remembered as the one where the product was already
|
|
13
|
+
there, and a frame that closes politely above the fold is the version nobody
|
|
14
|
+
remembers.
|
|
15
|
+
|
|
16
|
+
`foot` is the honesty line: a frame that shows an interface without saying what
|
|
17
|
+
it is drawn from is a mock wearing evidence's clothes.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Frame cropped caption="What needs attention" foot="Drawn from the product's own routes">
|
|
21
|
+
<ConsolePanel />
|
|
22
|
+
</Frame>
|
|
23
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FrameProps {
|
|
4
|
+
caption?: string;
|
|
5
|
+
/** The honesty line under the panel: what this view is drawn from. */
|
|
6
|
+
foot?: string;
|
|
7
|
+
/** The fold cuts the panel off — the pack's signature. See Frame.md. */
|
|
8
|
+
cropped?: boolean;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Frame({ caption, foot, cropped = false, children, className }: FrameProps) {
|
|
14
|
+
return (
|
|
15
|
+
<figure
|
|
16
|
+
className={['ps-frame', cropped ? 'ps-frame--cropped' : undefined, className]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(' ')}
|
|
19
|
+
>
|
|
20
|
+
{caption !== undefined && <figcaption className="ps-frame__cap">{caption}</figcaption>}
|
|
21
|
+
<div className="ps-frame__glass">{children}</div>
|
|
22
|
+
{foot !== undefined && !cropped && <p className="ps-frame__foot">{foot}</p>}
|
|
23
|
+
</figure>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Three levels, all fluid, all one family: level 1 is `--t-hero` (27px at 390 →
|
|
6
|
+
62px at 1024, both endpoints measured), level 2 is `--t-act` (28 → 62), level 3
|
|
7
|
+
is `--t-feature` (28 → 39).
|
|
8
|
+
|
|
9
|
+
The weights are the hierarchy, because the family never changes: 600 on the
|
|
10
|
+
display and the act heading, 700 on the feature heading. The reference loads
|
|
11
|
+
Inter at nine weights and no second face anywhere on the page.
|
|
12
|
+
|
|
13
|
+
Level 1 is capped at 32ch, which holds it to two lines at 1440. A headline that
|
|
14
|
+
reaches five lines is a broken hero, not a long one.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<Heading level={1}>Your company runs in Telegram</Heading>
|
|
18
|
+
<Heading>Everything around the conversation, in one place</Heading>
|
|
19
|
+
<Heading level={3}>Know who holds every account</Heading>
|
|
20
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HeadingProps {
|
|
4
|
+
/** 1 = page title (30px), 2 = section (24px), 3 = card title (16px). */
|
|
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={['ps-heading', `ps-heading--${level}`, className].filter(Boolean).join(' ')}
|
|
15
|
+
>
|
|
16
|
+
{children}
|
|
17
|
+
</Tag>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A 1px `--border` — the measured seam, and the reference's most-used border by a
|
|
6
|
+
factor of two. `strong` is `--border-strong` and belongs where a divider has to
|
|
7
|
+
be *seen*: between two groups of rows, not between two rows.
|
|
8
|
+
|
|
9
|
+
Unlike the hairline packs, a rule here is not the elevation model. This pack has
|
|
10
|
+
three named shadows doing three different jobs, and a rule is for structure
|
|
11
|
+
inside a surface that is already separated.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Rule />
|
|
15
|
+
<Rule tone="strong" />
|
|
16
|
+
```
|
|
@@ -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
|
+
'ps-rule',
|
|
11
|
+
tone === 'strong' ? 'ps-rule--strong' : undefined,
|
|
12
|
+
className,
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(' ')}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The loading idiom, and it is **static**. Blocks at `--r-inner` filled
|
|
6
|
+
`--panel-2`, sized to the element they stand in for — same heights, same widths,
|
|
7
|
+
so nothing jumps when the data lands.
|
|
8
|
+
|
|
9
|
+
No spinner under 400ms of expected wait, and never a spinner inside a frame that
|
|
10
|
+
has already drawn its arch: the frame is the promise that something is coming,
|
|
11
|
+
and a spinner inside it says the same thing twice.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<Skeleton width="38%" />
|
|
15
|
+
<Skeleton width="72%" />
|
|
16
|
+
<Skeleton height={64} />
|
|
17
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SkeletonProps {
|
|
2
|
+
/** Matches the real element's height, so the layout does not jump on load. */
|
|
3
|
+
height?: number;
|
|
4
|
+
/** A CSS width — a percentage for text lines, a length for blocks. */
|
|
5
|
+
width?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Skeleton({ height = 14, width = '100%', className }: SkeletonProps) {
|
|
10
|
+
return (
|
|
11
|
+
<span
|
|
12
|
+
className={['ps-skeleton', className].filter(Boolean).join(' ')}
|
|
13
|
+
style={{ height: `${height}px`, width }}
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Signature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
**The one dark act.** A full-bleed `--stage` gradient block — measured stops,
|
|
6
|
+
`#07061d` to `#2a0b78` at 86.41% — placed once at the middle of the page.
|
|
7
|
+
|
|
8
|
+
**Once.** A second `--stage` block makes the page read as a section list rather
|
|
9
|
+
than as a design with a middle, and it is this pack's most likely drift because
|
|
10
|
+
the block is the easiest thing on the page to like. The measured reference has
|
|
11
|
+
exactly one.
|
|
12
|
+
|
|
13
|
+
Inside it, cards separate by `--stage-panel` and a seam rather than by
|
|
14
|
+
elevation; the stylesheet does that swap for any `.ps-card` inside `.ps-stage`,
|
|
15
|
+
so a card does not have to know where it is standing.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Stage>
|
|
19
|
+
<Heading>One operating layer for every team working in Telegram</Heading>
|
|
20
|
+
<UseCaseCards />
|
|
21
|
+
</Stage>
|
|
22
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface StageProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The one dark act. One per page — see Stage.md. */
|
|
9
|
+
export function Stage({ children, className }: StageProps) {
|
|
10
|
+
return (
|
|
11
|
+
<section className={['ps-stage', className].filter(Boolean).join(' ')}>
|
|
12
|
+
<div className="ps-stage__inner">{children}</div>
|
|
13
|
+
</section>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Data
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Label above at `--t-meta` uppercase, tracked `--track-wide`, in `--muted`;
|
|
6
|
+
figure at `--t-card` weight 600 in `--ink-strong`; source below in
|
|
7
|
+
`--font-data`.
|
|
8
|
+
|
|
9
|
+
The label comes first so the eye lands on the figure without hunting. `source`
|
|
10
|
+
is optional and should almost always be given — a figure with no provenance is
|
|
11
|
+
the thing this library's own doctrine bans, and on a page whose whole argument
|
|
12
|
+
is a demonstration an unsourced number is the one element that can make the
|
|
13
|
+
demonstration read as a mock.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Stat label="Median response" value="2h 14m" source="past 30 days" />
|
|
17
|
+
```
|