Skip to content

Design system: identities, contracts, behaviour, models, derived variants, codegen and an example over @hwc/components - #36

Open
beharguy wants to merge 16 commits into
mainfrom
design-system
Open

beharguy wants to merge 16 commits into
mainfrom
design-system

Conversation

@beharguy

@beharguy beharguy commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #37

Summary

A .uidx file becomes the identity of a component: intent, anatomy, layout and styles, plus a written contract, behaviour guidelines and view models. Components are renders of that identity — the viewer canvas, HTML/CSS and React here; Figma unchanged. Nothing in the file computes.

Documented in six ADRs, implemented end to end, with an example package rendering six identities over the headless @hwc/components library, and an inspector that edits all of it.

Decisions (docs/decisions)

  • 0012 Design-system model: identity + contract + behaviour guidelines; components are renders.
  • 0013 ## Contract: props, events, slots, form, accessibility, composes. implements binds a headless root, part binds its parts. <Prop sample> for demonstration values. <State> only for states the element produces itself; <Part> descriptions optional. The identity never spells a library: uidx.json may carry a profile and per-component bindings.
  • 0014 ## Behavior: short - id: sentence bullets. No when, no code.
  • 0015 ## Models and ## Examples: view models declared once, shared across pages, named by a prop's type; every field explained; samples for demonstration only.
  • 0016 Variants as renders: a <Styles> table keyed by visual enum props and states derives the set. Enum props are axes, visual booleans are states, hover/focus/active need no declaration. Derived variants are addressable: a state is designed on the canvas and its row is written.
  • 0017 Collections and code targets: <Repeat> on a repeating slot (of="items"), slots and render props in React, compositions render the instance they hold, shadow parts through ::part(), codegen config shared by CLI and viewer.

Packages

  • format: the four regions, the <Styles> table, <Repeat>, spec types, diagnostics UIDX140–152, the style and contract patch ops with inverses; contract-only helpers (axes, state kinds).
  • schema: derived variant sets, sample bindings, repeat expansion, derivedTarget, contractJson, auditDesignSystem with a shared model index; pass-through instance props resolve in the consumer's scope; slots hug; whole-attribute paint aliases compose strokes.
  • codegen (new): HTML/CSS and React 19 over headless custom elements, contract JSON, conformance against custom-elements.json, shadow parts, library profiles and bindings.
  • server: uidx.json gains headless (path or { manifest, profile, bindings }) and codegen (out, targets); routes serve the library, discover libraries from dependencies' customElements, write the choice, and generate code; the symbol lint reads contract props.
  • viewer: a Contract tab beside Design — implements and parts bound from either end, repeat slot and count, library picker, the contract itself editable with "Fill from library", a Generate button; derived variants selectable and editable on the canvas; a Repeat tool.
  • cli: uidx check audits the whole document; uidx contract; uidx codegen reads headless and codegen from uidx.json.
  • examples/design-system: Checkbox, Field, CheckboxField, Button, ContactOption, ContactList over @hwc/components, generated output committed with a drift test, a Vite demo page; uidx check passes on the whole document.

Verification

  • Every suite passes (format, schema, server, agent, viewer, cli, codegen, example); root lint, typecheck and prettier clean; the example's generated output has no drift.
  • Verified live in the viewer: the derived checkbox states, the button set with per-variant hover, the contact list's repeated rows; editing a hover state from the canvas wrote root:opacity into its style row; editing a prop's description from the Contract tab rewrote the <Prop>; the Generate button wrote the same 36 files the CLI writes.

🤖 Generated with Claude Code

Guy Behar and others added 8 commits September 29, 2026 23:46
Identity, contract, behaviour guidelines, models and examples, variants as
renders, collections and code targets. Theming beyond modes is recorded as a
future task.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A .uidx file may carry ## Contract, ## Behavior, ## Models and ## Examples
after the visual contract, and a <Styles> table beside the root. They are
lowered into doc.spec as declarations, never scene nodes; the tree the
viewer draws is unchanged. <Repeat slot count> multiplies one instance for
the design tools. The emitter prints the regions back verbatim so uidx fmt
keeps them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…system audit, uidx contract

A component with a <Styles> table is drawn as the variant set the table
derives, one synthetic tree per combination, unlinked from the file. A
contract prop with a model resolves {item.field} to the model's samples, and
a <Repeat> multiplies its instance at successive sample indexes. The audit
checks parts, slots, rows, axes, models, bindings and repeats against each
other, and uidx check runs it. uidx contract prints the spec as JSON.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…system over @hwc/components

@uidx/codegen renders each component identity to code (ADR 0017): a
stylesheet keyed by the headless root's attributes and states, a markup
fragment with sample content, a typed React component with props and events
from the contract — a plain slot as a ReactNode prop, a repeating slot as
items plus renderItem generic over the model, text parts as compound
sub-components, compositions as the instance they hold — plus models.ts,
runtime.ts, elements.d.ts and the contract as JSON. With a
custom-elements.json manifest, each contract is checked against its element.
`uidx codegen` drives it; `--check` fails on drift.

examples/design-system carries Checkbox, Field, CheckboxField, Button,
ContactOption and ContactList as .uidx identities over the vendored
@hwc/components build, with generated/ committed and tested for drift, a
Vite page mounting the HTML fragments beside the React components, and the
React output type-checked against React 19.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gh props

Canvas fixes found by rendering the example end to end:

- A whole-attribute stroke alias (`strokes="{border#default}"`) skipped the
  stroke composition, so the stroke kept its colour and lost its weight and
  drew as nothing. Both paths now share `wholePaintAlias`.
- The derived component set no longer carries the component's own paint and
  layout; those live on each variant's root (ADR 0005 §5).
- A `<Slot>` that says nothing about its size hugs its placeholder instead of
  the engine's 100x100 box.
- `<Prop sample>`: a demonstration value for the canvas and generated markup
  (ADR 0013 amended); a text bound to a prop with neither sample nor default
  renders empty.
- An instance's `props={{ label: '{label}' }}` resolves in the consuming
  component's scope before entering the definition's, and a component
  declared by its contract alone accepts passed values. The HTML target does
  the same for compositions.
- `layoutAlign` maps to `align-self` in the CSS target.

Example: hug sizing declared on the hugging components, per-variant hover
rows for the button, samples on text props, light background for the demo.
Agent's insertable-element test now lists `Repeat`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ess library

The inspector gains a Contract tab beside Design (ADR 0013 §3 amended).
Figma's precedent: bindings live in the properties panel, declared on the
component and applied from the layer, with choices from a curated list.

- A component chooses the element it implements from the library's roots; a
  text field only when the document names no library.
- Each declared part is a row: bound rows name their layer and select it,
  unbound rows are a picker over the layers that could draw it. Binding from
  a new layer unbinds the old one in the same patch. A layer's own row offers
  the same parts, naming who holds the taken ones.
- A <Repeat> picks a declared repeating slot (reselecting itself at its new
  address) and its count. Slots show whether the tree provides them.
- The file's contract (props, events) is shown beside the bindings; the tab
  badge counts parts still to bind.

uidx.json gains "headless": the library's custom-elements.json, served on
/__uidx/headless and read on every document:opened; uidx codegen uses it
when --manifest is absent. Roots and parts follow the <root>-<part> naming
convention (plus cssParts), with a leaf test so hwc-text-input stays a root
beside hwc-text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The manifest's own vocabulary for parts is `cssParts`: parts inside the
root's shadow tree, styled from outside through `::part()`. Until now every
part had to be a `<root>-<part>` element, which is one library's convention.

- The component model records each part's kind. A shadow part is styled as
  `root::part(name)` in the CSS target, emits no element in HTML or React,
  and gets no compound sub-component.
- Conformance accepts either kind. A shadow part with design content under
  it warns rather than fails: the library draws the part, so that content is
  styled but not rendered.
- The Contract tab marks shadow parts with a pill on the component's list
  and in the layer's picker, so an author knows it can be styled, not filled.

ADR 0017 §3 records the rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The conformance warning for design content under a shadow part is gone. The
library drawing the part, and the design drawing its own for the canvas and
Figma, is how a shadow-part library works; a warning nobody can act on is
noise. Conformance accepts a cssParts entry exactly as it accepts an
element. ADR 0017 §3 reworded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Guy Behar and others added 8 commits September 30, 2026 12:51
The contract API, simplified to what each renderer actually reads:

- `<States structural styling>` is gone. A boolean prop marked `visual` is a
  state; `hover`, `focus` and `active` are the browser's and need no
  declaration; `<States><State name>` remains only for states the element
  produces itself, rendered as `:state(name)`. The state axis is default,
  the visual booleans in prop order, the interaction states in table order,
  then the declared ones.
- `<Parts>` is optional and written like every other list, `<Part name>`
  with a description. The tree's `part=` bindings are the declaration.
- A model is named by a prop's type — `Contact`, `Contact[]` — and shared
  across pages: declared once, on the page of the component that shows it.
  `model="{models#…}"` is gone; a repeating slot names the list prop it
  iterates, `of="items"`, so the model is written once. The example no
  longer declares Contact twice.
- The parser names each old spelling and says what replaced it.

The contract-only helpers (axes, state kinds) move to @uidx/format so the
server's symbol lint can read them: `componentProps` now includes contract
props, a `{label}` passed through an instance is not judged at the use, a
`{item.name}` binding resolves through the model prop, and a derived set
draws every combination. `uidx check` passes on the whole example for the
first time; the audit runs after every page is parsed with a shared model
index. The Contract tab marks visual booleans as states and lists declared
element states.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The identity never spells a library. This makes that true end to end:

- uidx.json's `headless` may be an object: `manifest`, a `profile` (how the
  library reflects props, its own states and parts — attributes, data
  attributes, classes, `::part()` or `data-part`) and `bindings` (its tag,
  part, event and attribute names per component). The code target and
  conformance read both, so the checkbox renders as `sl-checkbox`,
  `sl-checkbox[checked]::part(control)` and `sl-change` from the same file
  that renders over @hwc/components. Defaults are the example's conventions.
- When nothing is named, the server discovers libraries the project's
  dependencies ship (the `customElements` field of package.json) and the
  Contract tab offers them, or any path; the choice is written into
  uidx.json through the headless route. The tab says which tag a binding
  renders a component as.
- `uidx codegen` passes the profile and bindings through from uidx.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…b that edits the contract

Phase 3 — a state is designed the Figma way. Derived variants are
addressable, so selecting the hover checkbox on the canvas shows its root
frame in the inspector with a "state=hover of Checkbox" chip, and a change
to it becomes a cell of the `state="hover"` row through the new `style`
patch op (format), rather than a patch to a node with no source. An edit
to the default combination lands on the base tree; a position, a name or
structure is refused with the reason. Undo inverts the cell. The canvas'
own scene-write path resolves against the derived document too. The
toolbar gains a Repeat tool that wraps the selected instance in a
`<Repeat>` on the first open repeating slot; the patcher leaves a Repeat
unnamed, as it does a Variant.

Phase 4 — the Contract tab edits the contract. The new `contract` patch op
writes one `<Prop>`, `<Event>`, `<Slot>`, `<State>` or `<Part>` in
canonical form, creating its list and the `## Contract` region when absent
and removing a list its last item leaves; it inverts to the previous
declaration. In the tab, each declaration opens into a form (description,
type, default, sample, visual and controllable; a slot's repeats, `of` and
`accepts`), a row adds one, and "Fill from library" declares what the
implemented element exposes and the contract lacks, with the manifest's
descriptions or placeholders the tab marks. The viewer's library model
carries the manifest's types and descriptions for that.

Docs: ADR 0013 §3, ADR 0016 §4, ADR 0017 §2, a design-system walkthrough
in the README, the example README, the changelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ate button

uidx.json gains an optional `codegen` — `out` (relative to it) and
`targets` — that `uidx codegen` reads when `--out` is absent and that the
server's /__uidx/codegen route runs for the Contract tab's Generate
button: the same generator over the same pages with the same library, so
the panel and the command line write the same files. A page that does not
parse or a contract the library contradicts stops the run before anything
is written. The example names ../generated and the button reproduces its
36 committed files byte for byte. Issue #37 and the PR description now
record everything delivered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI typechecks before it builds, and the example's tsconfig did not carry
the base config's `customConditions: ["development"]`, so `@uidx/format`
and friends resolved to a dist that did not exist yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sories

Advisories published after main's last green run; every fix is a patch
release inside the existing ranges. The release audit passes again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Design system: identities, contracts, derived variants, code targets and the Contract tab

1 participant