Lesson 9 of 14
Layout
Stacks, gaps, alignment and per-axis sizing, in the propose/choose model.
#The idea
Layout in Milda is the propose/choose model: a container proposes room, each child says how much it wants, the container places them. That is what SwiftUI, Compose and Flutter already do; the web generator translates it to flexbox. You write intents, never mechanics.
#What this lesson adds
Sizing and distribution on the Card:
part Card: container { layout { stack column; gap space.md; size fill fit } } part Header: container { layout { stack row; gap space.sm; align center; distribute between } } part Footer: container { layout { stack row; gap space.sm; distribute end } }
#What to notice
- is the universal container. , or (children on top of one another). A part with no is not a layout container at all.
wrap wrapstarts new lines when out of room. - Two axes, two words. places children along the stack's axis (start, center, end, between, around, evenly); places them across it (start, center, end, stretch, baseline).
- A child sizes itself per axis.
size fill fitis width then height: take all the room offered across, size to content down.fixed(size.lg)is a token's length;weight(2)a share of the leftover against siblings;ratio(16:9)makes one axis follow the other; and any intent may be clamped withmin(…)andmax(…). The TextField's control already saidsize fillin lesson 8. - A part can leave the flow.
position overlayfloats it over its stack, placed by the stack's align and distribute;position pinned block-startkeeps it at that edge while the container scrolls;position bleed inlinekeeps it in flow and cancels the parent's padding on those edges, which is how a cover image reaches the edge of a padded card. Edges are logical, so the layout survives a right-to-left locale. - and are tokens, like every other length. Padding is a facet on the part; gap is the space between its children.
One layout vocabulary: stack, wrap, gap, align, distribute, inset, per-axis size with weight, fixed, ratio, min and max, and position flow, overlay, pinned and bleed on logical edges, read by the surface, held by the IR and drawn by the web and Figma targets. A grid is reserved and reported, not drawn.proposal 0056
#What the generator makes of it
display: flex, flex-direction, justify-content, align-items, gap; a main-axis becomes flex: 1 1 0% with the min-width: 0 the web needs and natives do not; a bleed becomes a logical negative margin. The web is the target that needs translating, because the model is already the native one.
#Try it
Change the Footer to distribute between, then give the Header stack column and watch the Badge move under the title. Then clamp the Card with size fill max(size.lg) fit.
Words in this lesson
- How the part lays out its children and how it sizes itself, in the propose/choose model.
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; size fill fit } fill surface ink text border line hairline corner radius.lg inset space.xl } part Header: container { layout { stack row; gap space.sm; align center; distribute between } } 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; 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 } }