A portable interface contract. Rules, tokens, semantic roles, components and inspectable reference scenes that carry one design system from the browser to Compose to GPUI — without turning any toolkit into the design system.
v4.6.0 · MIT · 36 gates, one command
A design system is a set of decisions that other people make every day. This package holds those decisions in a form a machine can check, and projects them onto the targets that have to honour them.
The distinguishing idea is that portable meaning is authoritative. The contract says what a surface is for; each target says what that is worth in its own idiom. Nothing here is a picture of a thing, and nothing here is somebody else's component library with the serial numbers filed off.
The system is in four tiers, and each one is a file you can read:
| Tier | File | What it holds |
|---|---|---|
| Rules | core/rules.json |
29 numbered obligations — COL-001, A11Y-001, SUR-001 — each with an observable and a test |
| Envelope | core/ENVELOPE.md |
the shell's composition invariants — compose first and paint once, one curvature authority, a settled pose rebuilt from the canonical state — and what the field's prototype proves of them |
| Palette & surfaces | core/TOKENS.json, core/SURFACES.json |
the approved colours, the elevation ramps, the spacing steps, the family and the weights |
| Semantic roles | core/ROLES.json |
16 roles — surface, ink, accent, focus, positive — bound to a token on each substrate |
| Components | core/COMPONENTS.json |
14 elements: their roles, states, variants, geometry, typography and the accessibility they owe |
Everything below the tiers is a projection: generated from the contract, checked in, and refused if it drifts.
Five approved colours. The system spends Signal Orange on the principal operation of a context and on nothing else; the rest carry structure.
Anthracite #111418 |
the dark substrate, and the ink on it | neutral/anthracite |
Shell White #F7F3ED |
the light substrate | neutral/shellWhite |
Signal Orange #FF6A00 |
the one accent | signalOrange/base |
Beret Green #5F7F52 |
a condition that is well | beretGreen/base |
Royal Blue #4169E1 |
focus, and selection | royalBlue/base |
Each family carries a ramp of steps. The surface layer adds 66 elevation steps across two substrates, every one of them identified by its own lightness and chroma, so a surface cannot drift out of its family without the check noticing.
The tier that was missing until this release, and the reason one frontend in the field carried two hundred and fifty hardcoded hex values in one stylesheet. A role says what a thing is for; the contract says what that is worth on each substrate.
| Role | Light | Dark | |
|---|---|---|---|
surface |
Shell White | Anthracite | the page behind everything |
surface-raised |
#F7F8F8 |
#1D2228 |
a card, a panel, a tile |
surface-overlay |
#FBFBFB |
#242A31 |
a dialog, a menu, a popover |
surface-sunken |
#E1E5E8 |
#14181C |
a well: a code block, a track |
ink |
Anthracite | Shell White | the text that is read first |
ink-muted |
#4B5560 |
#8D99A6 |
a caption, a secondary label |
hairline |
#E3E5E8 |
#2B3138 |
the line between two surfaces |
accent |
Signal Orange | Signal Orange | the principal operation, and only that |
accent-ink |
Anthracite | Anthracite | the text that sits on the accent |
focus |
Royal Blue | Royal Blue | where the keyboard is |
selection |
#E8EDFF |
#25396B |
the ground of a chosen thing |
info |
#2E6FA8 |
#3D8DCC |
what a reader is told, as a mark |
positive |
Beret Green | #8DA982 |
a condition that is well, as a mark |
positive-surface |
#E8EFE5 |
#455F3D |
the tinted surface behind the word |
caution |
#8A5600 |
#F5A524 |
a condition that asks for inspection |
critical |
#A32126 |
#F26367 |
a condition that is a failure |
The split between positive and positive-surface is measured, not tasteful:
the approved green measures 4.09 against the light substrate and cannot
carry body text, so it is the mark — the pip beside the word — and the tinted
surface behind the word is a separate role. Every pairing that carries text
is checked at 4.5 on both substrates.
The names come from a measurement of the field, not from a preference. The private frontends that use this system were walked, their component names counted, and the count taken over lineages rather than repositories: two of those repositories are one lineage, and the design sheet they share is one artefact written once, so counting them twice would have made one opinion look like an agreement.
Twenty-six names appear in two or more independent lineages. The parts of a dialog are parts of a dialog, and the domain-shaped ones are patterns rather than components. What remains is what a web surface, a native surface and a portal already agree on.
button |
the control a person presses | 4 variants · 6 states |
text-input |
a field with a visible label | 5 states |
badge |
a short mark that says what a thing is | 4 variants |
card |
a raised surface holding one thing | |
dialog |
an interruption that takes the keyboard | 6 parts |
tooltip |
an explanation that goes away | 3 parts |
select |
a control that opens a list of choices | 3 parts |
tabs |
panels that share one subject | 3 parts |
toaster |
where a transient message arrives | 2 parts |
toggle |
something that is on or off | 3 parts |
command-palette |
a field that takes a command | 5 parts |
icon |
a glyph from the ratified subset | |
section-heading |
the heading of a section | 4 parts |
system |
the typographic and numeric base |
Every component states its roles, its states, its variants, its geometry, its typography and the accessibility it owes — and the stylesheet is rendered from those statements by a renderer that is generic. Adding a component to the contract means stating it, not adding a case to the renderer.
The field uses thirty-six more arrangements the system did not describe: twenty-nine patterns and seven primitives. A pattern is an arrangement rather than a thing — a ledger, a rail, a notice, a run of prose — and a primitive is a thing the type, spacing, motion or accessibility tier governs, with no component of its own.
The contract specifies all thirty-six now, and it specifies them by measurement: the parts each one has, the tokens it consumes, the geometry it establishes, the type it sets and the behaviour it carries are read off the sheet the field already writes with, and a gate checks both ways. A part the contract names that the sheet does not have is a failure; a declaration in the sheet that the contract does not name is a failure. Nothing about the field's vocabulary is now a matter of opinion.
Three names overlap with the system's elements: the field's action,
state and section are its own spellings of the button, the badge
and the section-heading. The field's own code converges on the system's
names, so those are canonical, and the contract says exactly where the
sheet's versions differ — the sheet's action carries a mark and an arrow
the system does not declare, and its badge carries two conditions the
system's does not have.
The gnu6-design package carries all of it: the field's thirty-nine
names, the system's fourteen elements under the same prefix, and the
system's own roles and tokens underneath. One stylesheet, one convention,
and the contract behind it.
The contract is portable; the rendering is not. Each target consumes the projections and restates nothing.
<link rel="stylesheet" href="profiles/web/tokens.css">
<link rel="stylesheet" href="profiles/web/roles.css">
<link rel="stylesheet" href="profiles/web/components.css"><button class="gnosix-button" data-variant="primary">Enable</button>
<input class="gnosix-text-input" id="target">
<p class="gnosix-badge" data-variant="attention">Request pending</p>The substrate is chosen once, by the attribute:
<body data-gnosix-substrate="dark">Roles are bound per substrate, so a component is written once and reads on
both. profiles/web/state.ts projects a request into its observed state, and
never announces a success it has not observed.
use gnosix_ds::roles;
let theme = roles::DARK; // or roles::LIGHT
let surface = theme.surface; // the contract's own constantThe crate is dependency-free, and the values it carries are the contract's constants rather than a second transcription of them.
val surface = GnosixRoles.of("dark", "surface")profiles/interchange/ carries the tokens in the two formats other tools
already read: DTCG (gnosix.tokens.json) and Style Dictionary
(gnosix.style-dictionary.json).
figcraft/ projects the system into a FigCraft project — ten boards, the
whole component vocabulary, and a published library another .figcraft can
consume. The projection is a generated artefact of this repository, byte-gated
against its source, not a place the contract is edited.
core/ the contract: rules, tokens, surfaces, roles, components, semantics
profiles/ the projections: web, native, android, interchange, motion
references/ six annotated scenes — actions, adaptive, fields, identity, state, surfaces
site/ the public visual reference
tools/ the gates, and the generators behind every projection
figcraft/ the FigCraft projection
assets/ the ratified fonts, icons and identity masters
profiles/frontend-audit/ holds the audit of the ten private frontends that
consume this system: a matrix of what each one does against each
criterion, an inventory of every colour, family and length it uses that
the contract does not sanction, and an absorption plan that says what each
one must change and in what order.
The measurement is taken over lineages, and the reason is written down: a repository is not an opinion, and a design sheet written once and vendored in two places is one artefact, not two agreements.
The task inventory is ROADMAP.md: every task with its
objective, where it lands, what proves it is finished, what it needs
first and what it costs. A task without a proof is a wish.
The package is qualified by one command, and it is the same command locally and in CI:
python3 -m tools.ciIt runs every gate in the list and prints one verdict. The list lives in
tools/ci.py, in the repository, so a developer and an agent run exactly the
same thing. A check list reproduced in YAML is a second implementation of the
contract, and the second one is the one nobody runs locally.
The gates under tools/ need no packages at all. They are standard-library
only, deliberately: a gate that cannot run without a wheel is a gate that
does not always run.
tokens project cleanly the surface layer is derived
the contrast table measures what the palette says the token exports are current
the role layer is bound to the contract the component stylesheet is what the spec projects
every colour is one of ours the surfaces use the system's colours
version authority contract validates
manifest matches the tree alpha masters are straight
mascot alpha is repaired svg wrappers embed once
favicons are the right crop icon subset is vendored
no ad-hoc glyphs the FigCraft projection matches the source
the two token projections agree the web code matches the design
the native profile compiles and its tests pass the gnu6-design package is what the contract cuts
every name the field declares is classified every arrangement is specified as the sheet has it
every gate says what it must say the severity register is honest
the gates stay inside the repository the surfaces render as the system carries them
the work's own places are inside the repository the native profile carries the contract's states
every scheme keeps the system's guarantees the register of retractions is honest
the register of intent is honest the consultation suite is what the contract says
the register of upstream is honest test suite
A gate that exits zero is a gate that passes, whatever it prints on the way. For a long time the qualification said what passes and never what to expect, so a gate could begin to say something else and stay green.
ci/GATES.md says it now: one numbered section per gate, the command it
runs, and the words it must contain. The qualification compares each
gate's own output with what the document says, so a gate that changes its
mind fails the run instead of passing it quietly. A gate added without a
section fails the run too, and a section that names a gate that does not
exist fails it as well.
A contract says what it holds. It does not say what it looks like: that a badge carries its condition as a shape and a word, that a focus ring is visible, that a text reads on its ground. Those are facts about pixels, and a fact about pixels has to be looked at.
tools/eye.py renders the system's own web surfaces on both substrates,
walks them with axe, and compares the pictures with the ones the system
carries. The comparison is on the bytes, so the picture the system ships
is the picture that was looked at. The accessibility findings are a
ratchet: the system carries the list of what it knows, and a finding
that is not in it is a new one and a failure.
The browser is pinned by the lockfile and provisioned by
ci/provision-browser.sh, which the CI job, the release job and the
local simulation all run. Where there is no browser — a copy of the
package, which carries neither — the gate says so and stops: that is the
third answer, not a failure.
core/DEVIATIONS.json is the register of what the system and the field do
not agree about, what each difference is worth, and who owns it. The
scale runs from critical — the contract is wrong — to none, which is a
difference that is not a deviation at all.
A closed deviation carries a probe: the finding that the difference has not come back. The gate runs the probe, so a closed deviation cannot reopen without the gate saying so.
The package states its version in more than one place, and until V3.4.2 each of those places was written by hand — which is how a release shipped with a manifest still announcing an older version and a web manifest announcing a different one, with nothing in the gate chain noticing.
There is now one value with one owner:
python3 -m tools.version --show # the version and every slot that states it
python3 -m tools.version --check # the gate: does every slot agree?
python3 -m tools.version --sync # write VERSION into every slotVERSION is the source. The rules file, the motion profile, the projection
map, the six reference scenes and their sheets, the site's display slots, the
web manifest and the package manifest are the slots. --check verifies the
property rather than a diff, and reports the slot that disagrees rather than
a patch.
python3 -m tools.release status # where the repository stands
python3 -m tools.release bump --patch # move VERSION, sync, rebuild the manifest
git commit -am "release: 4.0.1"
python3 -m tools.release prove # run the release job locally, record the proof
python3 -m tools.release build # dist/Gnosix-DS-V4.0.1.tar.gz
python3 -m tools.release tag # annotated v4.0.1
git push && git push --tags
python3 -m tools.release publish # GitHub release with the archiveThe order is enforced, not conventional. bump moves the version and
rebuilds the manifest, because the manifest hashes the version file. tag
refuses a dirty tree, because a tag names a state and a dirty tree is not
one. publish refuses unless the tree is clean, the tag exists, the tag
agrees with the version, and the archive has already been built — and it is
idempotent, because a release that already exists with the same content
digest is already the one this tree would publish.
Push the tag, and let the workflow publish it. The release workflow qualifies the tagged commit, builds the archive from it and attaches it. A person publishing by hand as well is two correct things colliding.
The archive is built in two layers, and the two are reported separately because only one of them is a statement about the source alone:
- The tar stream inside is byte-reproducible — entries are sorted, timestamps are fixed, ownership is cleared, and the gzip header carries neither a name nor a time. Its digest is the same on every machine that checks out the same commit, and it is the one published in the release notes.
- The gzip wrapper around it is not, because gzip's output depends on the zlib build that produced it.
So a consumer who rebuilds the archive gets a bit-identical tar and, on a
different zlib, a different .tar.gz around it. Compare the content
digest.
The ratified icon pack is Tabler Icons 3.49.0
and the ratified flag pack is Tabler Flags 0.1.1.
Both are MIT licensed, Copyright (c) 2018-2026 Paweł Kuna, and the verbatim
licence text ships in assets/icons/licenses/.
Rule ICON-001 governs the use of glyphs: interface icons come from the
ratified subset, and a text character may not stand in for one. A character
that renders as an icon is still an icon, with none of the guarantees — it
inherits the font's metrics, it cannot be recoloured independently, and it is
a different shape on every platform.
The package vendors a curated subset at a pinned upstream tag rather than
referencing a CDN, exactly as it vendors Space Grotesk. A design contract
whose appearance changes when a third party publishes is not a contract. The
subset is 41 icons and 8 flags, each recorded in
assets/icons/icon-manifest.json with its upstream URL, its role and its
SHA-256.
The ratified gnosix, gnu6.live and gnu.in.labs cube images are the
visual masters, and the ratified overlapping orange / Royal Blue / green
plate mark is unchanged.
One alpha compositing variant per identity ships as *-cube-alpha.png
plus an exact-fidelity SVG wrapper. These are the same artwork with the
authored dark field carried as alpha: the RGB of every fully opaque pixel is
byte-identical to the corresponding opaque master, and the asset suite
verifies that rather than asserting it. The edge pixels are straight alpha —
the subject colour at true coverage, not a premultiplied blend. Per ID-004
they are sanctioned on neutral substrates only. Over Signal Orange, Royal
Blue or Beret Green the opaque master is used, because a chromatic substrate
would collapse the cube's dark faces into the brand hue.
profiles/motion/ supplies the motion meaning: six laws (causality,
performance, continuity, interruptibility, reduced-motion equivalence,
stillness-before-climax), four prohibitions, seven motion roles each with a
declared reduced-motion projection, and seven seams. It deliberately does
not ratify a global duration ladder.
MOT-BAN-IDENTITY-MOTIONforbids applying motion to a ratified master. A mark is a still thing and the frame moves around it.targets.remotion.reduced_motion.detectionrequires reduced motion to be an explicit input, because a headless renderer reports the render machine's motion preference rather than the viewer's.
Web, Android and gnos.ux/GPUI are mapped. Motion is mapped, implemented
by the Remotion target. None is promoted to rendered or verified until
target-scoped evidence is recorded in this package: renders produced by a
consumer live in that consumer's repository, and the evidence ladder is
explicit that a capture has to be recorded against the claim it supports.
The package does not promote a profile merely because another profile renders
correctly.
MIT — see LICENSE. The redistributed third-party assets and their
licences are listed in THIRD_PARTY_NOTICES.md and
per file in assets/LICENSES.md.
The release history is in CHANGELOG.md. The version this
checkout states lives in VERSION and nowhere else.