sheleg-design-skill 1.7.0 → 1.8.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 +53 -0
- package/README.md +2 -1
- package/bin/cli.js +6 -5
- package/cursor/rules/sheleg-design.mdc +4 -1
- package/kits/cyclorama/.design-sync/config.json +12 -0
- package/kits/cyclorama/.design-sync/conventions.md +69 -0
- package/kits/cyclorama/README.md +23 -0
- package/kits/cyclorama/package.json +14 -0
- package/kits/cyclorama/src/AppWindow.md +27 -0
- package/kits/cyclorama/src/AppWindow.tsx +46 -0
- package/kits/cyclorama/src/Button.md +26 -0
- package/kits/cyclorama/src/Button.tsx +43 -0
- package/kits/cyclorama/src/Card.md +19 -0
- package/kits/cyclorama/src/Card.tsx +33 -0
- package/kits/cyclorama/src/Chip.md +19 -0
- package/kits/cyclorama/src/Chip.tsx +34 -0
- package/kits/cyclorama/src/ComparePanel.md +27 -0
- package/kits/cyclorama/src/ComparePanel.tsx +61 -0
- package/kits/cyclorama/src/FieldStop.md +29 -0
- package/kits/cyclorama/src/FieldStop.tsx +33 -0
- package/kits/cyclorama/src/Heading.md +20 -0
- package/kits/cyclorama/src/Heading.tsx +27 -0
- package/kits/cyclorama/src/Rule.md +18 -0
- package/kits/cyclorama/src/Rule.tsx +27 -0
- package/kits/cyclorama/src/Stat.md +19 -0
- package/kits/cyclorama/src/Stat.tsx +25 -0
- package/kits/cyclorama/src/StatusPill.md +31 -0
- package/kits/cyclorama/src/StatusPill.tsx +37 -0
- package/kits/cyclorama/src/index.ts +25 -0
- package/kits/cyclorama/src/styles.css +569 -0
- package/kits/cyclorama/tsconfig.json +15 -0
- package/package.json +2 -2
- package/plugins/sheleg-design/.claude-plugin/plugin.json +2 -2
- package/plugins/sheleg-design/skills/sheleg-design/SKILL.md +1 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/cyclorama.md +408 -0
- package/plugins/sheleg-design/skills/sheleg-design/styles/tokens/cyclorama.css +201 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,59 @@ 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.8.0] - 2026-08-08
|
|
8
|
+
|
|
9
|
+
An eighth style pack, and the first one whose reference already implements this
|
|
10
|
+
skill's own core pattern.
|
|
11
|
+
|
|
12
|
+
`cyclorama` is extracted from [codos.ai](https://www.codos.ai/): a pale field
|
|
13
|
+
that breathes through **six pastel stops on a 32-second loop** under near-black
|
|
14
|
+
ink that never moves with it, a **monospaced typewriter serif** over a
|
|
15
|
+
monospaced sans, one orange used only as a fill, and no shadows anywhere. Its
|
|
16
|
+
reference ships GSAP ScrollTrigger with real pinning and two WebGL canvases
|
|
17
|
+
whose formation holds and then redeploys — principle 3 in production rather than
|
|
18
|
+
in a doc.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`styles/cyclorama.md`** on the thirteen-heading widened contract plus
|
|
23
|
+
`## Motion flavor`, with an addressable origin and every ratio computed by
|
|
24
|
+
importing `test/validate_palette.py` rather than by a second implementation.
|
|
25
|
+
- **`styles/tokens/cyclorama.css`** — the six cycle stops as named tokens, one
|
|
26
|
+
ease, three durations plus the 32s loop, a five-step radius ramp whose nesting
|
|
27
|
+
is arithmetic, and an inline accent-dot cursor.
|
|
28
|
+
- **`kits/cyclorama/`** — the six-component spine plus four signature parts:
|
|
29
|
+
`FieldStop` (the six stops as six static surfaces — the cycle itself does not
|
|
30
|
+
cross the border), `AppWindow` (a hairline frame with **no fill**, so the
|
|
31
|
+
field shows through it), `StatusPill` and `ComparePanel`.
|
|
32
|
+
- **`docs/adr/0001-style-pack-naming.md`** — restored to `main`. It was written
|
|
33
|
+
on a branch that held at its stage-0 gate and never merged, so the decision
|
|
34
|
+
register began at `0002` for four days while the rule it records was already
|
|
35
|
+
binding. It is why this pack is `cyclorama` and not `codos`.
|
|
36
|
+
- **T15** in `test/scenarios.md` — a routing pair with its negative branch, so
|
|
37
|
+
"is `cyclorama` distinguishable from `field-notes`?" can fail in the
|
|
38
|
+
interesting direction.
|
|
39
|
+
|
|
40
|
+
### Fixed — in the pack, not in the reference
|
|
41
|
+
|
|
42
|
+
- The reference paints its section eyebrows in the accent on the field, which
|
|
43
|
+
measures **1.71–1.97:1** across the six stops. This pack does not propagate
|
|
44
|
+
it: the accent is a fill, eyebrows take `--ink-soft` at 8.36:1. The three
|
|
45
|
+
darkened-orange candidates are recorded in `## Gotchas` **with their numbers**,
|
|
46
|
+
because each one trades a WCAG failure for a colour-blindness collision with
|
|
47
|
+
`--danger` or `--warning` — there is no text-safe orange in this palette, and
|
|
48
|
+
the next reader should not have to rediscover that.
|
|
49
|
+
- The pack states that `--accent` and `--signal` sit 6.8 apart under protanopia
|
|
50
|
+
and that **the palette gate cannot see it**, because `--signal` is not one of
|
|
51
|
+
the names it treats as semantic. Written down rather than renamed around: a
|
|
52
|
+
token renamed to satisfy a checker is worse than an unchecked token that says
|
|
53
|
+
so out loud.
|
|
54
|
+
- The reference's own display fallback is `Fraunces`, which is **proportional**,
|
|
55
|
+
while the face it replaces is monospaced — so a reader without the licensed
|
|
56
|
+
font gets a hero that reflows rather than merely restyles. The pack ships
|
|
57
|
+
measured substitutes instead (Courier Prime 0.600, Cutive Mono 0.605 against
|
|
58
|
+
the original's 0.590).
|
|
59
|
+
|
|
7
60
|
## [1.7.0] - 2026-08-05
|
|
8
61
|
|
|
9
62
|
The Claude Design border, and the first code this skill has ever shipped.
|
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ voice is the timeline and slides therefore never animate).
|
|
|
49
49
|
| `atrium` | warm cream daylight field with no dark bands, one terracotta accent, light serif with italic asides, fluted-glass hero over photography | consumer health, longevity & diagnostics, wellness, premium care, high-trust DTC |
|
|
50
50
|
| `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 |
|
|
51
51
|
| `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 |
|
|
52
|
+
| `cyclorama` | a pale field cycling through six pastel stops on a 32s loop under near-black ink that never moves with it, a monospaced typewriter serif over mono, one orange used only as a fill, a particle organ that holds then redeploys, no shadows anywhere | enterprise AI transformation, applied-AI services, technical consultancies — where what is sold is a change of state and there is no screenshot worth showing |
|
|
52
53
|
|
|
53
54
|
Each pack locks palette, type, texture, motion tokens, signature motifs and
|
|
54
55
|
bans — and ships a `tokens/<pack>.css` to copy verbatim, so the agent never
|
|
@@ -121,7 +122,7 @@ skills.
|
|
|
121
122
|
| `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 |
|
|
122
123
|
| `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 |
|
|
123
124
|
| `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 |
|
|
124
|
-
| `styles/*.md` | The
|
|
125
|
+
| `styles/*.md` | The eight style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
|
|
125
126
|
| `styles/tokens/*.css` | The ready-made token layer per pack, copied verbatim instead of transcribed (workbench ships a light `:root` plus a `data-theme="dark"` twin) |
|
|
126
127
|
| `styles/STYLE_PACK_TEMPLATE.md` | The pack contract as a skeleton, so a new style is authored against the same headings rather than improvised |
|
|
127
128
|
|
package/bin/cli.js
CHANGED
|
@@ -230,13 +230,14 @@ ${c("bold", "What it installs")}
|
|
|
230
230
|
SHELEG_DESIGN.md the full reference (architecture, recipes, why it works)
|
|
231
231
|
FIGMA_BRIDGE.md the design↔code contract (tokens ⇄ Figma variables)
|
|
232
232
|
AI_PRODUCT_PATTERNS.md chat / agent / streaming surfaces (honest state)
|
|
233
|
-
styles/
|
|
233
|
+
styles/ eight style packs — instrument-console (dark console),
|
|
234
234
|
editorial-luxury (warm editorial), workbench (light/dark
|
|
235
235
|
product UI), briefing-room (dark 16:9 presentation deck),
|
|
236
236
|
atrium (warm cream consumer health), orchard (friendly
|
|
237
|
-
consumer biotech), field-notes (warm paper dev tools)
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
consumer biotech), field-notes (warm paper dev tools),
|
|
238
|
+
cyclorama (a pastel field on a 32s cycle, for enterprise
|
|
239
|
+
AI transformation) — plus a ready-made token CSS per pack
|
|
240
|
+
and STYLE_PACK_TEMPLATE.md for authoring more
|
|
240
241
|
`);
|
|
241
242
|
}
|
|
242
243
|
|
|
@@ -315,7 +316,7 @@ function main() {
|
|
|
315
316
|
`\n${c("green", "✓")} ${c("bold", "SHELEG Design")} installed to ${c("blue", rel + "/")}\n` +
|
|
316
317
|
` ${c("dim", "SKILL.md")} the agent skill\n` +
|
|
317
318
|
` ${c("dim", "SHELEG_DESIGN.md")} the full reference\n` +
|
|
318
|
-
` ${c("dim", "styles/")} style packs + token CSS (instrument-console / editorial-luxury / workbench / briefing-room / atrium / orchard / field-notes)\n\n` +
|
|
319
|
+
` ${c("dim", "styles/")} style packs + token CSS (instrument-console / editorial-luxury / workbench / briefing-room / atrium / orchard / field-notes / cyclorama)\n\n` +
|
|
319
320
|
`Your Cursor / Claude agent can now discover the skill and build\n` +
|
|
320
321
|
`cinematic, scroll-driven pages — or style product UI (dashboards,\n` +
|
|
321
322
|
`admin, internal tools) from the workbench pack — on its principles.\n\n` +
|
|
@@ -17,7 +17,10 @@ never animate; atrium — warm cream consumer-health field, one terracotta
|
|
|
17
17
|
accent, fluted-glass hero; orchard — friendly consumer biotech, oat slabs,
|
|
18
18
|
sage brand and one candy-orange action; field-notes — open-source developer
|
|
19
19
|
tools on warm green-cast paper ruled by hairlines, one rust accent, a hero
|
|
20
|
-
that dissolves into the page, and colour that encodes how a claim is known
|
|
20
|
+
that dissolves into the page, and colour that encodes how a claim is known;
|
|
21
|
+
cyclorama — enterprise AI transformation on a pale field that cycles through
|
|
22
|
+
six pastel stops on a 32s loop under fixed near-black ink, a monospaced
|
|
23
|
+
typewriter serif, one orange used only as a fill, and no shadows at all);
|
|
21
24
|
otherwise follow the contract below (self-contained on purpose).
|
|
22
25
|
|
|
23
26
|
## Five principles, in order
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pkg": "@sheleg-design/cyclorama",
|
|
3
|
+
"globalName": "ShelegCyclorama",
|
|
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": ["guidelines/*.md"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Cyclorama — the contract this design system ships under
|
|
2
|
+
|
|
3
|
+
**Register.** Choose Cyclorama for **a company selling transformation to an
|
|
4
|
+
executive buyer**: enterprise AI, AI-transformation and applied-AI services,
|
|
5
|
+
technical consultancies whose argument is *we install this and your business
|
|
6
|
+
starts running differently*. It suits a product with no screenshot worth showing,
|
|
7
|
+
where the thing being sold is a change of state. The fork people get wrong is
|
|
8
|
+
against `field-notes`: both are warm, light and monospace-voiced, but that pack
|
|
9
|
+
is a **ruled document** selling *auditability* and nothing in it moves, while this
|
|
10
|
+
one is a **stage** selling *transformation* and the field itself is the motion.
|
|
11
|
+
Route by what the product's argument rests on — *how do you know?* takes
|
|
12
|
+
`field-notes`, *watch this change* takes this pack. Against `workbench`: a field
|
|
13
|
+
that recolours under you is a defect on a screen held for an hour, so dense
|
|
14
|
+
product UI goes there, not here. Build every screen against `var(--…)` and never
|
|
15
|
+
a literal.
|
|
16
|
+
|
|
17
|
+
**The field is not a colour.** It is a 32-second, six-stop, infinite loop, and
|
|
18
|
+
the ink never moves with it. Every contrast figure in this system is stated
|
|
19
|
+
against the **worst** stop, `--field-2` at 12.79:1 — not against a representative
|
|
20
|
+
one. When you prove a screen, prove it there. `FieldStop` exists to make that
|
|
21
|
+
possible; the animation itself is not part of this design system, because a kit
|
|
22
|
+
is the static half of a pack.
|
|
23
|
+
|
|
24
|
+
**The accent rule, and it is the one that gets broken.** `--accent` `#FF8C00` is
|
|
25
|
+
a **fill, a dot and a chart series — never text on the field.** As text it
|
|
26
|
+
measures 1.71–1.97:1 across the six stops. The reference itself paints its
|
|
27
|
+
section eyebrows that way and it fails WCAG at every stop; this system does not
|
|
28
|
+
propagate it. Eyebrows take `--ink-soft` at 8.36:1. And there is no darkened
|
|
29
|
+
orange to reach for instead: the warm-dark region is already occupied by
|
|
30
|
+
`--warning` and `--danger`, so every text-capable orange collides with one of
|
|
31
|
+
them — `#903A00` sits 4.6 from `--danger` against a hard floor of 10. There is no
|
|
32
|
+
text-safe orange in this palette. Ink on the accent is fine at 7.46:1, which is
|
|
33
|
+
what `--on-accent` is for.
|
|
34
|
+
|
|
35
|
+
**Status is never by colour alone.** `--good` and `--danger` separate by 7.2
|
|
36
|
+
under protanopia and 5.9 under deuteranopia; `--signal` and `--accent` by 6.8 and
|
|
37
|
+
6.7. The floor is 8. Every status therefore renders as a mark **plus its word** —
|
|
38
|
+
which is why `StatusPill` takes `label` as a required prop rather than an
|
|
39
|
+
optional one. An API that let you omit it would be an API that lets you ship the
|
|
40
|
+
bug.
|
|
41
|
+
|
|
42
|
+
**Bans** (verbatim from the pack):
|
|
43
|
+
|
|
44
|
+
- `--accent` as text on the field. Also banned: a darkened orange invented to get
|
|
45
|
+
around it — there isn't one.
|
|
46
|
+
- `--signal` as text, and any status rendered as a mark without its word.
|
|
47
|
+
- A shadow. Anywhere. Elevation is a hairline; there is no `--shadow-*` token
|
|
48
|
+
because there is nothing for it to describe.
|
|
49
|
+
- A fill on the app window; a second opaque surface beside `--surface` and
|
|
50
|
+
`--panel`; a gradient of any kind.
|
|
51
|
+
- `transition: all`, which the reference itself declares and this pack refuses.
|
|
52
|
+
- A proportional display face — Zilla Slab, Bitter, Fraunces or any serif that is
|
|
53
|
+
not monospaced. The hero is laid out per character; a proportional substitute
|
|
54
|
+
does not restyle it, it breaks it. Use Courier Prime.
|
|
55
|
+
- A sticky nav, a scrolled nav shape, or a backdrop blur.
|
|
56
|
+
- `100vh`; a scroll listener where the scroll store belongs.
|
|
57
|
+
- Scrubbing the field cycle to scroll position, or adding a second continuous
|
|
58
|
+
loop beside it.
|
|
59
|
+
- A third font family. Urbanist is vestigial, not an invitation.
|
|
60
|
+
- Pure black or pure white as ink; white as a label on an ink fill, where the
|
|
61
|
+
field colour belongs.
|
|
62
|
+
|
|
63
|
+
**Radius arithmetic.** An inner radius is the outer radius minus the padding
|
|
64
|
+
between them, never the same value twice. The reference gets this right and it is
|
|
65
|
+
worth copying exactly: a 16px window with 12px of padding holds 4px chips.
|
|
66
|
+
|
|
67
|
+
Motion is not part of this design system and must not be invented: a kit is the
|
|
68
|
+
static half of a pack, and anything that moves stays behind in the pack. The
|
|
69
|
+
cycle is six surfaces here, not an animation.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @sheleg-design/cyclorama
|
|
2
|
+
|
|
3
|
+
The React reference kit for the SHELEG **Cyclorama** style pack — a pale field
|
|
4
|
+
that breathes through six pastel stops on a 32-second loop under near-black ink
|
|
5
|
+
that never moves with it, a monospaced typewriter serif over mono, one orange
|
|
6
|
+
used only as a fill, and no shadows anywhere.
|
|
7
|
+
|
|
8
|
+
It is generated from the pack, not authored beside it: `src/styles.css` opens
|
|
9
|
+
with `styles/tokens/cyclorama.css` byte for byte, and the rules the design agent
|
|
10
|
+
must obey are in
|
|
11
|
+
[`.design-sync/conventions.md`](./.design-sync/conventions.md).
|
|
12
|
+
|
|
13
|
+
**The cycle is not in this kit, and that is deliberate.** A kit is the static
|
|
14
|
+
half of a pack; motion stays behind in the pack. What crosses is `FieldStop` —
|
|
15
|
+
the six stops as six surfaces — which is the more useful thing to design against
|
|
16
|
+
anyway: a screen proved on stop 2 and stop 5 is proved on every frame between.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install && npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run `/design-sync` in Claude Code from this directory to push it to
|
|
23
|
+
claude.ai/design.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sheleg-design/cyclorama",
|
|
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": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
|
|
10
|
+
"files": ["dist", "src"],
|
|
11
|
+
"scripts": { "build": "tsc -p tsconfig.json" },
|
|
12
|
+
"peerDependencies": { "react": ">=18" },
|
|
13
|
+
"devDependencies": { "typescript": "^5.6.0", "@types/react": "^18.3.0" }
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A `1px var(--line)` frame at `--radius-lg` with 12px of padding and **no fill at
|
|
6
|
+
all**, so the field cycle shows straight through it. Traffic-light dots at the
|
|
7
|
+
left, a mono title and sub-line in the centre, a slot for a `StatusPill` at the
|
|
8
|
+
right, and a row of chips along the foot.
|
|
9
|
+
|
|
10
|
+
**Giving it a background is the most common way to break this pack.** The window
|
|
11
|
+
is a frame, not a surface. Fill it and the page's signature stops at its border:
|
|
12
|
+
the screenshot inside becomes a foreign object pasted onto a pastel page,
|
|
13
|
+
instead of a product glimpsed through it.
|
|
14
|
+
|
|
15
|
+
Its 12px of padding is also what sets the chips inside to `--radius-sm`, since
|
|
16
|
+
`16 − 12 = 4`. Change the padding and the inner radius has to move with it.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<AppWindow
|
|
20
|
+
title="diagnostic interviews"
|
|
21
|
+
meta="Session ID4611.4 · VP Sales · 01:55"
|
|
22
|
+
status={<StatusPill status="live" label="Listening" />}
|
|
23
|
+
footer={<><Chip>Mic on</Chip><Chip>Phase 2 of 5</Chip></>}
|
|
24
|
+
>
|
|
25
|
+
Most start inbound — a founder demo request.
|
|
26
|
+
</AppWindow>
|
|
27
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface AppWindowProps {
|
|
4
|
+
title: string;
|
|
5
|
+
/** The mono sub-line under the title: a session id, a phase, a duration. */
|
|
6
|
+
meta?: string;
|
|
7
|
+
/** Rendered at the window's top right — a StatusPill belongs here. */
|
|
8
|
+
status?: ReactNode;
|
|
9
|
+
/** The row of chips along the window's foot. */
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How this pack draws product UI without breaking the page's one continuous
|
|
17
|
+
* surface: a `1px var(--line)` frame at `--radius-lg` with **no fill at all**,
|
|
18
|
+
* so the field cycle shows straight through it.
|
|
19
|
+
*
|
|
20
|
+
* Giving it a background is the single most common way to break this pack. The
|
|
21
|
+
* window is a frame, not a surface — the moment it fills, the page's signature
|
|
22
|
+
* stops at its border and the screenshot inside becomes a foreign object
|
|
23
|
+
* pasted onto a pastel page.
|
|
24
|
+
*
|
|
25
|
+
* Its 12px of padding is what makes the chips inside `--radius-sm`: 16 − 12 = 4.
|
|
26
|
+
*/
|
|
27
|
+
export function AppWindow({ title, meta, status, footer, children, className }: AppWindowProps) {
|
|
28
|
+
return (
|
|
29
|
+
<section className={['cy-window', className].filter(Boolean).join(' ')}>
|
|
30
|
+
<header className="cy-window__bar">
|
|
31
|
+
<span className="cy-window__lights" aria-hidden="true">
|
|
32
|
+
<i />
|
|
33
|
+
<i />
|
|
34
|
+
<i />
|
|
35
|
+
</span>
|
|
36
|
+
<span className="cy-window__titles">
|
|
37
|
+
<span className="cy-window__title">{title}</span>
|
|
38
|
+
{meta !== undefined && <span className="cy-window__meta">{meta}</span>}
|
|
39
|
+
</span>
|
|
40
|
+
{status !== undefined && <span className="cy-window__status">{status}</span>}
|
|
41
|
+
</header>
|
|
42
|
+
<div className="cy-window__body">{children}</div>
|
|
43
|
+
{footer !== undefined && <footer className="cy-window__foot">{footer}</footer>}
|
|
44
|
+
</section>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Actions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`primary` is an `--ink` fill and its label is `--on-ink` — **the field colour,
|
|
6
|
+
not white**, measured at 13.73:1. That is the pack's one counter-intuitive
|
|
7
|
+
button rule: the reference tints its button labels with the page, and a white
|
|
8
|
+
label reads instantly as a component borrowed from another kit.
|
|
9
|
+
|
|
10
|
+
`secondary` is the hairline outline (`1px var(--line)`), and `ghost` drops the
|
|
11
|
+
border for toolbars and table rows. The accent fill is a fourth state carried by
|
|
12
|
+
`Chip tone="accent"` and by the CTA class directly — it is deliberately not a
|
|
13
|
+
`variant` here, because a `variant` list invites picking one per button and this
|
|
14
|
+
pack allows at most one accent fill per view.
|
|
15
|
+
|
|
16
|
+
**Press is instant.** Hover scales to `1.02` over `--dur-base`; active drops to
|
|
17
|
+
`0.98` with `transition-duration: 0s`, so the control answers the finger instead
|
|
18
|
+
of easing after it. Filled and accent buttons carry the 22px accent-dot cursor.
|
|
19
|
+
Focus-visible is a 2px `--accent` outline at 2px offset — the one place the
|
|
20
|
+
accent is allowed beside text, because a ring is not text.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<Button onClick={bookDemo}>Book a demo →</Button>
|
|
24
|
+
<Button variant="secondary" onClick={emailFounders}>E-mail founders →</Button>
|
|
25
|
+
<Button variant="ghost" size="sm" onClick={dismiss}>Not now</Button>
|
|
26
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ButtonProps {
|
|
4
|
+
/** `primary` is the ink fill; `secondary` the hairline outline. At most one
|
|
5
|
+
* accent fill per view — see the `tone` note below. */
|
|
6
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
7
|
+
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `primary` is an `--ink` fill whose label is `--on-ink` — the *field* colour,
|
|
16
|
+
* not white. That is the pack's one counter-intuitive button rule and it is
|
|
17
|
+
* measured: the reference tints its button labels with the page.
|
|
18
|
+
*
|
|
19
|
+
* Press is instant on purpose. Hover scales to 1.02 over `--dur-base`; the
|
|
20
|
+
* active state drops to 0.98 with **zero** duration, so the button answers the
|
|
21
|
+
* finger rather than easing after it.
|
|
22
|
+
*/
|
|
23
|
+
export function Button({
|
|
24
|
+
variant = 'primary',
|
|
25
|
+
size = 'md',
|
|
26
|
+
disabled = false,
|
|
27
|
+
onClick,
|
|
28
|
+
children,
|
|
29
|
+
className,
|
|
30
|
+
}: ButtonProps) {
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className={['cy-btn', `cy-btn--${variant}`, `cy-btn--${size}`, className]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(' ')}
|
|
37
|
+
disabled={disabled}
|
|
38
|
+
onClick={onClick}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Surfaces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`--surface` at `--radius-lg` with 32px of padding, no border and **no shadow** —
|
|
6
|
+
there are no shadows anywhere in this pack, so there is nothing for a card to
|
|
7
|
+
lift off. Hover translates 2px and nothing else.
|
|
8
|
+
|
|
9
|
+
It is the only opaque surface besides `--panel`, and that scarcity is the point.
|
|
10
|
+
A card switches the pack's signature off locally: an opaque rectangle over a
|
|
11
|
+
cycling field is a hole in the thing the page is remembered for. Reach for one
|
|
12
|
+
when content genuinely must stop competing — a three-up of dense propositions,
|
|
13
|
+
a form — and let the field show everywhere else.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Card title="Midsize companies or Enterprise business-units" meta="300–3000 people">
|
|
17
|
+
Big enough to feel friction. Small enough to become AI-native fast.
|
|
18
|
+
</Card>
|
|
19
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface CardProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Right-aligned metadata on the title row: a count, an id, a timestamp. */
|
|
6
|
+
meta?: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The one opaque surface in this pack — `--surface`, `--radius-lg`, 32px of
|
|
13
|
+
* padding, no border and no shadow, because there are no shadows anywhere in
|
|
14
|
+
* this system.
|
|
15
|
+
*
|
|
16
|
+
* Use it where content must stop competing with the cycling field. Everywhere
|
|
17
|
+
* else the field simply shows: a card placed for decoration puts an opaque
|
|
18
|
+
* rectangle over the pack's signature and switches it off locally.
|
|
19
|
+
*/
|
|
20
|
+
export function Card({ title, meta, children, className }: CardProps) {
|
|
21
|
+
const head = title !== undefined || meta !== undefined;
|
|
22
|
+
return (
|
|
23
|
+
<section className={['cy-card', className].filter(Boolean).join(' ')}>
|
|
24
|
+
{head && (
|
|
25
|
+
<div className="cy-card__head">
|
|
26
|
+
{title !== undefined && <h3 className="cy-card__title">{title}</h3>}
|
|
27
|
+
{meta !== undefined && <span className="cy-card__meta">{meta}</span>}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
<div className="cy-card__body">{children}</div>
|
|
31
|
+
</section>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Data
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Ink at 6% on `--radius-sm` with `4px 8px` of padding, DM Mono at `--t-sm`.
|
|
6
|
+
|
|
7
|
+
The 4px radius is arithmetic, not taste: a chip sits inside a 16px window with
|
|
8
|
+
12px of padding, and `16 − 12 = 4`. Concentric curves are what separate a
|
|
9
|
+
machined object from two rectangles that happen to touch, and the reference gets
|
|
10
|
+
this right throughout — copy the arithmetic, not the number.
|
|
11
|
+
|
|
12
|
+
`tone="accent"` fills with `--accent` and labels it `--on-accent` at 7.46:1.
|
|
13
|
+
Accent-coloured *text* on the field is a different thing and is banned: it
|
|
14
|
+
measures 1.71:1 on the pack's worst stop.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<Chip>Phase 2 of 5</Chip>
|
|
18
|
+
<Chip tone="accent" selected>Go-to-market</Chip>
|
|
19
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* Ink at 6% on `--radius-sm` with `4px 8px` of padding — the small radius is
|
|
12
|
+
* not a taste call, it is arithmetic: a chip sits inside a 16px container with
|
|
13
|
+
* 12px of padding, and 16 − 12 = 4.
|
|
14
|
+
*
|
|
15
|
+
* `tone="accent"` fills with `--accent` and sets `--on-accent` as the label at
|
|
16
|
+
* 7.46:1. It is never accent-coloured *text* on the field; that measures 1.7:1
|
|
17
|
+
* and the pack bans it.
|
|
18
|
+
*/
|
|
19
|
+
export function Chip({ children, selected = false, tone = 'neutral', className }: ChipProps) {
|
|
20
|
+
return (
|
|
21
|
+
<span
|
|
22
|
+
className={[
|
|
23
|
+
'cy-chip',
|
|
24
|
+
`cy-chip--${tone}`,
|
|
25
|
+
selected ? 'cy-chip--selected' : undefined,
|
|
26
|
+
className,
|
|
27
|
+
]
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.join(' ')}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Data
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The mist panel — `--panel` at `--radius-lg` with hairline rows. Ink measures
|
|
6
|
+
9.76:1 on it, so it is safe for body copy; it and `--surface` are the only two
|
|
7
|
+
surfaces in the pack that are.
|
|
8
|
+
|
|
9
|
+
It is the one place below the hero where a real fill appears, and it earns that
|
|
10
|
+
because a comparison is the one thing that genuinely reads worse over a moving
|
|
11
|
+
field: the eye has to hold two columns still to compare them.
|
|
12
|
+
|
|
13
|
+
Every marked cell carries its **phrase**, not just its mark. A column of bare
|
|
14
|
+
accent dots is a legend a reader with protanopia cannot use — the accent and the
|
|
15
|
+
signal green sit 6.8 apart there, and see `StatusPill` for the rest of the
|
|
16
|
+
numbers. It is also simply worse writing: "Battle-tested, scalable" argues,
|
|
17
|
+
a dot does not.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<ComparePanel
|
|
21
|
+
headings={['Build it yourself', 'With Codos']}
|
|
22
|
+
rows={[
|
|
23
|
+
{ dimension: 'System architecture', without: 'Ad-hoc, experimental', with: 'Battle-tested, scalable' },
|
|
24
|
+
{ dimension: 'ROI commitment', without: 'None', with: '10x+' },
|
|
25
|
+
]}
|
|
26
|
+
/>
|
|
27
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface CompareRow {
|
|
4
|
+
/** The dimension being compared — the row's label. */
|
|
5
|
+
dimension: string;
|
|
6
|
+
/** The unmarked side: what happens without the product. */
|
|
7
|
+
without: string;
|
|
8
|
+
/** The marked side. Always rendered with its mark AND its words. */
|
|
9
|
+
with: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ComparePanelProps {
|
|
13
|
+
rows: CompareRow[];
|
|
14
|
+
/** Column headings. Defaults to no header row at all. */
|
|
15
|
+
headings?: [string, string];
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The mist panel — the one place below the hero where a real fill appears.
|
|
22
|
+
* `--panel` at `--radius-lg`, hairline rows, and one mark per row on the
|
|
23
|
+
* marked side.
|
|
24
|
+
*
|
|
25
|
+
* Ink measures 9.76:1 on `--panel`, so the panel is safe for body copy; it is
|
|
26
|
+
* the only surface in the pack besides `--surface` that is.
|
|
27
|
+
*
|
|
28
|
+
* The marked side always carries its phrase. A column of bare accent dots is a
|
|
29
|
+
* legend a reader with protanopia cannot use — see `StatusPill` for the
|
|
30
|
+
* measurements — and it is also just worse writing.
|
|
31
|
+
*/
|
|
32
|
+
export function ComparePanel({ rows, headings, children, className }: ComparePanelProps) {
|
|
33
|
+
return (
|
|
34
|
+
<section className={['cy-compare', className].filter(Boolean).join(' ')}>
|
|
35
|
+
<table className="cy-compare__table">
|
|
36
|
+
{headings !== undefined && (
|
|
37
|
+
<thead>
|
|
38
|
+
<tr>
|
|
39
|
+
<th />
|
|
40
|
+
<th>{headings[0]}</th>
|
|
41
|
+
<th>{headings[1]}</th>
|
|
42
|
+
</tr>
|
|
43
|
+
</thead>
|
|
44
|
+
)}
|
|
45
|
+
<tbody>
|
|
46
|
+
{rows.map((row) => (
|
|
47
|
+
<tr key={row.dimension}>
|
|
48
|
+
<th scope="row">{row.dimension}</th>
|
|
49
|
+
<td className="cy-compare__without">{row.without}</td>
|
|
50
|
+
<td className="cy-compare__with">
|
|
51
|
+
<i className="cy-compare__mark" aria-hidden="true" />
|
|
52
|
+
{row.with}
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
))}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
{children}
|
|
59
|
+
</section>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Foundations
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The six stops of `ctaCycle`, each as a static surface.
|
|
6
|
+
|
|
7
|
+
**The cycle itself does not cross into a design system.** A kit is the static
|
|
8
|
+
half of a pack and motion stays behind in the pack — so what arrives here is not
|
|
9
|
+
an animation but the six extremes, which is the more useful thing to design
|
|
10
|
+
against anyway: a screen proved against stop 2 and stop 5 is proved against
|
|
11
|
+
every frame between them.
|
|
12
|
+
|
|
13
|
+
| Stop | Value | Note |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 1 | `--field-1` | `0%`/`100%` — the rest stop; what a screenshot shows |
|
|
16
|
+
| 2 | `--field-2` | the **darkest**: the pack's contrast floor, ink at 12.79:1 |
|
|
17
|
+
| 3 | `--field-3` | blush |
|
|
18
|
+
| 4 | `--field-4` | lilac |
|
|
19
|
+
| 5 | `--field-5` | the lightest, ink at 14.67:1 |
|
|
20
|
+
| 6 | `--field-6` | warm cream |
|
|
21
|
+
|
|
22
|
+
Prove contrast against stop 2. Every ratio the pack claims is stated there,
|
|
23
|
+
because a claim made against the friendliest stop is not a claim about the page.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<FieldStop stop={2}>
|
|
27
|
+
<Heading>Most AI programs never reach the P&L.</Heading>
|
|
28
|
+
</FieldStop>
|
|
29
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The six stops of `ctaCycle`, in the order it plays them. Stop 2 is the
|
|
3
|
+
* darkest and therefore the pack's contrast floor; stop 5 is the lightest.
|
|
4
|
+
*/
|
|
5
|
+
export type Stop = 1 | 2 | 3 | 4 | 5 | 6;
|
|
6
|
+
|
|
7
|
+
export interface FieldStopProps {
|
|
8
|
+
stop?: Stop;
|
|
9
|
+
children?: import('react').ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One named stop of the field cycle, rendered **static**.
|
|
15
|
+
*
|
|
16
|
+
* The cycle itself does not cross into a design system: a kit is the static
|
|
17
|
+
* half of a pack, and motion stays behind in the pack. What crosses is the six
|
|
18
|
+
* stops as six surfaces, so a design agent can lay a screen out against the
|
|
19
|
+
* real extremes instead of against one representative pink.
|
|
20
|
+
*
|
|
21
|
+
* Build against stop 2 when you need to prove contrast — every ratio the pack
|
|
22
|
+
* claims is stated there, because it is the worst of the six.
|
|
23
|
+
*/
|
|
24
|
+
export function FieldStop({ stop = 1, children, className }: FieldStopProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={['cy-field', `cy-field--${stop}`, className].filter(Boolean).join(' ')}
|
|
28
|
+
data-stop={stop}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|