sheleg-design-skill 1.54.0 → 1.55.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 +129 -0
- package/README.md +5 -4
- package/bin/cli.js +132 -1
- package/cursor/rules/sheleg-design.mdc +8 -0
- package/kits/chorus/.design-sync/config.json +14 -0
- package/kits/chorus/.design-sync/conventions.md +50 -0
- package/kits/chorus/README.md +36 -0
- package/kits/chorus/package.json +29 -0
- package/kits/chorus/src/Bubble.md +23 -0
- package/kits/chorus/src/Bubble.tsx +40 -0
- package/kits/chorus/src/Button.md +23 -0
- package/kits/chorus/src/Button.tsx +33 -0
- package/kits/chorus/src/Capsule.md +20 -0
- package/kits/chorus/src/Capsule.tsx +21 -0
- package/kits/chorus/src/Card.md +20 -0
- package/kits/chorus/src/Card.tsx +23 -0
- package/kits/chorus/src/Chip.md +15 -0
- package/kits/chorus/src/Chip.tsx +25 -0
- package/kits/chorus/src/Delta.md +19 -0
- package/kits/chorus/src/Delta.tsx +24 -0
- package/kits/chorus/src/Empty.md +18 -0
- package/kits/chorus/src/Empty.tsx +19 -0
- package/kits/chorus/src/Field.md +16 -0
- package/kits/chorus/src/Field.tsx +40 -0
- package/kits/chorus/src/GridFrame.md +19 -0
- package/kits/chorus/src/GridFrame.tsx +34 -0
- package/kits/chorus/src/Heading.md +16 -0
- package/kits/chorus/src/Heading.tsx +17 -0
- package/kits/chorus/src/NavPill.md +19 -0
- package/kits/chorus/src/NavPill.tsx +19 -0
- package/kits/chorus/src/Rule.md +14 -0
- package/kits/chorus/src/Rule.tsx +12 -0
- package/kits/chorus/src/Skeleton.md +16 -0
- package/kits/chorus/src/Skeleton.tsx +27 -0
- package/kits/chorus/src/Slab.md +18 -0
- package/kits/chorus/src/Slab.tsx +17 -0
- package/kits/chorus/src/Stat.md +15 -0
- package/kits/chorus/src/Stat.tsx +17 -0
- package/kits/chorus/src/Sweep.md +17 -0
- package/kits/chorus/src/Sweep.tsx +16 -0
- package/kits/chorus/src/Well.md +15 -0
- package/kits/chorus/src/Well.tsx +16 -0
- package/kits/chorus/src/index.ts +37 -0
- package/kits/chorus/src/styles.css +878 -0
- package/kits/chorus/tsconfig.json +15 -0
- package/package.json +3 -3
- 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 +2 -2
- package/plugins/sheleg-design/skills/sheleg-design/STYLE_PACK_INDEX.md +3 -2
- package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +6 -5
- package/plugins/sheleg-design/skills/sheleg-design/styles/STYLE_PACK_TEMPLATE.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/chorus.md +474 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/deskmate.md +10 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/surveyor.md +11 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tenor.md +1 -1
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/chorus.css +386 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,135 @@ follow [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.55.0] - 2026-08-30
|
|
10
|
+
|
|
11
|
+
### The thirty-ninth pack — somebody else's question, on a sheet that never stops being ruled
|
|
12
|
+
|
|
13
|
+
`chorus`, extracted from `crowdreply.io`. Warm off-white paper under a construction
|
|
14
|
+
grid that never switches off — two 1px verticals at `x=162` and `x=1278` of 1440,
|
|
15
|
+
crossed by horizontals carrying a small plus at each intersection, 63 rule elements in
|
|
16
|
+
the census, and the same grid continued across the dark band. The set piece is a
|
|
17
|
+
**cut-corner bubble**: 24px on three corners and 0 on the top right, measured on all 24
|
|
18
|
+
instances of one object, holding a stranger's question in the display face.
|
|
19
|
+
|
|
20
|
+
Named for the register per ADR-0001, with `switchboard`, `soapbox`, `quorum` and
|
|
21
|
+
`earshot` weighed and rejected — the record is in the ADR.
|
|
22
|
+
|
|
23
|
+
**Measured, not restated.** The site is Framer, and the token dump and the render
|
|
24
|
+
disagree: of its 52 declared `--token-<uuid>` properties, 15 are referenced once and
|
|
25
|
+
reach no painted surface, so an area-weighted census over all 2,439 elements of an
|
|
26
|
+
11,750px page decided every value. Read through CDP at 1440×900, an emulated
|
|
27
|
+
768×1024×2 and an emulated 390×844×2. Ratios computed by importing this repository's
|
|
28
|
+
own palette gate.
|
|
29
|
+
|
|
30
|
+
**Six corrections, each a number the reference produces and the pack refuses.**
|
|
31
|
+
|
|
32
|
+
- The primary CTA fails AA and no size saves it — `#ffffff` on `#f96f4b` is **2.84:1**,
|
|
33
|
+
under the 3:1 large floor too. The fix keeps the brand hue exactly and changes the
|
|
34
|
+
label: `#1b181c` on `#f96f4b` is **6.20:1**. Darkening the coral was rejected.
|
|
35
|
+
- The coral cannot be a word: `#f96f4b` on `#fbfaf9` is **2.72:1**, the link token
|
|
36
|
+
`#ff5d30` is **2.94:1**. `--coral-ink` at 4.59:1 is derived at hue 35.7°.
|
|
37
|
+
- A card is separated from the field by **1.04:1** and nothing else — no border and no
|
|
38
|
+
shadow on any card over 250×150px in the census. Every card gains a 1px `--line`.
|
|
39
|
+
- **Zero `:focus` rules and zero `:focus-visible` rules** across 274,355 bytes, plus two
|
|
40
|
+
`outline: none` declarations. The whole mechanism is the pack's, including the
|
|
41
|
+
dark-surface re-declaration: the ink ring is 1.00:1 against `--slab`.
|
|
42
|
+
- The muted step does not read: `#8a8692` on `#fbfaf9` is **3.41:1** at 14px/400, 38
|
|
43
|
+
instances.
|
|
44
|
+
- **Zero reduced-motion rules against 383 script-set inline transforms** and 1,057
|
|
45
|
+
script-set opacities. The media query the pack adds cannot reach them, so the JS half
|
|
46
|
+
of the contract is stated in the pack rather than implied.
|
|
47
|
+
|
|
48
|
+
Two more facts recorded because they will surprise an implementer: the controls are
|
|
49
|
+
36px and miss the 44px tap floor, so the kit pads the hit area rather than resizing the
|
|
50
|
+
button; and the declared amber `#ffc300` paints nothing at any of the three widths,
|
|
51
|
+
which is why this pack has three status roles rather than four and says so.
|
|
52
|
+
|
|
53
|
+
**The mint is dark-only, and that is a constraint rather than an omission.** `#36ff94`
|
|
54
|
+
is 13.31:1 in a well and reaches AA on paper only at near-black, which is a different
|
|
55
|
+
colour rather than a step — so the light ladder is a separate answer, not a
|
|
56
|
+
translation.
|
|
57
|
+
|
|
58
|
+
### Also in this release
|
|
59
|
+
|
|
60
|
+
- Reciprocal forks: `chorus` ↔ `surveyor` (the AI-visibility collision — a reading
|
|
61
|
+
against a conversation) and `chorus` ↔ `deskmate` (the speech-bubble collision —
|
|
62
|
+
whose bubble it is).
|
|
63
|
+
- `kits/chorus`: the six-name spine plus `Bubble`, `GridFrame`, `Slab`, `Well`,
|
|
64
|
+
`Delta`, `NavPill`, `Sweep`, `Field`, `Empty`, `Skeleton`, `Capsule`. Rendered at
|
|
65
|
+
1440 / 768 / 390 and its computed values read back against the pack's claims before
|
|
66
|
+
the tag; the log is in `docs/evidence/plans/2026-08-30-chorus-pack-acceptance.md`.
|
|
67
|
+
- The kit's button carries no border on any variant: the reference's control is 36px
|
|
68
|
+
with 8px/16px padding and no edge, and a 1px border measured 38px. The ghost
|
|
69
|
+
variant's line is an inset ring instead, so no variant shifts when it swaps.
|
|
70
|
+
- Counted claims moved 38 → 39 across eleven files, and the widened-contract remainder
|
|
71
|
+
moved 32 → 33 — the number that had been stale in two consecutive releases.
|
|
72
|
+
- `SURFACE_COMPOSITION.md`: `chorus` joins the status map, and the accent-role
|
|
73
|
+
paragraph gains the fourth pack that names its accent itself (`--coral`).
|
|
74
|
+
|
|
75
|
+
### Shipped with a stated debt
|
|
76
|
+
|
|
77
|
+
Scenario **T37** (`chorus` against `surveyor`, with its negative branch) is **written,
|
|
78
|
+
not yet run** — the filing session was instructed not to spawn subagents, so the two
|
|
79
|
+
blind branches could not execute before the tag. Per standing instruction 3 the debt
|
|
80
|
+
ships in the same commit as the artifact and is on the board. The render check behind
|
|
81
|
+
REQ-8 is *not* part of that debt: it ran in full.
|
|
82
|
+
|
|
83
|
+
## [1.54.1] - 2026-08-29
|
|
84
|
+
|
|
85
|
+
### The installer refuses the shadow it documents
|
|
86
|
+
|
|
87
|
+
Both installers write a plain skill copy on request, and on a machine where
|
|
88
|
+
`sheleg-design` is installed as a Claude Code plugin, a write to that home's
|
|
89
|
+
`~/.claude/skills/sheleg-design` is a copy that shadows the plugin and serves
|
|
90
|
+
this frozen version forever. Neither installer checked the plugin channel at
|
|
91
|
+
all, and CI tested a fresh HOME only, so the plugin-present case had never run
|
|
92
|
+
anywhere. Canon: make-skill v0.25.0, `references/distribution.md` §"The
|
|
93
|
+
installer must refuse the shadow it documents"; family audit SHD-07/UM-03.
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- `bin/cli.js` and `install.sh` now consult the **target home's**
|
|
98
|
+
`~/.claude/plugins/installed_plugins.json` before any write to that home's
|
|
99
|
+
`~/.claude/skills/sheleg-design`, and refuse with **exit 3** when the plugin
|
|
100
|
+
channel owns the skill. The refusal names the real spec read from the JSON
|
|
101
|
+
(`sheleg-design@<marketplace>` — the marketplace name differs from the
|
|
102
|
+
plugin name here, and a remedy that guesses it sends the operator to a
|
|
103
|
+
marketplace that does not exist), prints the plugin-channel remedy
|
|
104
|
+
(`claude plugin marketplace update` + `claude plugin update <spec>`, plus
|
|
105
|
+
the family launcher line), and offers `--force` as the deliberate override.
|
|
106
|
+
The `plugins/marketplaces/` directory is read only as the fallback signal —
|
|
107
|
+
it under-reports (a `directory`-sourced marketplace has no dir there), which
|
|
108
|
+
is the fail-open class the canon names. A missing or unparsable JSON reads
|
|
109
|
+
as "no plugin": fail open, never crash. Only the Claude Code channel is
|
|
110
|
+
gated — `.cursor/` and every other agent's install are untouched, and a
|
|
111
|
+
project-level `.claude/` falls open naturally because a project holds no
|
|
112
|
+
plugin registry.
|
|
113
|
+
- `install.sh` accepts `--force` (the override for the gate above) and refuses
|
|
114
|
+
unknown flags with exit 2 instead of treating them as a target directory.
|
|
115
|
+
- `test/installer_test.js` — thirteen cases against throwaway HOMEs, wired
|
|
116
|
+
into `npm test` and CI: plugin-present (exit 3 + remedy + nothing written,
|
|
117
|
+
all three asserted), the differently-named marketplace spec carried into the
|
|
118
|
+
remedy, `--force` installing, corrupt JSON installing, a prefix-collider
|
|
119
|
+
(`sheleg-design-extra@x`) not falsely refused, marketplaces-dir-only still
|
|
120
|
+
refusing, fresh HOME still installing, the `.cursor` channel untouched by
|
|
121
|
+
the gate, and the install.sh mirrors of the same. Watched failing first: run
|
|
122
|
+
against the pre-fix installers, 7 cases red.
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
|
|
126
|
+
- Both installers now end a successful install by saying how the next version
|
|
127
|
+
arrives (`npx sheleg-design-skill@latest --force`, or the family launcher) —
|
|
128
|
+
an installer that never mentions updates has still chosen an update model:
|
|
129
|
+
never.
|
|
130
|
+
- `bin/cli.js --help` documents the exit-code contract, including the new
|
|
131
|
+
exit 3.
|
|
132
|
+
- CONTRIBUTING: release tags must be **annotated** (`git tag -a`) — v1.53.0
|
|
133
|
+
and v1.54.0 were lightweight, and `git submodule status` describes a pinned
|
|
134
|
+
commit with `git describe`, which ignores lightweight tags, so the family
|
|
135
|
+
umbrella misreported the member's version (SHD-07/UM-03). Applies from this
|
|
136
|
+
release forward; the old tags are not re-cut.
|
|
137
|
+
|
|
9
138
|
## [1.54.0] - 2026-08-29
|
|
10
139
|
|
|
11
140
|
### The thirty-eighth pack — the terrain is mapped
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ problem — invented colors, six accent hues, dark mode retrofitted later.
|
|
|
31
31
|
|
|
32
32
|
This skill is the taste layer. It gives a coding agent **one motion
|
|
33
33
|
methodology** for cinematic, scroll-driven pages, **a motion doctrine** that
|
|
34
|
-
decides whether to animate before it decides how, and **thirty-
|
|
34
|
+
decides whether to animate before it decides how, and **thirty-nine locked style
|
|
35
35
|
packs** with ready-made design tokens, so what it builds reads as one system
|
|
36
36
|
instead of a pile of effects.
|
|
37
37
|
|
|
@@ -78,6 +78,7 @@ into the cinematic layer, and says so in its own *Motion flavor* section.
|
|
|
78
78
|
| `deskmate` | a warm beige working day lit from **one source above the top edge** — every gradient origin in the reference sits at or above `y=0` — where a single four-stop ramp (peach, lilac, violet, deep navy) washes the field, fills a panel and fills one word of a heading, and everything a hand touches is a 56px pill against 32px slabs | **products sold as a colleague rather than a tool** — AI employees and chat-native agents, assistants living in Slack or Teams, delegation and approval surfaces (standalone) |
|
|
79
79
|
| `test-drive` | warm paper and white cards where **the live product runs inside drawn browser chrome** — traffic-light dots, a real address, the engine on — one coral in two tokens (the brand hue lights, fills and blinks; a darker step carries every body-size word), every control lit by a four-layer shadow in its own hue, a handwritten face and a yellow marker annotating from the margin | **self-serve SaaS sold by handing over the running product** — analytics, dashboards, developer tools with a visible console, indie SaaS (standalone) |
|
|
80
80
|
| `surveyor` | peach paper where **an unseen terrain is mapped** — fine contour lines as the one texture, flat white and tinted cards with no shadow anywhere, a teal whose text-bearing steps are the reference's own hover ladder, a pink that answers but may not speak, and the product appearing only as flat portraits | **visibility, monitoring and benchmark tools** — AI-search and SEO trackers, share-of-voice and brand-listening platforms sold to marketing teams (standalone) |
|
|
81
|
+
| `chorus` | warm off-white paper under **a construction grid that never switches off** — two 1px verticals crossed by horizontals with a small plus at every intersection, continued across the dark band — where the set piece is **somebody else's question** in a cut-corner bubble, 24px on three corners and 0 on the top right, set in the display face; white cards with a hairline and no shadow, near-black slabs cut 30px into the sheet, one coral that fills but may never be a word | **AI-search visibility, brand monitoring and community marketing** — answer-engine and share-of-voice trackers, social listening, review and reputation tools whose proof is other people's conversations (standalone) |
|
|
81
82
|
| `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 |
|
|
82
83
|
| `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 |
|
|
83
84
|
| `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 |
|
|
@@ -178,7 +179,7 @@ skills.
|
|
|
178
179
|
| `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 |
|
|
179
180
|
| `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 |
|
|
180
181
|
| `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 |
|
|
181
|
-
| `styles/*.md` | The thirty-
|
|
182
|
+
| `styles/*.md` | The thirty-nine style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
|
|
182
183
|
| `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) |
|
|
183
184
|
| `styles/STYLE_PACK_TEMPLATE.md` | The pack contract as a skeleton, so a new style is authored against the same headings rather than improvised |
|
|
184
185
|
|
|
@@ -249,7 +250,7 @@ cd ./ds-workbench && npm install && npm run build
|
|
|
249
250
|
then `/design-sync` in that directory, from Claude Code. Three layers cross: the
|
|
250
251
|
pack's **bans** as the design system's own README, `styles.css` built from
|
|
251
252
|
`tokens/<pack>.css` verbatim, and the components — a six-name spine that is
|
|
252
|
-
identical in all thirty-
|
|
253
|
+
identical in all thirty-nine kits, so switching packs swaps identity rather than API,
|
|
253
254
|
plus each pack's signature parts. **Motion does not cross**, exactly as it does
|
|
254
255
|
not cross into Figma: a kit is the static half of a pack, and saying so is what
|
|
255
256
|
stops an agent inventing motion to fill the silence.
|
|
@@ -299,7 +300,7 @@ a pack's four widened sections used to make two gates *quieter* and still green.
|
|
|
299
300
|
One honest limit: the npx installer is checked by asserting its runtime bundle
|
|
300
301
|
walker exists, not by reading a file list — it has none by design. What proves
|
|
301
302
|
it ships the right files is CI, which installs the bundle through **both**
|
|
302
|
-
installers and `diff -r`s the result against the source, then builds all thirty-
|
|
303
|
+
installers and `diff -r`s the result against the source, then builds all thirty-nine
|
|
303
304
|
kits.
|
|
304
305
|
|
|
305
306
|
`test/scenarios.md` (T1–T34) is the behavioral harness: fresh subagents given a
|
package/bin/cli.js
CHANGED
|
@@ -225,6 +225,13 @@ ${c("bold", "Default")}
|
|
|
225
225
|
Auto-detects: uses .cursor/ if present, else .claude/ if present,
|
|
226
226
|
otherwise creates .cursor/skills/${SKILL_SLUG}/.
|
|
227
227
|
|
|
228
|
+
${c("bold", "Exit codes")}
|
|
229
|
+
0 installed or skipped 2 usage error
|
|
230
|
+
1 packaging bug, or 3 refused: the sheleg-design PLUGIN is installed
|
|
231
|
+
overwrite refused in the target home — a plain copy in
|
|
232
|
+
~/.claude/skills would shadow it and serve this
|
|
233
|
+
frozen version forever (--force overrides)
|
|
234
|
+
|
|
228
235
|
${c("bold", "What it installs")}
|
|
229
236
|
SKILL.md the agent-facing skill (discovery + principles)
|
|
230
237
|
SHELEG_DESIGN.md the full reference (architecture, recipes, why it works)
|
|
@@ -234,7 +241,7 @@ ${c("bold", "What it installs")}
|
|
|
234
241
|
DESIGN_SYNC_BRIDGE.md the Claude Design contract (what a pack sends, and
|
|
235
242
|
what does not cross)
|
|
236
243
|
AI_PRODUCT_PATTERNS.md chat / agent / streaming surfaces (honest state)
|
|
237
|
-
styles/ thirty-
|
|
244
|
+
styles/ thirty-nine style packs — instrument-console (dark console),
|
|
238
245
|
editorial-luxury (warm editorial), workbench (light/dark
|
|
239
246
|
product UI), briefing-room (dark 16:9 presentation deck),
|
|
240
247
|
atrium (warm cream consumer health), patchbay (dark live
|
|
@@ -258,6 +265,10 @@ ${c("bold", "What it installs")}
|
|
|
258
265
|
contour lines, flat shadowless cards, a teal that
|
|
259
266
|
states and a pink that answers, and the product
|
|
260
267
|
shown only as portraits),
|
|
268
|
+
chorus (warm paper under a crosshair grid, a
|
|
269
|
+
cut-corner bubble carrying somebody else's
|
|
270
|
+
question, near-black slabs cut into the sheet, one
|
|
271
|
+
coral that fills but never writes),
|
|
261
272
|
babylove (white +
|
|
262
273
|
one orange, seven tokens over Tailwind defaults),
|
|
263
274
|
outrank (white +
|
|
@@ -328,6 +339,112 @@ function resolveTargetDir(opts, cwd) {
|
|
|
328
339
|
return path.join(cwd, ".cursor", "skills", SKILL_SLUG);
|
|
329
340
|
}
|
|
330
341
|
|
|
342
|
+
// Exit codes are the contract: 0 installed or skipped, 1 packaging bug or
|
|
343
|
+
// overwrite refusal, 2 usage error, 3 refused — the plugin channel owns the
|
|
344
|
+
// target home's Claude Code install (--force overrides).
|
|
345
|
+
const EXIT_PLUGIN_PRESENT = 3;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The home whose Claude Code channel this write would land in, or null.
|
|
349
|
+
*
|
|
350
|
+
* Only the Claude Code channel is gated: a write to `<H>/.claude/skills/
|
|
351
|
+
* sheleg-design` is the shape that can shadow a plugin installed in `<H>`.
|
|
352
|
+
* `.cursor/` and every other agent's skill directory have no plugin channel,
|
|
353
|
+
* so installs there are untouched by the check. A project-level `.claude/`
|
|
354
|
+
* matches this shape too, and falls open naturally: a project holds no
|
|
355
|
+
* `plugins/installed_plugins.json`, so the gate reads "no plugin" and the
|
|
356
|
+
* install proceeds.
|
|
357
|
+
*/
|
|
358
|
+
function claudeHomeOf(targetDir) {
|
|
359
|
+
const parts = path.resolve(targetDir).split(path.sep);
|
|
360
|
+
const tail = parts.slice(-3).join("/");
|
|
361
|
+
if (tail !== `.claude/skills/${SKILL_SLUG}`) return null;
|
|
362
|
+
return parts.slice(0, -3).join(path.sep) || path.sep;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The plugin spec (`<name>@<marketplace>`) installed for sheleg-design in
|
|
367
|
+
* this home, or null.
|
|
368
|
+
*
|
|
369
|
+
* `installed_plugins.json` is the record of what is actually installed. The
|
|
370
|
+
* `plugins/marketplaces/<name>` directory under-reports: a marketplace added
|
|
371
|
+
* from a local `directory` source has no dir there at all, and plugin names
|
|
372
|
+
* differ from marketplace names — this very plugin is `sheleg-design` shipped
|
|
373
|
+
* from the `sheleg-design-skill` marketplace — so a check keyed on the dir
|
|
374
|
+
* alone stays green while the shadow lands. Absence and corruption both read
|
|
375
|
+
* as "no plugin": the fresh HOME is the common case, and an installer that
|
|
376
|
+
* crashes on a parse error refuses the machines that need it most.
|
|
377
|
+
*/
|
|
378
|
+
function installedPluginSpec(home) {
|
|
379
|
+
try {
|
|
380
|
+
const raw = fs.readFileSync(
|
|
381
|
+
path.join(home, ".claude", "plugins", "installed_plugins.json"),
|
|
382
|
+
"utf8",
|
|
383
|
+
);
|
|
384
|
+
const parsed = JSON.parse(raw);
|
|
385
|
+
const plugins =
|
|
386
|
+
parsed &&
|
|
387
|
+
typeof parsed === "object" &&
|
|
388
|
+
parsed.plugins &&
|
|
389
|
+
typeof parsed.plugins === "object"
|
|
390
|
+
? parsed.plugins
|
|
391
|
+
: parsed;
|
|
392
|
+
if (!plugins || typeof plugins !== "object") return null;
|
|
393
|
+
for (const spec of Object.keys(plugins)) {
|
|
394
|
+
if (spec === SKILL_SLUG) return `${SKILL_SLUG}@${SKILL_SLUG}`;
|
|
395
|
+
if (spec.startsWith(SKILL_SLUG + "@")) return spec;
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
// missing or corrupt = no plugin — fail open on absence, never crash
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* One channel per agent. A plain `<H>/.claude/skills/sheleg-design` beside an
|
|
405
|
+
* installed plugin is two listings of the same skill, and the stale copy wins
|
|
406
|
+
* — the exact shadow the family canon forbids (make-skill
|
|
407
|
+
* references/distribution.md §"The installer must refuse the shadow it
|
|
408
|
+
* documents"). Refuse rather than create it, and refuse LOUDLY: a refusal
|
|
409
|
+
* that exits 0 reads as success to every script above it. Reproduced live
|
|
410
|
+
* 2026-08-29: a bare `npx @ssheleg/telegram-dev` shipped three shadows past a
|
|
411
|
+
* marketplace-dir-only check while the plugin was enabled.
|
|
412
|
+
*/
|
|
413
|
+
function refuseIfPluginOwnsChannel(targetDir, force) {
|
|
414
|
+
const home = claudeHomeOf(targetDir);
|
|
415
|
+
if (!home || force) return;
|
|
416
|
+
const spec = installedPluginSpec(home);
|
|
417
|
+
const marketplaces = path.join(home, ".claude", "plugins", "marketplaces");
|
|
418
|
+
const mktDir = [SKILL_SLUG, "sheleg-design-skill"]
|
|
419
|
+
.map((n) => path.join(marketplaces, n))
|
|
420
|
+
.find((p) => fs.existsSync(p));
|
|
421
|
+
if (!spec && !mktDir) return;
|
|
422
|
+
|
|
423
|
+
const remedySpec = spec || "sheleg-design@sheleg-design-skill";
|
|
424
|
+
const remedyMarketplace = remedySpec.split("@")[1];
|
|
425
|
+
const found = spec
|
|
426
|
+
? `installed as the Claude Code plugin ${spec}\n` +
|
|
427
|
+
` (declared in ${path.join(home, ".claude", "plugins", "installed_plugins.json")})`
|
|
428
|
+
: `registered as a Claude Code marketplace\n (${mktDir})`;
|
|
429
|
+
console.error(
|
|
430
|
+
c("yellow", `refused: ${SKILL_SLUG} is already ${found}.`) +
|
|
431
|
+
`\n A plain copy in ${path.join(home, ".claude", "skills", SKILL_SLUG)}\n` +
|
|
432
|
+
` would shadow the plugin and serve this frozen version forever.\n` +
|
|
433
|
+
` Update the plugin channel instead:\n` +
|
|
434
|
+
` ${c("bold", `claude plugin marketplace update ${remedyMarketplace}`)}\n` +
|
|
435
|
+
` ${c("bold", `claude plugin update ${remedySpec}`)}\n` +
|
|
436
|
+
` Family launcher (updates every member, prunes shadow copies):\n` +
|
|
437
|
+
` ${c("bold", "npx --yes sshlg-skills@latest update")}\n` +
|
|
438
|
+
` Pass --force to write the plain copy anyway — a deliberate choice\n` +
|
|
439
|
+
` to run two channels, where the stale one wins.`,
|
|
440
|
+
);
|
|
441
|
+
// Offered on the refusal path too: the skill IS present on this machine —
|
|
442
|
+
// as the plugin — so the routing block is exactly as wanted as on the
|
|
443
|
+
// install path.
|
|
444
|
+
offerRouters();
|
|
445
|
+
process.exit(EXIT_PLUGIN_PRESENT);
|
|
446
|
+
}
|
|
447
|
+
|
|
331
448
|
function main() {
|
|
332
449
|
const opts = parseArgs(process.argv.slice(2));
|
|
333
450
|
|
|
@@ -354,6 +471,10 @@ function main() {
|
|
|
354
471
|
|
|
355
472
|
const targetDir = resolveTargetDir(opts, cwd);
|
|
356
473
|
|
|
474
|
+
// Before any write to a home's ~/.claude/skills/sheleg-design: if that
|
|
475
|
+
// home's plugin channel already owns this skill, refuse (exit 3).
|
|
476
|
+
refuseIfPluginOwnsChannel(targetDir, opts.force);
|
|
477
|
+
|
|
357
478
|
// Verify the bundle is intact before touching the filesystem.
|
|
358
479
|
for (const f of CORE_FILES) {
|
|
359
480
|
if (!fs.existsSync(path.join(SKILL_DIR, f))) {
|
|
@@ -398,6 +519,16 @@ function main() {
|
|
|
398
519
|
);
|
|
399
520
|
|
|
400
521
|
offerRouters();
|
|
522
|
+
|
|
523
|
+
// The last line says how the next version arrives — "installed" is not a
|
|
524
|
+
// complete sentence. Auto-update is off on purpose: this member composes
|
|
525
|
+
// with its family, and per-marketplace autoUpdate moves each member on its
|
|
526
|
+
// own clock, into combinations nobody tested together.
|
|
527
|
+
console.log(
|
|
528
|
+
`Updates: rerun ${c("bold", "npx sheleg-design-skill@latest --force")}, or refresh the\n` +
|
|
529
|
+
`whole family with ${c("bold", "npx --yes sshlg-skills@latest update")} (every channel,\n` +
|
|
530
|
+
`and it prunes plain copies that would shadow a plugin).\n`,
|
|
531
|
+
);
|
|
401
532
|
}
|
|
402
533
|
|
|
403
534
|
/**
|
|
@@ -138,6 +138,14 @@ detached nav, one teal whose text-bearing steps climb the reference's own hover
|
|
|
138
138
|
ladder, a pink that answers but may not speak, TT Fors at a 600 ceiling with tabular
|
|
139
139
|
numerals, and the product shown only as flat portraits, for visibility, monitoring
|
|
140
140
|
and benchmark tools sold to marketing teams;
|
|
141
|
+
chorus — warm off-white paper under a construction grid that never switches off, two
|
|
142
|
+
1px verticals crossed by horizontals with a small plus at every intersection and
|
|
143
|
+
continued across the dark band; the set piece is somebody else's question in a
|
|
144
|
+
cut-corner bubble (24px on three corners, 0 on the top right) set in the display face,
|
|
145
|
+
white cards with no shadow and a hairline edge, near-black slabs cut 30px into the
|
|
146
|
+
sheet, one coral that fills but may never be a word, and a periwinkle that only exists
|
|
147
|
+
in a gradient or on the dark, for AI-search visibility, brand monitoring, social
|
|
148
|
+
listening and community marketing whose proof is other people's conversations;
|
|
141
149
|
otherwise follow the contract below (self-contained on purpose).
|
|
142
150
|
|
|
143
151
|
## Whether to animate at all — before how
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pkg": "@sheleg-design/chorus",
|
|
3
|
+
"globalName": "ShelegChorus",
|
|
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
|
+
# Chorus — conventions for a design agent
|
|
2
|
+
|
|
3
|
+
This kit is the built form of the Chorus style pack. Read these before generating
|
|
4
|
+
anything with it; they are the pack's bans, stated where a design tool will see them.
|
|
5
|
+
|
|
6
|
+
## Colour
|
|
7
|
+
|
|
8
|
+
- **The coral fills and never writes.** `--coral` (#f96f4b) is a fill, a flat block, a
|
|
9
|
+
gradient stop and an icon beside a label that carries the meaning. As a word it is
|
|
10
|
+
2.72:1 on the field; under a white label it is 2.84:1. Both are below the 3:1
|
|
11
|
+
large-text floor, so no size rescues either. A coral word takes `--coral-ink`
|
|
12
|
+
(#cb441f, 4.59:1 on the field).
|
|
13
|
+
- **The primary button's label is INK**, `--on-coral` at 6.20:1. This is the pack's
|
|
14
|
+
correction to the reference and it keeps the brand hue exactly rather than darkening
|
|
15
|
+
it.
|
|
16
|
+
- **The mint lives only on the dark.** `--good-on-dark` (#36ff94) is 13.31:1 in a
|
|
17
|
+
well and does not exist on paper — held at its own hue it reaches AA on paper only
|
|
18
|
+
at near-black. A green figure on a card takes `--good` (#198400).
|
|
19
|
+
- **Status is never by colour alone.** `--good` and `--danger` are 6.2 apart under
|
|
20
|
+
deuteranopia. Every delta ships its arrow and its number, both.
|
|
21
|
+
- **There is no warn.** Three status roles. The reference declares an amber that paints
|
|
22
|
+
nothing; a fourth severity here would be invented.
|
|
23
|
+
- The periwinkle is a gradient stop and a series line on the dark. It is not a second
|
|
24
|
+
button colour and never carries a word on paper.
|
|
25
|
+
|
|
26
|
+
## Surface and edge
|
|
27
|
+
|
|
28
|
+
- A card is `--surface` at `--r-card` with a 1px `--line` — **32px padding and 32px
|
|
29
|
+
gap at every width**, including 390. Do not tighten it on narrow screens.
|
|
30
|
+
- Four objects carry a shadow and nothing else may: the bubble, the floating panel,
|
|
31
|
+
the hero deck, the nav. Everything else takes `--line` for its edge.
|
|
32
|
+
- The construction grid never switches off, and it continues across the dark slab.
|
|
33
|
+
The plus at each intersection is part of it.
|
|
34
|
+
- The dark slab is a **surface**, not a theme. The page never inverts.
|
|
35
|
+
|
|
36
|
+
## Type
|
|
37
|
+
|
|
38
|
+
- Outfit sets two things: the display, and the question inside a bubble. Nothing else.
|
|
39
|
+
- Display tracking **relaxes** as the type shrinks. Do not tighten a small display.
|
|
40
|
+
- Body line-height is 1.7. No italic anywhere; nothing above weight 700.
|
|
41
|
+
|
|
42
|
+
## Geometry and motion
|
|
43
|
+
|
|
44
|
+
- `--r-bubble` is 24px on three corners and 0 on the top right. It is the pack's whole
|
|
45
|
+
geometry, and it belongs to one object.
|
|
46
|
+
- Nothing lifts, scales or parallaxes; there is no scroll clock; gradients do not move.
|
|
47
|
+
- Loading is a static skeleton, never a spinner and never a shimmer.
|
|
48
|
+
- The reduced-motion contract has a JavaScript half: a reveal observer must read
|
|
49
|
+
`matchMedia('(prefers-reduced-motion: reduce)')` and mount at the final transform.
|
|
50
|
+
The media query in the token layer cannot reach a transform a script sets.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @sheleg-design/chorus
|
|
2
|
+
|
|
3
|
+
The React reference kit for the **Chorus** style pack — warm off-white paper under a
|
|
4
|
+
construction grid that never switches off, where the set piece is somebody else's
|
|
5
|
+
question in a cut-corner bubble.
|
|
6
|
+
|
|
7
|
+
The pack itself is the authority:
|
|
8
|
+
[`styles/chorus.md`](../../plugins/sheleg-design/skills/sheleg-design/styles/chorus.md).
|
|
9
|
+
This kit is one built reading of it, not a second source of truth. `src/styles.css`
|
|
10
|
+
opens with the pack's token layer copied byte for byte from
|
|
11
|
+
`styles/tokens/chorus.css`; everything below the `/* ── components ── */` marker
|
|
12
|
+
consumes `var(--…)` and contains no colour literal.
|
|
13
|
+
|
|
14
|
+
## Build
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npm run build # tsc only — no bundler, no runtime dependency but React
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What is here
|
|
22
|
+
|
|
23
|
+
The six-name spine every SHELEG kit shares — `Button`, `Card`, `Chip`, `Stat`,
|
|
24
|
+
`Heading`, `Rule` — with identical props everywhere, so switching packs swaps identity
|
|
25
|
+
rather than API. Then this pack's own: `Bubble` (the signature element), `GridFrame`,
|
|
26
|
+
`Slab`, `Well`, `Delta`, `NavPill`, `Sweep`, `Field`, `Empty`, `Skeleton`, `Capsule`.
|
|
27
|
+
|
|
28
|
+
Each component carries a `.md` beside it with its category and the numbers that
|
|
29
|
+
license its colours.
|
|
30
|
+
|
|
31
|
+
## The three rules that are easiest to break
|
|
32
|
+
|
|
33
|
+
1. `--coral` may not be a word, at any size, and its button's label is ink.
|
|
34
|
+
2. `--r-bubble`'s cut corner belongs to one object and may not be restyled.
|
|
35
|
+
3. The reduced-motion contract has a JavaScript half — see
|
|
36
|
+
`.design-sync/conventions.md`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sheleg-design/chorus",
|
|
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: Signature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
**The pack's signature element, and the one component that may not be restyled.**
|
|
6
|
+
|
|
7
|
+
`--r-bubble` is 24px on three corners and **0 on the top right** — a speech-bubble
|
|
8
|
+
tail drawn by subtraction, measured on all 24 instances of this object in the
|
|
9
|
+
reference. The question is 24px/600 in `--font-display` at 18.88:1 on `--surface`;
|
|
10
|
+
`--shadow-bubble` is its own three-stop shadow and belongs to nothing else.
|
|
11
|
+
|
|
12
|
+
`surface="slab"` is the same object on the dark side of a pair, `--on-slab` at
|
|
13
|
+
16.54:1. `mirrored` swaps the cut to the top left for the answering side and for RTL
|
|
14
|
+
— the reference ships only one direction, so the mirror is the pack's decision and is
|
|
15
|
+
labelled as one.
|
|
16
|
+
|
|
17
|
+
Change its corner, its face or its shadow and the pack is gone. If the page has no
|
|
18
|
+
stranger's words to put in it, the page does not want this pack.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<Bubble question="Best CRM for B2B companies?" source="r/sales · 18 Mar" />
|
|
22
|
+
<Bubble question="Have you tried it?" surface="slab" mirrored />
|
|
23
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface BubbleProps {
|
|
4
|
+
/** The quoted question. Set in the display face — that is the point. */
|
|
5
|
+
question: string;
|
|
6
|
+
/** Where it was asked: a platform name, a subreddit, a date. */
|
|
7
|
+
source?: string;
|
|
8
|
+
/** `paper` is a stranger's card; `slab` is the answering side of a pair. */
|
|
9
|
+
surface?: 'paper' | 'slab';
|
|
10
|
+
/** Mirrors the cut corner for the answering side and for RTL. */
|
|
11
|
+
mirrored?: boolean;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Bubble({
|
|
17
|
+
question,
|
|
18
|
+
source,
|
|
19
|
+
surface = 'paper',
|
|
20
|
+
mirrored = false,
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
}: BubbleProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={[
|
|
27
|
+
'ch-bubble',
|
|
28
|
+
`ch-bubble--${surface}`,
|
|
29
|
+
mirrored ? 'ch-bubble--mirrored' : null,
|
|
30
|
+
className,
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(' ')}
|
|
34
|
+
>
|
|
35
|
+
<p className="ch-bubble__q">{question}</p>
|
|
36
|
+
{source && <p className="ch-bubble__src">{source}</p>}
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Actions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
36px tall at `--r-control`, 8px/16px padding on a 14px/500 label, and a fill that
|
|
6
|
+
only ever changes colour — nothing lifts, scales or travels.
|
|
7
|
+
|
|
8
|
+
`primary` is the reference's own coral with the pack's correction: `--coral` fill and
|
|
9
|
+
an `--on-coral` label at 6.20:1. **A white label here measures 2.84:1 and is banned at
|
|
10
|
+
every size** — that is the whole reason this variant exists in this shape. `secondary`
|
|
11
|
+
is `--ink-strong` with `--on-slab`. `ghost` is `--surface` with a 1px `--line`.
|
|
12
|
+
|
|
13
|
+
Hover steps the fill one value; active repeats hover. Focus paints a 2px
|
|
14
|
+
`--focus-color` ring **and** keeps the fill step. On a slab the ring resolves to
|
|
15
|
+
`--focus-color-on-dark`, because the ink ring is 1.00:1 there.
|
|
16
|
+
|
|
17
|
+
The 36px height is measured and misses the 44px tap floor: the hit area is padded to
|
|
18
|
+
`--tap-min` on touch rather than the button being resized.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<Button>Start free trial</Button>
|
|
22
|
+
<Button variant="secondary">Book a demo</Button>
|
|
23
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ButtonProps {
|
|
4
|
+
/** `primary` is the coral fill with an INK label — never a white one. */
|
|
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={['ch-btn', `ch-btn--${variant}`, `ch-btn--${size}`, className]
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join(' ')}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Signature
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The closing band's scatter: stadium-radius pills at `--r-pill`, some outlined in
|
|
6
|
+
`--line-on-slab`, some filled `--coral`, `--periwinkle` or `--parchment`, jumbled at
|
|
7
|
+
angles between platform marks. Many voices, one page — it is the pack's name made
|
|
8
|
+
visible.
|
|
9
|
+
|
|
10
|
+
The tilt is a static transform, not an animation: nothing in this pack moves on
|
|
11
|
+
scroll. Keep it under about 12 degrees, which is the range the reference's own scatter
|
|
12
|
+
occupies.
|
|
13
|
+
|
|
14
|
+
Decorative by construction — a capsule carrying a word would be a coral or periwinkle
|
|
15
|
+
label, and neither may be text.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Capsule tone="coral" tilt={-6} />
|
|
19
|
+
<Capsule tone="outline" tilt={3} />
|
|
20
|
+
```
|