Minor Changes
-
1dfe461: feat(DST-1489): add non-form interactive list component
Adds
<ListView>and<ListView.Item>, a non-form list built on React Aria'sGridList/GridListItemfor rows the user operates in place — dismissing with a<Button>, opening a per-row<ActionMenu>, or following a link — without leaving the page. Unlike<SelectList>/<Select>/<ListBox>,<ListView>has no selection, no hidden input, and never becomes a submitted form value.A row is a named-area grid —
'label actions' 'description actions'— so every region names the cell it wants instead of counting columns and rows. Row content is authored with Marigold's slot-aware components (<TextValue>,<Description>,<Title>), and each of them claims its cell through slot context, so text still lands correctly when it's wrapped in a fragment, behindmemo(), or inside a consumer's own component. A bare string child is wrapped as the row's text value; to emphasise part of a line, nest<Text as="span">inside<TextValue>/<Description>. A row carries one<Description>— it takes inline markup, so several facts go on one line.Trailing controls claim their cell through Marigold's
ButtonContextand inherit the row'sghostcascade, the same mechanism<Panel.Header>and<SelectList.Option>use. A row with more than one control must wrap them in a<ButtonGroup>: each control reads the same context and would otherwise be placed in the same cell. A dev-only warning catches that at authoring time. The trailing cell takes Button-family controls only; trailing badges, pills or timestamps have no slot yet.<ListView.Item>forces any nested<Title>to render as a<span>instead of a real heading, so a list of rows never emits one document heading per row.The list is flat (divider lines only) and draws no surface of its own, the way
<Table>does — a framed list nested in a container that already draws a surface is a ring inside a ring. A standalone framed list is<Card><ListView /></Card>.This closes the gap between
<List>(presentational, no roles),<ListBox>(selection only — forbids focusable controls inside a row),<SelectList>/<Select>(form fields),<Table>(tabular), and<Menu>(commands that close on activation). The motivating consumer is the Popover notifications panel (DST-1485), and the docs cover a second must-support scenario: a resource list with a per-row action menu.v1 is intentionally minimal: no selection, no bulk-action bar, no async loading, no drag-and-drop, and no leading media. Leading icons and images need slot-aware icon/image components so placement doesn't depend on authoring order, which is tracked separately along with the rest of the follow-ups (selection ships together with a bulk-action bar, building on DST-1487).
Documentation
New
/components/collection/listviewdocs page: anatomy, appearance, a "which list?" decision guide, the must-support demos, accessibility notes (including the<Title>-as-span caveat), and cross-links to<List>,<ListBox>,<SelectList>,<Table>, and<Menu>. -
1dfe461: feat(DST-1489):
ListViewis flat, and aligns likeTableandAccordioninside a bled container.What changed:
- The list ships one variant.
defaultis the flat list (divider lines only) and there is no framed variant — the oldvariant="plain"is gone, since it is whatdefaultnow does. - Row padding is sourced from
--bleed-px(via--listview-item-px) on the base rather than per variant. That is the custom property a bledPanel.Content/Panel.CollapsibleContent/Drawer.Contentpublishes; it falls back to the standalone row padding (--spacing-stretch-regular-x) when unset.
Why:
A framed list was what a consumer got without asking, and it collides with anything that draws its own surface — a
Card, aPanel, a docs preview — as a ring inside a ring 8px apart. EveryListViewin the repo lives inside such a container, so the frame had no consumer of its own;Tableanswers the same shape by drawing no frame at all and leaving the surface to its container. A standalone framed list is<Card><ListView /></Card>.Binding the bleed to a variant had the same problem from the other side:
<Table>and<Accordion>adopt--bleed-pxunconditionally, so both align inside a bledPanelwith no opt-in, while<ListView>needed two knobs to do the same thing. - The list ships one variant.
-
ce2720e: feat(Table): expandable rows for grouped records
<Table>can now nest rows, so records that belong together (a settlement run
and its individual clearings, an order and its line items) share one table
instead of being faked with a button and a menu. SettreeColumnto theidof
the column that carries the hierarchy and nest<Table.Row>inside
<Table.Row>; use the new<Table.ExpandableRows>when the nested rows come from
data, and mark a rowexpandablewhen they are fetched only once it is opened.
Nested rows stay real table rows, so their values line up under the same headers
and each one carries its own actions.The tree column renders the expand control itself, reusing the caret and morph
animation from<Accordion>. The caret starts where the column's values start:
its hit target is wider than the caret and hangs back into the cell's edge
padding (--tree-caret-inset), so the ghost button's own whitespace is the
gutter rather than something added to it. Behind it every row reserves that
gutter, so a group row and a childless row at the same level start their value at
the same x whether or not there is a control to show. Levels below the root
indent by one gutter (--tree-indent), so a nested row's caret starts where its
parent's value starts, and the column's own header label takes the same gutter,
so it sits above the values it heads. Containment is carried by the children
rather than the parent: nested rows share a filled band across the full row, so
what belongs to a group is legible in the leading drag and selection columns
too, which don't indent. Expansion is collapsed by default and can be controlled
withexpandedKeys/defaultExpandedKeys/onExpandedChange.Drop indicators are level-aware: the insertion line starts at the x of the level
the row would land at, so a reorder inside an expanded group reads as landing in
that group, while a root-level drop still spans the row. Moving a row from one
level to another is not supported.The 32px control keeps that hit target without setting row height: a negative
block margin cancels the surplus over the line box, so a group row is exactly as
tall as a leaf row and thesizevariant's cell padding stays the only thing
that decides — 33 / 41 / 53px forcompact/default/spacious.New theme keys
treeIndentandexpandButton. The band isforeground/8,
which holds ~1.17:1 whether the rows sit onsurface, onbackgroundor inside
amutedtable, and yields to the hover wash so hovering still reads.admin
andmasterrows keep their access colour at every level — who may see a row
outranks where it sits in the tree.Inline editing composes with it:
Table.EditableCellrenders the tree column's
gutter too, so a row keeps its expand control even when that cell is the editable
one. When the edited value lives outside the row items, passdependenciesto
Table.ExpandableRowsas well as toTable.Body. Selection checkboxes, drag
handles and thecompact/default/spaciouspaddings all keep working
alongside the tree column, and the tree column still takes its edge padding from
aPanel's bleed contract.Selection works on grouped rows but does not cascade: each row selects on its
own, and a group row shows no partial state while only some of its children are
selected. Cascading selection is not part of this release.Potentially breaking for opted-in tables: setting
treeColumnchanges the
table's accessibility role fromgridtotreegrid, which is what makes screen
readers announce expanded state and nesting level. Tests asserting
getByRole('grid')on such a table need to becomegetByRole('treegrid').
Tables that don't settreeColumnare unaffected.Collapsed child rows are deliberately not rendered, so searching or deep-linking
to a nested row means expanding its ancestors first. Moving focus to a specific
row is not supported yet, which is tracked in DST-1713. Sorting doesn't reorder
levels for you either, so sort each level in your own data, and there is no
expand all control, so setexpandedKeysto every group's key when you need one.
Patch Changes
-
c7b5c1d: style(DST-1386): give
Checkbox,RadioandSwitcha hover affordance, and fix the indeterminateCheckbox.Hovering an unselected
CheckboxorRadionow darkens its border, and hovering an off-stateSwitchdarkens its track. These were the only interactive controls in the theme without a hover state, so the hit area DST-1295 widened moved silently under the cursor. Selected, indeterminate, disabled and read-only controls are unchanged on hover — the bold accent already carries those states, and a read-only control should not advertise that it can be changed.No new token.
CheckboxandRadiostep the alpha on--color-control-border, the idiomToggleButtonalready uses, and theSwitchsteps--color-control— which has been alpha-based since DST-1672, so the opaque--color-control-hoverthe ticket originally proposed is not needed. All three land on the same perceived step: a ~1.4× contrast multiplier, matchingToggleButton's.On
Checkboxthe hover rule also excludesfocus-visible. Both declarations setborder-color, and the hover one compiles to a higher specificity, so without the exclusion hovering a keyboard-focused checkbox would keep the hover border instead of flipping to--color-ring— leaving the soft halo as the only marker of focus, which is 2.08:1 on its own and below the 3:1 a focus indicator needs.RadioandSwitchneed no such clause: neither flips its border on focus, so there is nothing for hover to outrank.The
Switchtint is abackground-imagerather than abackground-color. The track keeps thetransition-colorsDST-1436 left it for the toggled state, andbackground-imageis not a transitioned property, so hover lands instantly while switching on and off still eases.Separately, an indeterminate
Checkboxnow renders the filled box it was always meant to have. Its three state declarations usedgroup-[indeterminate], whose arbitrary-variant syntax takes a selector — the bare word compiled to a type selector for a nonexistent<indeterminate>element, so the rules never matched and the box rendered white with a resting border. They now use thegroup-indeterminatevariant, which reads thedata-indeterminateReact Aria actually renders. -
17f9158: fix(DST-1675): treat
collapsedas axis-less when resolving inset paddingresolveInsetAxesappended an axis suffix to every non-numeric inset token, includingcollapsed— the one token where an axis split is meaningless, since it means "no spacing" on both axes. So<Panel p="collapsed">resolved tovar(--spacing-collapsed-x)rather thanvar(--spacing-collapsed).That variable went undeclared until it was added to
@marigold/theme-rui, so the declaration was invalid at computed-value time andpaddingfell back to its initial value of0— the right answer for the wrong reason. The cost was that every theme implementing this API had to ship three variables that all mean0(--spacing-collapsed,-x,-y) to make one documented prop value work, with nothing to tell it so.collapsedis now recognised as axis-less and passed through unsuffixed, so a theme declares--spacing-collapsedonly. The same guard is applied to the deliberately-divergent copy of the axis logic inSelectList, which had the identical bug on itspprop. A newisAxislessTokenpredicate is exported from@marigold/systemso the concept has one home instead of being inlined at both call sites.isScaleis unchanged:collapsedstays non-numeric there, becausecreateWidthVar,createHeightVarandcreateSpacingVardepend on that classification.Theme:
--spacing-collapsed-xand--spacing-collapsed-yare removed from@marigold/theme-rui. They shipped intheme-rui6.0.0 stable but existed only to satisfy the suffix that is no longer generated — nothing else referenced them, and the documented token has always beencollapsed. -
1dfe461: fix(DST-1489): an icon
Buttonkeeps its square in any flex container.size="icon"now carriesshrink-0. An icon button is a fixed square, so in a tight flex row it could previously lose its aspect ratio to make room for a neighbour that won't compress — invisible until its hover fill revealed the squashed box.This replaces a
[&>*]:shrink-0that briefly sat onButtonGroup. Putting it on the control that needs it protects icon buttons everywhere (aPanel.Header, aTablecell, anActionBar), not only inside a group, and stopsButtonGrouppinning children it knows nothing about. Text buttons are unaffected either way:ui-button-basesetswhitespace-nowrap, so their min-content floor is their own label. -
eeb0a29: Fix the
Switchcursor so it covers the whole control. Hovering the label text
showed the default arrow instead of the pointer, becausecursor-pointersat
only on the track. Moving it to the container also fixes the disabled and
read-only cursors, which the track's own rule was overriding. -
f2dff15: fix(DST-1730): drop the duplicated focus z-index from
ToggleButton's theme styles.in-[.group]:focus-visible:z-10sat in both the theme'sbuttonslot andToggleButtonitself.
Rendered output is unchanged —cn()was deduping the pair, which the component's inline class
snapshots confirm by still passing untouched. What it removes is the drift: two copies of one
stacking decision, either of which could be changed without the other.Stacking order is a guarantee the component makes, not something a theme should be able to
reorder.pnpm check:theme-zindexnow fails CI on any z-index in a*.styles.ts. -
Updated dependencies [1dfe461]
-
Updated dependencies [17f9158]
-
Updated dependencies [148ef97]
-
Updated dependencies [1dfe461]
-
Updated dependencies [1fd3f85]
-
Updated dependencies [ce2720e]
-
Updated dependencies [4a18a38]
-
Updated dependencies [e41e633]
- @marigold/components@18.1.0
- @marigold/system@18.1.0