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, andcontext Group. Each target maps them: a TypeScript union here, a Swift enum there. required when loadingis 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 & ButtonLoadingLabelWhenWhere 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 } }