Lesson 11 of 14

Usage rules

How the system says how it is meant to be used: constraints in the contract, rules over the usage tree.

#The idea

A system that lists what it offers leaves the hardest knowledge, how to put the pieces together, to be rediscovered by every consumer and every agent. Milda carries that too, in two layers, under one principle: a rule never restates the contract, it says only what the contract cannot.

#What this lesson adds

An icon-only button whose contract constrains its props, and four rules for the library:

milda
component IconButton "A button whose only visible content is an icon." {
  archetype Button
  group Actions

  props {
    icon : string required
    label : string "The accessible name, when nothing visible names the button."
    labelledBy : string "The part that names the button, when one does."
    one-of label, labelledBy
  }

  events {
    click
  }

  part IconButton: container {
    emits { click on activate }
    layout { stack row; align center; distribute center }
    fill surface
    ink text
    corner radius.sm
    inset space.sm
    state hovered {
      fill accent-subtle
      ink accent
    }
    state focus-visible {
      ring accent
    }
  }

  part icon: icon {
    content bind icon
    ink text
    when ancestor IconButton hovered {
      ink accent
    }
  }

  part label: text {
    content bind label
    presentation accessible-name
  }
}
milda
rule one-primary-per-card warning {
  max 1 Button where variant = Primary within Card
  because "A card has one way forward. Two primary buttons make the reader choose between them."
}

rule no-badge-in-button warning {
  never Badge within Button
  because "A badge inside a button is read as part of its label, so the count becomes part of the action's name."
}

rule no-stacked-dialogs error {
  never archetype Dialog within archetype Dialog
  because "A dialog over a dialog traps focus twice. Replace the content of the first one instead."
}

rule values-from-tokens warning {
  tokens color, spacing, radius
  because "Colours, spacing and radii come from the foundations, so a theme change reaches every component."
}

#What to notice

#Interface constraints live in the props block

  • one-of label, labelledBy: exactly one is supplied. is at least one; at most one. required when loading from lesson 5 is the fourth member of the family.
  • They are decided from one instance, so the generated types can enforce them. Supplied means written at the use site; a declared default does not count.
  • presentation accessible-name on the label part says the text is not drawn, it becomes the accessible name. That is how an icon-only button keeps a label, and the constraint makes sure it has one.
  • when ancestor IconButton hovered paints the icon while the root is hovered: a descendant styled on an ancestor's state, by path.

#Usage rules are decided over the usage tree

  • A rule has an id, a severity, one statement and a reason. The reason is mandatory: it is what a person or an agent reads when the rule fires.
  • Six statements, each opening with its own word: max N S within S and min N S within S (counts, decided per nearest enclosing scope), never S within S (forbidden nesting at any depth), only S within S, S (a required parent), tokens color, spacing (values only from tokens of those types), prefer C for archetype A (the component an intent goes through).
  • A subject is a component by name, an (anything that realizes it, a native form included), or either narrowed by where variant = Primary over its props.

What the rules above say at a use site, in the generated library's terms:

tsx
// one-primary-per-card
<Card><Button variant="primary" /><Button variant="primary" /></Card>   // reported
<Card><Button variant="primary" /><Button variant="ghost" /></Card>     // fine
<Card><Button /></Card>                                                // fine: the default is primary, once

// no-badge-in-button: depth does not matter
<Button>Inbox <Badge label="3" /></Button>                             // reported

// no-stacked-dialogs: an archetype subject matches a native <dialog> too
<Dialog title="Delete?"><Dialog title="Sure?" /></Dialog>              // reported

// values-from-tokens: a literal is reported even when a token has that value
<div style={{ color: '#7c3aed' }} />                                   // reported
<div style={{ color: 'var(--ds-color-accent)' }} />                    // fine

#What the generator makes of it

The IconButton's props type rejects both labels and no label at compile time; the rules ship in AGENTS.md, errors first, each with its reason, and as data in design-system.json. A release lists added, removed and re-graded rules in its changes, and they never move the version: a rule cannot break a build.

tsx
/** Set exactly one of `label` or `labelledBy`. */
type IconButtonOneOf1 =
  | { label: NonNullable<IconButtonBaseProps['label']>; labelledBy?: never }
  | { label?: never; labelledBy: NonNullable<IconButtonBaseProps['labelledBy']> }
Built

milda check evaluates every rule kind and the interface constraints over a consumer’s TSX and JSX, following its own components across files. Stylesheets and CSS-in-JS are not read yet.proposal 0053

#Try it

Write rule tabs-have-a-tab error { min 1 Tab within Tabs because "…" } and read the warning: the rule names components the file does not declare, and is kept. Then leave out the .

Words in this lesson

  • A constraint over a group of props.
  • The prop must be given.
  • Inside a part: a presentation intent.
  • A condition.
  • A usage rule: how the system is meant to be composed, checked over the instances a consumer writes.

Open a word for its entry, or read them all in the reference.

#The file at the end of this lesson

milda
milda 0.10
prelude 0.0.2

meta { name "Acme" }

context-group ColorScheme { Light Dark }

context-group Variant { Primary Secondary Ghost }

group Actions "Things a person presses to make something happen."
group Forms "Collecting input."
group Feedback "Telling a person what is happening."
group Layout "Surfaces that hold the rest."

foundations {
  layer Base {
    token violet/100 : color = hex(ede9fe)
    token violet/500 : color = hex(8b5cf6)
    token violet/600 : color = hex(7c3aed)
    token violet/700 : color = hex(6d28d9)
    token violet/900 : color = hex(4c1d95)
    token gray/50 : color = hex(f8fafc)
    token gray/200 : color = hex(e2e8f0)
    token gray/500 : color = hex(64748b)
    token gray/800 : color = hex(1e293b)
    token gray/900 : color = hex(0f172a)
    token green/500 : color = hex(22c55e)
    token amber/500 : color = hex(f59e0b)
    token white : color = hex(ffffff)
  }

  layer Semantic {
    token accent : color = alias by ColorScheme { default -> violet.600; Dark -> violet.500 }
    token accent-hover : color = compute accent in oklch { darken 10% }
    token accent-subtle : color = alias by ColorScheme { default -> violet.100; Dark -> violet.900 }
    token on-accent : color = alias by ColorScheme { default -> white }
    token surface : color = alias by ColorScheme { default -> white; Dark -> gray.900 }
    token text : color = alias by ColorScheme { default -> gray.900; Dark -> gray.50 }
    token text-muted : color = alias by ColorScheme { default -> gray.500 }
    token line : color = alias by ColorScheme { default -> gray.200; Dark -> gray.800 }
    token success : color = alias by ColorScheme { default -> green.500 }
    token warning : color = alias by ColorScheme { default -> amber.500 }

    token space/xs : spacing = 4px
    token space/sm : spacing = 8px
    token space/md : spacing = 12px
    token space/lg : spacing = 16px
    token space/xl : spacing = 24px

    token radius/sm : radius = 6px
    token radius/lg : radius = 12px
    token radius/pill : radius = pill

    token hairline : border = 1px

    token size/sm : font-size = 13px
    token size/md : font-size = 15px
    token size/lg : font-size = 18px
    token weight/medium : font-weight = 500
    token weight/bold : font-weight = 700

    token font/sans : font-family = { family: "Inter"; fallback: sans-serif; source: google; license: "OFL-1.1" }
    token body : typography = { family: font.sans; size: 15px; weight: 400; line-height: 1.5 }
    token title : typography = { family: font.sans; size: 18px; weight: 700; line-height: 1.3 }

    token shadow/card : elevation = raw "0 4px 12px -2px rgba(15,23,42,0.12)"
    token fast : duration = 120ms
    token standard : easing = raw "cubic-bezier(0.2, 0, 0, 1)"
  }
}

component Button "The primary action, in three emphases." {
  archetype Button
  group Actions

  props {
    variant : context Variant = Primary "Which emphasis the button carries."
    disabled : boolean = false
    loading : boolean = false "Shows a spinner in place of the label."
    loadingLabel : string required when loading "Read out while the spinner replaces the label."
    label : string = "Get started"
    icon : string "A glyph shown before the label, by name."
  }

  events {
    click
  }

  part Button: container {
    emits { click on activate }
    bind state disabled disabled
    bind state loading loading
    layout { stack row; gap space.sm; align center; distribute center }
    fill accent
    ink on-accent
    border accent hairline
    corner radius.sm
    inset space.sm space.lg
    when variant = Secondary {
      fill surface
      ink text
      border line hairline
    }
    when variant = Ghost {
      fill surface
      ink accent
      border surface hairline
    }
    state hovered {
      fill accent-hover
      border accent-hover hairline
    }
    state hovered when variant = Secondary {
      fill accent-subtle
    }
    state hovered when variant = Ghost {
      fill accent-subtle
      border accent-subtle hairline
    }
    state focus-visible {
      ring accent
    }
    state disabled {
      opacity 50
    }
    motion {
      transition fill ink duration fast easing standard
    }
  }

  part icon: icon {
    content bind icon
    ink on-accent
    when variant = Secondary {
      ink text
    }
    when variant = Ghost {
      ink accent
    }
  }

  part label: text {
    slot of content default
    content bind label
    text body
    weight weight.medium
  }
}

component IconButton "A button whose only visible content is an icon." {
  archetype Button
  group Actions

  props {
    icon : string required
    label : string "The accessible name, when nothing visible names the button."
    labelledBy : string "The part that names the button, when one does."
    one-of label, labelledBy
  }

  events {
    click
  }

  part IconButton: container {
    emits { click on activate }
    layout { stack row; align center; distribute center }
    fill surface
    ink text
    corner radius.sm
    inset space.sm
    state hovered {
      fill accent-subtle
      ink accent
    }
    state focus-visible {
      ring accent
    }
  }

  part icon: icon {
    content bind icon
    ink text
    when ancestor IconButton hovered {
      ink accent
    }
  }

  part label: text {
    content bind label
    presentation accessible-name
  }
}

component Badge "A small status pill." {
  archetype Badge
  group Feedback

  props {
    tone : enum(neutral, success, warning) = neutral
    label : string = "New"
  }

  part Badge: container {
    layout { stack row; align center }
    fill accent-subtle
    ink accent
    corner radius.pill
    inset space.xs space.sm
    when tone = success {
      fill success
      ink on-accent
    }
    when tone = warning {
      fill warning
      ink gray.900
    }
  }

  part label: text {
    slot of content default
    content bind label
    text size.sm
    weight weight.bold
  }
}

component Card "A surface composed from the components above." {
  group Layout
  props {
    tone : enum(neutral, success, warning) = neutral
  }

  part Card: container {
    layout { stack column; gap space.md; size fill fit }
    fill surface
    ink text
    border line hairline
    corner radius.lg
    inset space.xl
    elevation shadow.card
  }

  part Header: container {
    layout { stack row; gap space.sm; align center; distribute between }
  }

  part Header.Title: text {
    content "Project Aurora"
    text title
  }

  part Header.Status: instance {
    instance Badge {
      label = "Beta"
      tone = bind tone
    }
  }

  part Body: text {
    slot of content default
    ink text-muted
    text body
  }

  part Footer: container {
    layout { stack row; gap space.sm; distribute end }
  }

  part Footer.Primary: instance {
    instance Button {
      label = "Open"
    }
  }

  part Footer.Secondary: instance {
    instance Button {
      label = "Share"
      variant = "Secondary"
    }
  }
}

component TextField "A single-line input with an error state." {
  archetype TextField
  group Forms

  props {
    label : string required "What the field asks for. Also its accessible name."
    value : string
    placeholder : string = "you@example.com"
    invalid : boolean = false
    error : string "Shown under the control while invalid."
    disabled : boolean = false
  }

  events {
    valueChange
  }

  part TextField: container {
    bind state disabled disabled
    bind state error invalid
    layout { stack column; gap space.xs }
  }

  part label: text {
    content bind label
    text size.sm
    weight weight.medium
  }

  part control: input {
    content bind placeholder
    emits {
      valueChange on input payload bind value
    }
    bind value value
    value string
    a11y { labelled-by label; described-by hint }
    layout { size fill }
    fill surface
    ink text
    border line hairline
    corner radius.sm
    inset space.sm space.md
    text size.md
    state focus-visible {
      border accent hairline
    }
    state error {
      border warning hairline
    }
    state disabled {
      opacity 50
    }
  }

  part hint: text {
    present when invalid
    content bind error
    ink warning
    text size.sm
  }
}

component Switch "One boolean, as a sliding switch." {
  archetype BooleanInput
  group Forms

  props {
    checked : boolean = false "Whether it is on."
    label : string required
    disabled : boolean = false
  }

  events {
    checkedChange
  }

  part Switch: container {
    bind state disabled disabled
    layout { stack row; gap space.sm; align center }
  }

  part toggle: input {
    control switch
    a11y { name bind label }
    emits {
      checkedChange on change payload bind checked
    }
    bind value checked
    value boolean
  }
}

component Dialog "A blocking layer that asks one question." {
  archetype Dialog
  group Feedback

  uses presentation(surface: dialog) as P
  uses dismiss as D
  wire D.dismissRequested -> P.close

  props {
    trigger : string = "Open" "The label of the control that opens the dialog."
    title : string required "The dialog's accessible name."
    open : boolean = false
  }

  part Dialog: container {
    bind state open open
  }

  part trigger: control {
    layout { stack row; gap space.sm; align center; distribute center }
    fill accent
    ink on-accent
    corner radius.sm
    inset space.sm space.lg
    state focus-visible {
      ring accent
    }
  }

  part trigger.label: text {
    content bind trigger
    weight weight.medium
  }

  part backdrop: container {
    fill text
    opacity 50
  }

  part surface: container {
    layout { stack column; gap space.md }
    fill surface
    ink text
    corner radius.lg
    inset space.xl
  }

  part surface.title: text {
    content bind title
    text size.lg
    weight weight.bold
  }

  part surface.content: container {
    slot of content default
    ink text-muted
  }
}

rule one-primary-per-card warning {
  max 1 Button where variant = Primary within Card
  because "A card has one way forward. Two primary buttons make the reader choose between them."
}

rule no-badge-in-button warning {
  never Badge within Button
  because "A badge inside a button is read as part of its label, so the count becomes part of the action's name."
}

rule no-stacked-dialogs error {
  never archetype Dialog within archetype Dialog
  because "A dialog over a dialog traps focus twice. Replace the content of the first one instead."
}

rule values-from-tokens warning {
  tokens color, spacing, radius
  because "Colours, spacing and radii come from the foundations, so a theme change reaches every component."
}