The workshop's design system: react-components (jsr) + a self-built docs site.
- TypeScript 46.1%
- SCSS 29.9%
- Svelte 23.3%
- Shell 0.3%
- JavaScript 0.3%
- Other 0.1%
token: the substrate was always able to move
Every atom is registered with `@property` and a real syntax, which is what the
theme contract needed to refuse a missing one — and which also makes every atom
interpolable. Nothing had ever spent that. A theme change is a set of custom
property changes on one element, so a transition on the shell slides the whole
cascade instead of cutting it.
`tokens.morph` derives its property list from the same three lists `dress()`
reads, so an atom added tomorrow moves with the rest and no second list has to
be kept in step. It takes its duration as an argument rather than reading
`--beat`: a beat is a language's own tempo, four languages state `0s`, and a
transition between two languages belongs to neither of them. `Shell slide`
states it, through a local `--slid` on the precedent `--ruled` set.
Off under `prefers-reduced-motion`, and off unless a caller asks for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tabs: a tab that turns on its own until someone takes it
`Tabs beat` advances the open tab on an interval. Taking any tab stops it for
good, which is the pause mechanism auto-advancing content owes a reader, held
once in the library rather than in each consumer. Reduced motion never starts
it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
voice: eleven languages walk through one card
The run carrying the surface's one claim was proving it with four small static
cards, and four of eleven is a sample — one curated for maximum ground spread,
with a caption that never said it had been chosen. It shows one card now and
every language takes it in turn, on a 2600ms beat with a 900ms slide, and a
reader who takes a tab keeps it. A sample that is not a sample cannot be
curated wrongly.
The markup stands beside the render, unchanged across all eleven, so the run's
claim can be checked instead of believed.
`grounds-differ` cannot judge eleven and is retired. Measured over every
ordering, the best cycle reaches a worst consecutive gap of 5.6 in Lab against
its threshold of 10, because eight of the eleven grounds are near-white — most
design languages are paper. It read one channel because the four specimens it
judged had been picked on that channel.
`change-shown` replaces it, aimed at what a sequence is for: a step nobody can
see is not a demonstration. Eight channels, each with its own noticing
threshold, at least four crossing between consecutive specimens. Verified in
both directions: the designed order carries a minimum of five, and a naive
order fires on base/ant 2, ant/carbon 2 and cupertino/material 3 — the three
pairs the eye reads as repeats and no others. The order of the eleven is solved
for, not chosen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ledger: what showing all eleven cost
P179 to P183, and the two entries that carry no number. Motion cost no atom.
The duration cannot be a language's beat. A law that reads one channel can only
judge a set picked on that channel. Faces and films cut rather than slide,
because `syntax: "*"` does not interpolate. And density fell from 647 to 548
without a single fact leaving the page, because the glyphs that went were three
repetitions of one sentence — the gauge counts repetition.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
navigator: the strip at the top is not a header
`Banner` was defended on the grounds that the platform had already fixed the
word: the component renders a `<header>` with no sectioning ancestor, so it does
map to `role="banner"`, and the ARIA definition of that landmark describes the
job closely. That is a real fact and it is the wrong one to name from. A
landmark role is what the platform must call this to expose it; it is not what
the design system calls it to its own readers, and to a reader `banner` is an
advertisement, a cookie notice, or a hero image.
The objection to `Navigator` was that a name stating one of two jobs installs
the two-role defect on day one — `--space-3`, `--nib` and `--heft` all acquired
a second role in silence, and a name can put one there deliberately. It fails
because the identity half is not a second job. The wordmark is a stop, not a
heading: it is where the reader is, and the ways out stand beside it.
Renaming found the defect the name was hiding. `Banner` set its title in `<h1>`
and every page also sets one in its `Hero`, so every surface here shipped two
`<h1>`, each declaring itself the heading of the document. The title is a
`<p class="navigator-name">` now, carrying the same declarations.
Nothing moved in `look.json` but the names: seven keys per language renamed,
every value under them byte-identical across all fourteen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
navigator: a crest, a lockup, and controls that stop being raw
Five complaints, and the interesting half of each was where the answer had to
live rather than what it looked like.
`@perish/crest` is a new package because a brand is the one graphic that must
not change when the language does — the exact opposite of `@perish/sign`, whose
sets are chosen by look. Chosen by product instead. The base geometry is
exported once and a product declares only its own mark, so a variant that
redraws the base cannot be written; what is left over is falsifiable and is a
law: a mark must stand inside the hold the base leaves it, measured in the
browser lane beside the icon framing law. The placeholder deliberately does not
fix colour, and that is recorded: the crest takes `--accent`, so a real brand
colour is the first thing that will fight fourteen grounds.
The wordmark was one string in one weight and it is not one thing. `@perish/`
is a scope and `design` is a name; the scope recedes to `--muted`, the name
takes `--bright` at `--heft`, and the identifier strip finally takes `--track`
instead of resetting tracking to zero. The lockup is a link, which is the
naming decision made operable: a wordmark that is a stop should be takeable.
`Link look="nav"` reveals its underline on focus as well as hover, and states
`here` for the surface the reader is on, with `aria-current`.
The two pickers are no longer a native select. They are `Menu look="bare"` with
`tongue` and `shade` — new shapes in base, material and ant, spurned by
terminal because a text medium has no mark for them. The trigger falls back to
its label through `.menu-cue:has(.sign)`, so the words appear only when no
shape rendered, without a second declaration saying so.
The requested expand-to-words was refused as a width transition: these controls
end a strip, so growing one moves every neighbour on hover. The words appear on
a layer — a tip under the mark, the label as the accessible name, the choices
in the panel with the standing one ticked. Zero layout shift.
`Menu` also did not hold its own room's contract. `layer` takes responsibility
for the top layer, escape and a trap, and it had a list that opened with
nothing that closed it. Escape and outside dismissal are held now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
room: a room is an obligation, so give two of them a door
The rooms carried obligations in prose and nothing checked that a member met
one. Worse, the catalog could file a component in a room its source does not
stand in and every guard stayed green: `drift.test.ts` compared the set of
names and never the grouping, so the axis the whole library is filed on was the
one thing no law read.
Two rooms state something a machine can hold, and which two was decided by the
rooms' own wording rather than by effort. `focus` says it owns a focusable
element and `layer` says escape; both are facts about the source.
`mark`, `arrange`, `enclose` and `document` say what a generator means, and
checking meaning would need a component to declare its own role — the proposal
this line refused for the cascade band and that keeps `one-payload` in a
person's hands. P117 again.
All three verified to bite: `Navigator` moved to `layer` in the catalog alone,
`Link` rendered as a span, `Menu` listening for the wrong key.
`Views` escapes all of it, and that is recorded rather than repaired. It has no
stylesheet, and every law that reads the tree reads only the generators that
carry one, because that filter is what makes the documentation law true. A
filter written to make one law true also decides what the laws built on it
cannot see.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
design: take the port from the sidecar binding
The manifest pinned `port = 4290` and `4287`, and `vite/config.ts` read
`SIDECAR_PORT` out of the environment and validated it by hand. Both existed
because the binding did not, and both are what the sidecar closed-loop task
declared as its acceptance test.
- `sidecar.toml`: both targets lease with `port = 0`. The trailing `--` in each
argument list is gone with the flag it used to guard.
- `packages/design/src/vite/config.ts`: `client.connect()` from
`@perish/sidecar`, then `held.control.port()`. The range check leaves with the
parsing; a caller of a binding does not restate the grant's own rules.
- `packages/design/src/vite/lib.ts`: `connect()` is async, so the plugin's
`config` hook awaits it once and every later hook reads the resolved value.
Vite runs `config` before `configResolved`, `buildStart`, `configureServer`
and `generateBundle`, so nothing observes the placeholder.
- `.npmrc`: the `@perish` scope resolves from the Forgejo registry.
Verified against the published binding rather than a local build:
```
preview -> grants {broker: tcp://127.0.0.1:41761, port: "33259"}
pid 2492265 (host), target 2492266
curl http://127.0.0.1:33259/health -> 200
```
`port = 0` leased 33259, sidecar announced it, the binding read it, vite pinned
it, `health_url` resolved against it, and `start --wait` returned only after the
target answered. No `process.env.SIDECAR_PORT` remains anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Land-Source: design/sidecar-binding@ed03d34cdb
|
||
|---|---|---|
| .forgejo | ||
| .runseal/resources | ||
| apps/docs | ||
| packages | ||
| rubric | ||
| .gitignore | ||
| .npmrc | ||
| AGENTS.md | ||
| biome.json | ||
| ectropy.toml | ||
| LEDGER.md | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| runseal.toml | ||
| sidecar.toml | ||
design
The Perish Svelte design system.
import { Button, Frame, Note } from "@perish/design";
import { design } from "@perish/design/vite";
packages/token carries the atoms, the contract that guards them, and ten
design languages built on them. packages/design carries one-word Svelte
components plus the framework-neutral Vite integration. apps/docs is the
bilingual gallery published at design.perish.uk.
Every generator is filed by what it takes responsibility for: mark,
arrange, enclose, focus, layer, document.
pnpm install --frozen-lockfile
pnpm check
pnpm typecheck
pnpm test
pnpm build
ectropy .
typecheck reads workspace source directly; it does not generate package
artifacts. build creates the npm payload and prerendered documentation site.
/proof renders every component on one screen. Add ?system= and a language
to dress it: swiss, material, terminal, cupertino, carbon, ant,
brutal, relief, glass, folio.