Lesson 5 of 14

The contract

Props with types, defaults, prose and conditions; events, and the parts that raise them.

#The idea

The contract is the component's identity. Consumers code against it, the generated types enforce it, and a release's version is computed from how it changed. This lesson spells out everything a props block can say.

#What this lesson adds

milda
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"
}

events {
  click
}
milda
bind state loading loading

#What to notice

  • A prop line reads left to right: name, colon, type, then optionally a default, whether it is required, and its prose in quotes. The colon is required: a name and then its type is written one way everywhere in the language.
  • Types are neutral. , , , enum(a, b), array<T>, object { … }, a shared by name, and context Group. Each target maps them: a TypeScript union here, a Swift enum there.
  • required when loading is a constraint in the contract. The label that replaces a spinner must exist whenever there is a spinner. The condition reads the component's other props only, because at the use site nothing else has a value yet. Lesson 11 has the rest of this family.
  • The prose is part of the contract. The quoted sentence after a prop travels into the generated types as a doc comment, into DESIGN.md, and into what an agent reads before it writes code.
  • Events have two halves. The block declares them; a part's block says when one is raised and with what payload (payload bind value, in lesson 8).

#What the generator makes of it

A required-when becomes a type the compiler enforces: the props type is the base interface intersected with a union, so <Button loading /> without a loadingLabel is a compile error in the consuming app.

tsx
/** `loadingLabel`: Required when `loading`. */
type ButtonLoadingLabelWhen =
  | { loading: true; loadingLabel: NonNullable<ButtonBaseProps['loadingLabel']> }
  | { loading?: Exclude<ButtonBaseProps['loading'], true | undefined> }

export type ButtonProps = ButtonBaseProps & ButtonLoadingLabelWhen
Where versions come from
A release diffs this contract against the previous release's. A new optional prop is a minor version; a removed prop, a narrowed type or a new required prop is a major. You never type a version number, and a breaking change cannot ship by accident.

#Try it

Make label required and remove its default. The Playground shows the contract change; a release would call it a major.

Words in this lesson

  • The component’s public inputs.
  • What a prop, a demo variable or a part value holds.
  • A shared type, declared once and used by name in any prop.
  • The prop must be given.
  • The events the component raises.

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 }

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 = alias by ColorScheme { default -> violet.700; Dark -> violet.600 }
    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
  }
}

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

  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"
  }

  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
    }
  }

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