Lesson 7 of 14
Composition: instances and groups
A Badge, then a Card built from Button and Badge, and the library filed into groups.
#The idea
Components are built from components. A part can be an instance of another one, with its props fixed or passed through, and the library is filed into groups so that fifty components read like a system rather than a list.
#What this lesson adds
Four groups, a Badge, and a Card that places a Badge and two Buttons:
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."
part Header.Status: instance { instance Badge { label = "Beta" tone = bind tone } } part Footer.Secondary: instance { instance Button { label = "Share" variant = "Secondary" } }
#What to notice
- An instance names a component and sets its props. A value is fixed (
label = "Beta") or bound to one of the outer component's props (tone = bind tone), which is how a Card's tone reaches its Badge. - A component without an archetype is fine. The Card has none, so every part is the author's and the component brings no behaviour of its own. It is a surface; the behaviour lives in what it places.
- Groups nest by path.
group Forms/Selectionwould be a child of Forms; the ancestors are implied. A component joins a group withgroup Actionsinside its body, and the group's prose is what the docs show at the top of the section. - No component may contain itself, directly or through others. The reader refuses the cycle.
#What the generator makes of it
export function Card({ tone, children }: CardProps) {
return (
<div className={styles.card}>
<div className={styles.header}>
<span className={styles.title}>{"Project Aurora"}</span>
<Badge label={"Beta"} tone={tone} />
</div>
<span className={styles.body}>{children}</span>
<div className={styles.footer}>
<Button label={"Open"} />
<Button label={"Share"} variant={"secondary"} />
</div>
</div>
)
}The Card imports the Badge and the Button and passes exactly the props the file set. The variant arrives as the generated API spells it, lowercase, because the target owns the platform casing.
#Try it
Add a part Header.Close: instance placing a Button with variant = "Ghost". Then try placing a Card inside the Card and read the diagnostic.
Words in this lesson
- The part is another component of the library, nested, with its props set.
- Inside a component: the library category the component sits in, by group path.
- At the top level: a category of the component library.
Open a word for its entry, or read them all in the reference.
#The file at the end of this lesson
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 = 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 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 } } 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 size.md weight weight.medium } } 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 } fill surface ink text border line hairline corner radius.lg inset space.xl } part Header: container { layout { stack row; gap space.sm; align center } } part Header.Title: text { content "Project Aurora" text size.lg weight weight.bold } part Header.Status: instance { instance Badge { label = "Beta" tone = bind tone } } part Body: text { slot of content default ink text-muted text size.md } part Footer: container { layout { stack row; gap space.sm } } part Footer.Primary: instance { instance Button { label = "Open" } } part Footer.Secondary: instance { instance Button { label = "Share" variant = "Secondary" } } }