github marigold-ui/marigold @marigold/system@18.2.0

latest releases: @marigold/cli@2.0.1, @marigold/theme-rui@6.2.0, @marigold/icons@2.0.2...
4 hours ago

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: Checkbox and Radio anchor their control to the first line of the label with items-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 is text-sm leading-4 (16px). A default Badge is 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-center fixes the badge and breaks wrapping labels: the control floats to the middle of the block, 32px off a five-line label. So the fix keeps items-start and stops tall decorations from inflating the line.

    Badge gains size="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, Radio and Switch gain a badge slot. 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 to inline automatically via context. An explicit size on 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.

    Switch carried the mirror of the same bug and is now consistent with the other two. It used items-center, so a wrapping variant="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 shared Label, whose leading-none gives a 14px line against a 16px track. It now uses its own label slot with a 16px line box, matching Checkbox and Radio. 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-center to items-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: children is arbitrary, and consumer layouts (e.g. <Inline alignX="between">) rely on filling a block-level label the way they fill any other block container.

    WithBadge and LongMultilineLabel stories 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 a Record with required keys, so a theme outside this repo that defines Switch without the new label slot now fails tsc. Badge's size="inline" and the badge prop on Checkbox/Radio/Switch are 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 and selectionMode was hardcoded to "none". It now takes selectionMode="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 no FieldBase wiring, no hidden input, and no name, form or validate, 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 two GridList wrappers from being redundant, now that both render a nearly identical stack of rows.

    onSelectionChange receives React Aria's raw Selection ('all' | Set<Key>), deliberately not <SelectList>'s mode-typed onChange, because 'all' is meaningful for a view and unsubmittable for a field. disallowEmptySelection passes React Aria's default through in both modes, unlike <SelectList>, which defaults it to true in single mode for radio-group semantics. A view's selection has to be abandonable, and true would also disable Escape-to-clear.

    selectionBehavior is fixed to "toggle" and stays unexposed, matching <Table>. There is no equivalent of React Spectrum's selectionStyle="highlight", so no press replaces the whole selection. Range selection is unaffected: React Aria checks Shift before it consults selectionBehavior, so Shift+click and Shift+↑/↓ extend a range in "multiple" mode.

    Row layout

    The row's named-area grid gains a leading indicator region, so the template goes from 'label actions' 'description actions' to 'indicator label actions' 'indicator description actions'. The ListView slot union in @marigold/system gains indicator to match. Spacing rides on the cell (me-3) rather than a column gap, mirroring how actions carries ms-3, so with nothing in the area the auto track 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

    onAction still works alongside selectionMode, 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 carries sticky bottom-(--actionbar-offset) in the component, so rendered inside the list's scroll container it pins itself to the bottom. Drive it with the exported useActionBar hook, which holds the selection, fills in the count and clear button, and measures the bar so you can reserve its height in padding-bottom and scroll-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.tsx moves to utils/GridSelectionIndicator.tsx and is shared by both wrappers. Nothing here was publicly exported, so this is internal naming only, chosen because three separate things share the bare name SelectionIndicator: ListBox's, this one, and React Aria's own.

    Documentation

    /components/collection/listview gains 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. completedKeys is 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. selectableKeys replaces the built-in "completed, errored, or current" rule when a backend decides what is reachable, and disabledKeys always wins over both. Errored steps stay clickable by default, so a user who is told a step failed has a way back to it, unless selectableKeys leaves them out.

    Steps with an href render as real links and route through RouterProvider. 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. hideLabels drops 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 cva configuration to the 1.0.0-beta.12 API

    cva@1.0.0-beta.12 moved defineConfig off the package root onto a cva/config
    subpath, replaced its hooks.onComplete option with a direct cx concatenator,
    and stopped returning the compose helper. @marigold/system still built its
    cva and cn exports from the old shape, so the bump broke every build and test
    job at once: esbuild and Vite both reported that cva provides no export named
    defineConfig, and TypeScript added an implicit any on className, which had
    taken its type from the option that disappeared.

    The new cx option owns the class name grammar rather than just post-processing
    the result, so handing twMerge to it directly would have narrowed what cn
    accepts to tailwind-merge's ClassNameValue. That rejects the object form and
    render props, both of which components pass today. cx is therefore cva's own
    clsx-flavored one with twMerge run over its output, which is what
    hooks.onComplete did before.

    Nothing to change in consuming code. cva and cn keep the same signatures and
    the same tailwind-merge behavior. The removed compose was never re-exported
    from the package barrel.

  • 23a7323: fix(DST-1816): pin cva to 1.0.0-beta.12

    @marigold/system@18.1.0 declared cva as ^1.0.0-beta.1. Because cva is still in beta, that range picks up breaking releases: a fresh install now resolves 1.0.0-beta.12, which no longer exports defineConfig from the package root, and the build fails. The dependency is now pinned to the exact version the package is written against, so a future cva beta can't break an install again.

Don't miss a new marigold release

NewReleases is sending notifications on new releases.