Minor Changes
-
d7cf7e4: fix(DST-1607): align boolean-field controls to the first line of their label
A
<Badge variant="master">placed inline in a<Checkbox>label left the box sitting 2px above the text it belongs to. The box was not the problem:CheckboxandRadioanchor their control to the first line of the label withitems-start, which is correct: it is what keeps the box on line one of a label that wraps. The problem was the line. The label line istext-sm leading-4(16px). A defaultBadgeis 20px (18px line box plus 1px borders), so the badge inflated the first line and the 16px control, pinned to its top, stopped reaching the line's optical centre.Flipping to
items-centerfixes the badge and breaks wrapping labels: the control floats to the middle of the block, 32px off a five-line label. So the fix keepsitems-startand stops tall decorations from inflating the line.Badgegainssize="inline": 16px tall, sized to sit inside a line of text rather than next to one, with the access icon scaled to match. The default size is unchanged.Checkbox,RadioandSwitchgain abadgeslot. Pass the badge there instead of building it into the label:<Checkbox label="Enable early bird pricing" badge={<Badge variant="master">Master</Badge>} />
The slot takes the height of the label's line (
1lh, so it follows the theme), centres the decoration in it, and sizes a<Badge>passed to it toinlineautomatically via context. An explicitsizeon the badge still wins. A decoration that fits lands dead on the line. One that does not overflows symmetrically instead of pushing the line apart, so the control stays put either way. The guardrail holds even if a consumer passes a default-sized badge. Those classes live in the component, not in a theme file, so a theme cannot reopen the bug.Switchcarried the mirror of the same bug and is now consistent with the other two. It useditems-center, so a wrappingvariant="settings"label dropped the track to the middle of the block instead of the first line, measured 28.5px off. It also rendered its label through the sharedLabel, whoseleading-nonegives a 14px line against a 16px track. It now uses its own label slot with a 16px line box, matchingCheckboxandRadio. The accessible name is unchanged, because it comes from the wrapping<label>'s text either way. Single-line switches keep their exact height and position.Checkbox's and Radio's label rows moved from
items-centertoitems-start, which is identical for a single-line label and correct for a wrapping one.Radio's label stays a plain text block rather than becoming a flex row:childrenis arbitrary, and consumer layouts (e.g.<Inline alignX="between">) rely on filling a block-level label the way they fill any other block container.WithBadgeandLongMultilineLabelstories for all three components pin both cases under Chromatic, each with a test asserting the control is within 0.5px of the first line's centre.Breaking for external themes:
Theme['components']['Switch']is aRecordwith required keys, so a theme outside this repo that definesSwitchwithout the newlabelslot now failstsc.Badge'ssize="inline"and thebadgeprop onCheckbox/Radio/Switchare additive. -
455eca2: feat(DST-1665): add selection to
<ListView><ListView>shipped with selection deliberately omitted: all six selection props were stripped from its public type andselectionModewas hardcoded to"none". It now takesselectionMode="single"or"multiple", defaulting to"none"so a list that does not ask for selection is byte-identical to before.The selection is view state, not a field value. Read it through
onSelectionChange, hold it yourself, and decide when it commits.<ListView>has noFieldBasewiring, no hidden input, and noname,formorvalidate, and it will not grow them: a selection that has to submit and validate with a form is what<SelectList>is for. That split is what keeps twoGridListwrappers from being redundant, now that both render a nearly identical stack of rows.onSelectionChangereceives React Aria's rawSelection('all' | Set<Key>), deliberately not<SelectList>'s mode-typedonChange, because'all'is meaningful for a view and unsubmittable for a field.disallowEmptySelectionpasses React Aria's default through in both modes, unlike<SelectList>, which defaults it totruein single mode for radio-group semantics. A view's selection has to be abandonable, andtruewould also disable Escape-to-clear.selectionBehavioris fixed to"toggle"and stays unexposed, matching<Table>. There is no equivalent of React Spectrum'sselectionStyle="highlight", so no press replaces the whole selection. Range selection is unaffected: React Aria checks Shift before it consultsselectionBehavior, so Shift+click and Shift+↑/↓ extend a range in"multiple"mode.Row layout
The row's named-area grid gains a leading
indicatorregion, so the template goes from'label actions' 'description actions'to'indicator label actions' 'indicator description actions'. TheListViewslot union in@marigold/systemgainsindicatorto match. Spacing rides on the cell (me-3) rather than a column gap, mirroring howactionscarriesms-3, so with nothing in the area theautotrack sizes to 0 and the margin does not exist.The indicator is centred against the whole text stack and pinned to the start of its column. Pinning matters once the column widens, which it does when a row carries an unslotted child.
That is worth knowing when authoring: a child claiming none of the row's three regions, a
<Badge>being the likely case, is auto-placed by the grid and lands in the indicator column, widening it so that row's text no longer lines up with any other row's. Nest it in<TextValue>or<Description>instead. Before selection this misplaced a badge onto its own line. Now it misaligns the list.Selecting and opening a row
onActionstill works alongsideselectionMode, and which gesture a press performs depends on whether anything is selected. With an empty selection a row press, or Enter, opens the item, and the checkbox or Space selects without opening. Once anything is selected a press marks a row instead and nothing opens. Escape clears the selection and opening works again.One rough edge in that second state, measured rather than inferred: Enter does nothing at all. It neither opens nor toggles, so a keyboard user gets no response of any kind, where a mouse click at least marks the row. React Aria's own guidance covers only clicking and taps, so this is undocumented upstream. A story test pins all four keys.
Bulk actions
A multi-select list composes with
<ActionBar>with no new API. The bar carriessticky bottom-(--actionbar-offset)in the component, so rendered inside the list's scroll container it pins itself to the bottom. Drive it with the exporteduseActionBarhook, which holds the selection, fills in the count and clear button, and measures the bar so you can reserve its height inpadding-bottomandscroll-padding-bottom.There is no select-all control. The Bulk Actions pattern puts it in a header checkbox and a list has no header row to hold one, so a flow where users select every visible record still wants a
<Table>. Tracked separately. Cmd/Ctrl+A does still select every row, and React Aria exposes no way to turn it off, so handle the'all'sentinel even in a list that shows no select-all of its own.Internal
SelectList/SelectionIndicator.tsxmoves toutils/GridSelectionIndicator.tsxand is shared by both wrappers. Nothing here was publicly exported, so this is internal naming only, chosen because three separate things share the bare nameSelectionIndicator:ListBox's, this one, and React Aria's own.Documentation
/components/collection/listviewgains a Selection section covering the modes, the view-state rule, the gesture switch, and the bulk-actions composition, with three new demos. The indicator is added to the anatomy. Both component pages lead with one decision test, "does the selection need to submit with the form?", with one exception named: a pick that never submits but needs a visible label, helper text, or a validation message is still a<SelectList>, because<ListView>renders none of those.The Pick pattern moves with it. Its "List or table" section now chooses by surface, and the people pick, a dialog that stages a selection and commits it with its own button, migrates from
<SelectList>to<ListView>. The Abonnement pick stays a<SelectList>, being the labelled-field case. -
0c56a11: feat(DST-1391): add
Stepper, a progress indicator for multi-step tasks.<Stepper>shows where a user stands in a checkout, an onboarding flow, or a multi-page form, replacing the one-off "Step 1 of 4" widgets that several product flows had each built for themselves. It renders a<nav>landmark around an ordered list, announces each step's label, position, and state, and never relies on colour alone to convey which step is which.State is entirely consumer-owned.
completedKeysis a set rather than a high-water mark, so non-contiguous completion coming from a server is expressible, and the component never infers that a step is finished: only your code knows whether validation passed.selectableKeysreplaces the built-in "completed, errored, or current" rule when a backend decides what is reachable, anddisabledKeysalways wins over both. Errored steps stay clickable by default, so a user who is told a step failed has a way back to it, unlessselectableKeysleaves them out.Steps with an
hrefrender as real links and route throughRouterProvider. Steps without one render as buttons. Steps that are not reachable render as plain text rather than as disabled controls, since an unreachable step is not a disabled widget.hideLabelsdrops labels visually for flows with too many steps to label, keeping them for screen readers and adding a visible "Step 3 of 5" counter so sighted users still know how far along they are.
Patch Changes
-
165377c: fix(DST-1789): adapt the
cvaconfiguration to the 1.0.0-beta.12 APIcva@1.0.0-beta.12moveddefineConfigoff the package root onto acva/config
subpath, replaced itshooks.onCompleteoption with a directcxconcatenator,
and stopped returning thecomposehelper.@marigold/systemstill built its
cvaandcnexports from the old shape, so the bump broke every build and test
job at once: esbuild and Vite both reported thatcvaprovides no export named
defineConfig, and TypeScript added an implicitanyonclassName, which had
taken its type from the option that disappeared.The new
cxoption owns the class name grammar rather than just post-processing
the result, so handingtwMergeto it directly would have narrowed whatcn
accepts to tailwind-merge'sClassNameValue. That rejects the object form and
render props, both of which components pass today.cxis therefore cva's own
clsx-flavored one withtwMergerun over its output, which is what
hooks.onCompletedid before.Nothing to change in consuming code.
cvaandcnkeep the same signatures and
the same tailwind-merge behavior. The removedcomposewas never re-exported
from the package barrel. -
23a7323: fix(DST-1816): pin
cvato 1.0.0-beta.12@marigold/system@18.1.0declaredcvaas^1.0.0-beta.1. Becausecvais still in beta, that range picks up breaking releases: a fresh install now resolves1.0.0-beta.12, which no longer exportsdefineConfigfrom the package root, and the build fails. The dependency is now pinned to the exact version the package is written against, so a futurecvabeta can't break an install again.