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.
Files changed (60) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +5 -4
  3. package/bin/cli.js +132 -1
  4. package/cursor/rules/sheleg-design.mdc +8 -0
  5. package/kits/chorus/.design-sync/config.json +14 -0
  6. package/kits/chorus/.design-sync/conventions.md +50 -0
  7. package/kits/chorus/README.md +36 -0
  8. package/kits/chorus/package.json +29 -0
  9. package/kits/chorus/src/Bubble.md +23 -0
  10. package/kits/chorus/src/Bubble.tsx +40 -0
  11. package/kits/chorus/src/Button.md +23 -0
  12. package/kits/chorus/src/Button.tsx +33 -0
  13. package/kits/chorus/src/Capsule.md +20 -0
  14. package/kits/chorus/src/Capsule.tsx +21 -0
  15. package/kits/chorus/src/Card.md +20 -0
  16. package/kits/chorus/src/Card.tsx +23 -0
  17. package/kits/chorus/src/Chip.md +15 -0
  18. package/kits/chorus/src/Chip.tsx +25 -0
  19. package/kits/chorus/src/Delta.md +19 -0
  20. package/kits/chorus/src/Delta.tsx +24 -0
  21. package/kits/chorus/src/Empty.md +18 -0
  22. package/kits/chorus/src/Empty.tsx +19 -0
  23. package/kits/chorus/src/Field.md +16 -0
  24. package/kits/chorus/src/Field.tsx +40 -0
  25. package/kits/chorus/src/GridFrame.md +19 -0
  26. package/kits/chorus/src/GridFrame.tsx +34 -0
  27. package/kits/chorus/src/Heading.md +16 -0
  28. package/kits/chorus/src/Heading.tsx +17 -0
  29. package/kits/chorus/src/NavPill.md +19 -0
  30. package/kits/chorus/src/NavPill.tsx +19 -0
  31. package/kits/chorus/src/Rule.md +14 -0
  32. package/kits/chorus/src/Rule.tsx +12 -0
  33. package/kits/chorus/src/Skeleton.md +16 -0
  34. package/kits/chorus/src/Skeleton.tsx +27 -0
  35. package/kits/chorus/src/Slab.md +18 -0
  36. package/kits/chorus/src/Slab.tsx +17 -0
  37. package/kits/chorus/src/Stat.md +15 -0
  38. package/kits/chorus/src/Stat.tsx +17 -0
  39. package/kits/chorus/src/Sweep.md +17 -0
  40. package/kits/chorus/src/Sweep.tsx +16 -0
  41. package/kits/chorus/src/Well.md +15 -0
  42. package/kits/chorus/src/Well.tsx +16 -0
  43. package/kits/chorus/src/index.ts +37 -0
  44. package/kits/chorus/src/styles.css +878 -0
  45. package/kits/chorus/tsconfig.json +15 -0
  46. package/package.json +3 -3
  47. package/plugins/sheleg-design/.claude-plugin/plugin.json +2 -2
  48. package/plugins/sheleg-design/commands/sheleg-design.md +2 -2
  49. package/plugins/sheleg-design/skills/sheleg-design/DESIGN_SYNC_BRIDGE.md +1 -1
  50. package/plugins/sheleg-design/skills/sheleg-design/FIGMA_BRIDGE.md +1 -1
  51. package/plugins/sheleg-design/skills/sheleg-design/MOBILE_SURFACES.md +1 -1
  52. package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +2 -2
  53. package/plugins/sheleg-design/skills/sheleg-design/STYLE_PACK_INDEX.md +3 -2
  54. package/plugins/sheleg-design/skills/sheleg-design/SURFACE_COMPOSITION.md +6 -5
  55. package/plugins/sheleg-design/skills/sheleg-design/styles/STYLE_PACK_TEMPLATE.md +1 -1
  56. package/plugins/sheleg-design/skills/sheleg-design/styles/chorus.md +474 -0
  57. package/plugins/sheleg-design/skills/sheleg-design/styles/deskmate.md +10 -0
  58. package/plugins/sheleg-design/skills/sheleg-design/styles/surveyor.md +11 -0
  59. package/plugins/sheleg-design/skills/sheleg-design/styles/tenor.md +1 -1
  60. 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-eight locked style
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-eight style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
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-eight kits, so switching packs swaps identity rather than API,
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-eight
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-eight style packs — instrument-console (dark console),
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
+ ```