Releases

v2.1.1

Maintenance release. Three fixes; no API changes.

Fixed

  • A scrolling Tabs row no longer scrolls the page - bringing the selected tab into view now scrolls the row itself, along its own axis. It used to scroll the whole page to the row as well, so a page with a scrolling Tabs in it opened scrolled down to it.

  • A Menu that is open on mount no longer scrolls the page - the focus it puts on its first row (or the checked one) when it opens is set without scrolling, so a page with a menu open by default, or opened from code, opens at its top and not scrolled down to the menu.

  • A Sidebar rail keeps its own width - its layout (inline or stacked) was also read as the page layout grid, which gave the rail width: 100% and inline padding from the first breakpoint up. A stacked rail in a wide window took all the room and left its panel a sliver; it is now stackedWidth wide, and an inline one expandedWidth or collapsedWidth, at every size.

v2.1.0

Tooltip takes a disabled control, a description that holds while it is closed, and a point to be placed at, all asked for by the design studio when it moved off the native title.

Added

  • Tooltip isDescriptionPersistent - with shouldDescribeChild, the child is described the whole time, as by a native title, not only while the tooltip is open: a visually hidden copy of the title is rendered beside the child for aria-describedby to point at. It is also a theme default (theme.components.Tooltip.defaultProps). docs

  • Tooltip anchorPosition - a point in the window (client coordinates, as Popover's) to place the tooltip at, for a hint that depends on where the pointer is. children is optional then, and isOpen opens it; it keeps its gap from the point and flips for want of room. docs

  • Tooltip isInteractive - with false the tooltip takes no pointer events, so a click goes to what is under it, and it closes as the pointer leaves the child: for a text hint that holds nothing to use. The default (true) keeps it hoverable (WCAG 1.4.13). Also a theme default. docs

  • Tooltip hands what it is handed to its child - a parent that clones its child (a Menu with a Tooltip for its trigger) now reaches the button: the ref, id, aria-* and handlers go on to it, the child's own handler and the handed one both run, and a handed description is added to. A child hidden from assistive technology is not named by the title. shouldWrapDisabledChild (default true, also a theme default) leaves out the wrapper of a disabled child that still takes the pointer.

  • TooltipGroup - tooltips inside it open with no enter delay while another is open, or within skipDelayDuration (300 ms) of the last one closing, so a toolbar reads icon to icon; the one still showing closes at once. Tooltips outside a group, touch and controlled tooltips (isOpen) are unchanged. skipDelayDuration in theme.components.Tooltip.defaultProps does the same for every tooltip under a theme, with no wrapper (unset or 0 is off; a TooltipGroup wins over it). docs

  • ContextMenu opens from code - isOpen and anchorPosition (client coordinates, as Popover's) open it at a point whatever was right-clicked, with target optional; it asks to close through onClose, moves as the point does, and gives the focus back to where it was.

  • Menu and ContextMenu slotProps.list - data-*, class and aria-* for the role="menu" list, and for every submenu's; an onKeyDown runs before the list's own. Its role, id and name stay the menu's.

Fixed

  • A press of a Tooltip's child closes it - as a native title goes away, so an open tooltip is no longer left over what the pointer goes on to click. A touch is left to its long press.
  • Tooltip over a disabled child - a disabled control gets no pointer events from React, so the tooltip never opened. It now wraps a disabled child in an inline-grid span and listens and is placed on that; the child keeps its own ref.
  • No native title in Tabs and MenuItem - the close and overflow buttons of Tabs show their labels, and a MenuItem's note, as non-interactive tooltips; the note still describes the row, open or closed, and opens over a disabled row.

v2.0.0

The theme holds the page's layout system - the window size classes as breakpoints, and the layout tokens per class - so an app and the design studio work from the same values. Everything responsive is CSS: pages work with JavaScript off. (#344)

Action menus - a trigger and the list of commands it opens, with its own keyboard, and MenuItem as a menu row (#11); tabs, a row of tabs with content of its own around it, as a tablist or a navigation (#15); and a component catalog for tools, shipped with the package (#21). Inputs - a rating and a number field first (#31). Breadcrumbs, the first of the navigation components (#29).

Added

  • More spacing steps - theme.space gains 0.125 (1px), 0.25 (2px), 0.75 (6px), 1.25 (10px) and 1.75 (14px), and spacing() rounds to whole pixels with a 1px floor for any non-zero step (every default step is exact on the 8px unit). 0.125 and 0.25 are for hairline gaps; lines below 4px are border widths. docs
  • theme.breakpoints - each key a range, from its value until the next: xs 0-599px (compact, the base), sm 600-839 (medium), md 840-1199 (expanded), lg 1200-1599 (large), xl 1600+ (extra-large). Responsive props use them by name (p={{ xs: 2, md: 4 }}), by position (p={[2, 3, 4]}) or as container queries (cqSm). BreakpointOverrides adds, renames or removes breakpoints with type safety. docs
  • Query helpers - theme.breakpoints.up / down / only / between / not and theme.breakpoints.containerQueries, rebuilt from the theme's own sizes by createTheme and ThemeProvider; plain up, down, only, between, not and containerQueries exports for baseTheme, and withBreakpointHelpers for a scale of your own.
  • theme.layout - layout tokens per window class: content (columns 1-16, margin and gutter as theme.space keys, fluid or fixed with maxWidth), panels (start / end, each inline, overlay, collapsed or hidden with width and railWidth) and bars (a 64px top bar on every class). baseTheme uses 4 / 8 / 12 / 12 / 12 columns, the start panel as a 72px rail from sm and inline at xl, the end panel as an overlay at md and lg and inline at xl.
  • layout prop - on any View-based component, Flex included, it is the page frame: the theme's margin and, in fixed mode, its centred max width per class - the element keeps its own display (<View as="article" layout>, <Flex layout gap={3}>). On Grid it adds the column tracks. The grid's own gap / columnGap is its gutter - single or responsive - and the theme's gutter applies where it is not set. layout={{ lg: { columns: 16 } }} replaces the theme's columns from lg up, layout={{ maxWidth: '1280px' }} its max width at every class, and the two mix. A frame spans its parent in any context (a grid item, a flex column or row), capped by its max width in fixed mode, rather than shrinking to its content.
  • DrawerToggle - the button that opens and closes a Drawer through popovertarget.
  • LayoutProvider - a page-level layout on the ./theme subpath: <LayoutProvider layout={{ lg: { content: { maxWidth: '1376px' } } }}> merges the page's values onto theme.layout for everything inside, field by field and mobile-first, and changes nothing else - colours and light/dark switching stay the surrounding theme's. Providers nest.
  • @soroush.tech/design-system/testing - installPopover(), a Popover API stand-in for jsdom unit tests: show, hide, the toggle event, Escape and popovertarget clicks. It returns a function that removes it, and does nothing where the real API exists.
  • span on View - how many columns of a grid an element takes (span={{ xs: 4, md: 6 }}), or span="full" for the whole row whatever the column count; every View-based component takes it.
  • Menu - a trigger and the MenuList it opens. Not modal: nothing is laid over the page and focus isn't trapped. The list is placed position: fixed under the trigger (align start or end, mirrored right to left), flips above it near the bottom of the window, stays marginThreshold inside it and follows the trigger on scroll and resize - so a panel's overflow can't clip it - while staying inside the menu's own box in the DOM. ArrowDown/ArrowUp on the trigger open it; Escape, a chosen row, the trigger again, or focus leaving close it (onClose(event, reason)), focus going back to the trigger unless it left. Uncontrolled (defaultOpen) or controlled (isOpen, onOpen), with onStep(-1 | 1) for a menu bar; emptyLabel for a menu with no rows; elevation and layer. (#13) docs
  • MenuList - a role="menu" list that owns the keyboard: the arrow keys, Home and End move focus between its own rows, skipping disabled ones and wrapping (shouldWrap); typing jumps to a row by its label. shouldAutoFocus focuses the checked row, or the first enabled one. onSelect(value) reports the chosen row; color, textColor, size and dense set every row's defaults. Rows are found by their nearest role="menu", so a nested list keeps its own. (#12) docs
  • MenuItem menu rows - inside a MenuList, a MenuItem is a menuitem, menuitemradio or menuitemcheckbox (role), with selected as aria-checked, a leading startIcon column kept on every row, a trailing shortcut (also aria-keyshortcuts), a note that tells why a disabled row is off, secondaryAction - a control of its own beside it, the pointer's, hidden from assistive technology - and onClick. The pointer moves focus as it goes, so one row is highlighted at a time. Inside a Select it is the same option row as before. docs
  • Submenus - submenu on a MenuItem opens a nested MenuList beside its row, to any depth; null or [] is an empty one, which shows emptyLabel. The row and its submenu sit side by side in a role="none" entry, the row aria-haspopup="menu" with aria-expanded and a chevron that mirrors right to left. Focus decides the submenu: a focused row - by key or by the pointer - has it open, a sibling taking focus closes it. Right, Enter or Space go in; Left or Escape come back (swapped right to left). A choice in a submenu is the outer list's onSelect and closes the whole Menu. Placed beside the row on the side away from the menu's align edge, or the other side when the window has no room - its own submenus then go on that way, never back over the menu; with room on neither side, under the row (or over it), never covering it. (#14) docs
  • ContextMenu - the same list opened by a right-click over its target, at the pointer, or by the context-menu key / Shift+F10 at the focused element; placed and closed as Menu is, with focus going back to where it was. The browser's own menu is held back only when this one opens. (#20) docs
  • Menus from data - MenuItems renders rows from MenuItemData[] (rows, { type: 'divider' }, submenus as nested entries), and Menu and ContextMenu take the same array as items. (#20) docs
  • MenuDivider - a role="separator" line between groups of rows. docs
  • One subpath for the menus - Menu, MenuList, MenuDivider, MenuItems and ContextMenu are one family, imported from @soroush.tech/design-system/Menu, as Table's parts are. It also exports MenuListContext and useMenuListContext - present inside a MenuList, the rows' look and how a row reports its choice - and the MenuItemValue and MenuItemData types.
  • Theme customization - theme.components.Menu (root, paper), .ContextMenu (root, paper - its surface alone, over Menu's), .MenuList and .MenuDivider, and MenuItem's new startIcon, label, shortcut, submenuIcon, entry and secondaryAction slots. defaultProps for Menu (align, marginThreshold, elevation, layer, borderRadius and the rows' look), ContextMenu, MenuList (shouldWrap and the rows' look) and MenuItem - a list's own values win.
  • Tabs, Tab - a row of tabs. A tablist by default: one tab in the page's tab order, the arrow keys of its orientation moving between tabs (skipping disabled ones, coming round, swapped right to left), Home/End, Enter or Space selecting - or moving focus selecting, with shouldSelectOnFocus. as="nav" gives the same look as a navigation of buttons or links, marked aria-current="page". value / defaultValue / onChange, null for none; a tab without a value takes its position. variant underline (a sliding indicator), boxed (outlined on a raised ground), enclosed (outlined on its top and sides, open onto the divider) or tonal (the tabs in a rounded container, the selected one a tinted pill) - an augmentable TabsVariants; color, textColor, size, fullWidth, isCentered; hasDivider, a line the row's whole length - round the selected tab when enclosed, which without it is outlined all round. startContent / endContent hold the row's own components outside the arrow keys, a space from the tabs. A Tab takes label, an icon placed by iconPosition (top by default) at the size it is given - every label in a row on one line - isWrapped, disabled, href and as. action.updateIndicator() re-measures. (#16) docs · Tab
  • Closable tabs - a Tab with a closeLabel has a ✕ of its own beside it, out of the tab order, shown on the selected tab and under the pointer and keeping its room on the others; it, a middle click and Delete fire Tabs' onTabClose(value). The ✕ is the pointer's - a tablist owns tabs alone, so it is hidden from assistive technology - and the tab announces Delete (aria-keyshortcuts). secondaryAction puts a control of the tab's own after its label. The selected outline and raised ground are drawn around the whole tab, its controls included. (#17)
  • Scrolling tabs - overflowBehavior="scroll" scrolls the list along its orientation when its tabs don't fit, with scrollButtons auto (each shown while tabs are out of view its way), always (disabled at an end) or never; hidden on narrow screens unless shouldShowScrollButtonsOnMobile, the scrollbar hidden unless hasVisibleScrollbar. A button scrolls a view's worth (smoothly, unless reduced motion is preferred), the selected tab is scrolled fully into view, and right to left everything is mirrored. action.updateOverflow(). The buttons are TabScrollButtons - exported, and replaced with slots={{ scrollButton }}. (#19) docs
  • Tabs that don't fit go to a menu - overflowBehavior="menu" draws the tabs that fit whole, in order - the selected one, or the first enabled one, always - and lists the rest in a Menu behind a button at the list's end, named by overflowLabel. Nothing scrolls and no tab is cut by the edge. Choosing a row selects its tab, which brings it into the row; a closable tab's row has its ✕ beside it, and Delete on the row closes it too. Measured after every render and whenever the list resizes, down a column too; prerendered, every tab is drawn. action.updateOverflow() measures again. (#18) docs
  • TabContext, TabList, TabPanel - a TabContext holds the value (value / defaultValue / onChange) for a TabList - the Tabs that shows it, each tab linked to its panel - and the TabPanels beside it, wherever they sit; a TabPanel is a role="tabpanel" named by its tab, shown while it is selected (shouldKeepMounted keeps hidden content mounted, bg its ground). TabContext · TabList · TabPanel
  • One subpath for the tabs - Tabs, Tab, TabContext, TabList, TabPanel and TabScrollButton are imported from @soroush.tech/design-system/Tabs.
  • Grounds - bg colors the tab strip and selectedBg the selected tab (paper by default in the boxed look, and in the enclosed look without a divider - which draws no border then); give the TabPanel the same bg and tab and panel read as one. A Tab takes its own bg while unselected, and borderRadius - only the corners away from the divider when enclosed; a TabPanel takes borderRadius too.
  • Theme customization - theme.components.Tabs (root, list, indicator, startContent, endContent, scrollButton, overflowButton, overflowClose, with defaultProps for variant / color / size / hasDivider / selectedBg), .Tab (root, label, icon, secondaryAction, close, with defaultProps for isWrapped / bg / borderRadius / color / textColor / size / selectedBg), .TabScrollButton (defaultProps for color / textColor) and .TabPanel (defaultProps for shouldKeepMounted).
  • A tab's own look - like a table cell, a Tab takes its row's color, textColor, size and selectedBg, and its own value wins: the tab's prop, then the row's, then theme.components.Tab.defaultProps, then the row's default. The underline indicator, the enclosed divider under every other tab and the overflow button take the selected tab's color. A TabScrollButton takes color (its stretch of the divider) and textColor (its chevron) the same way. A tab's size sets its type and its sides; across a row every tab takes the row's tallest top and bottom padding, so a smaller tab keeps its label on the others' line.
  • Breadcrumbs - the trail of links from a site's root to the current page: a nav landmark named by aria-label, its crumbs - the app's Links and Typography - an ordered list with an aria-hidden separator between two (/ by default; an icon separator mirrors right to left). Past maxItems (8) only the first itemsBeforeCollapse and last itemsAfterCollapse crumbs show, with a button named by expandLabel that shows the whole trail and moves focus to its first link; slots.collapsedIcon and slotProps.collapsedIcon replace its icon. The root is a Typography: color (secondary by default), typography - its variant - align, as. theme.components.Breadcrumbs styles root, list, item, separator, collapsed and collapsedIcon, with defaultProps for separator, maxItems, itemsBeforeCollapse, itemsAfterCollapse, expandLabel - a trail's language - and color. Icon gains more_horiz. (#30) docs
  • Rating - a row of icons the user sets as a rating, or a read-only display of one. A group of visually hidden native radios, so the keys (Tab to the rating, the arrows to change it), assistive technology and forms work as the platform's. value / defaultValue / onChange (null when cleared), max, precision (0.5 for halves - the rating is rounded to it), highlightSelectedOnly, icon / emptyIcon, size, color, readOnly (one role="img"), disabled, required, error, name. Hovering previews the value (onChangeActive), pressing the chosen item clears it. Every label is the app's: getLabelText names each item and emptyLabelText the "no rating" radio. In a FormControl it takes the field's size, color, error, disabled and required state, and its FormLabel names the group. theme.components.Rating styles root, item, icon, decimal, input, text and emptyLabel, with defaultProps for max, precision, highlightSelectedOnly, size and color. Icon gains star and star_border; FormLabel registers with its FormControl (labelId from useFormControl), the way FormHelperText does. (#32) docs
  • NumberInput - a field for a number. A text input - so what is typed is the user's until it is settled - that reads and writes the number in its locale (locale, format - an Intl.NumberFormat, a percent read as its fraction), clamps it to min/max, and steps by the arrow keys (step; Alt smallStep; Shift largeStep), Home/End, the wheel (shouldScrubOnWheel) and buttons that repeat while held (spinButtons: stacked, sides or none). value / defaultValue / onChange (as typed) / onChangeCommitted (on blur, Enter or a step), shouldSnapOnStep, variant, size, color, error, disabled, readOnly, required, fullWidth, name (a hidden input carries the raw number to a form). A role="spinbutton"; the buttons' names (incrementLabel, decrementLabel) are the app's. In a FormControl it takes the field's size, color, error, disabled and required state, and its FormLabel targets the field. theme.components.NumberInput styles root, input, spinButtons, increment and decrement, with defaultProps for the frame, the steps and the two labels. Icon gains add and remove. (#33) docs
  • RangeInput - a slider: one value on a range, or a thumb for each value of an array. Each thumb is a visually hidden native <input type="range">, so the arrows, Page, Home and End, assistive technology and forms work as the platform's; the pointer drags a thumb, touch included. value / defaultValue / onChange(event, value, activeThumb) / onChangeCommitted, min, max, step (null limits the value to marks), shiftStep, marks, orientation, track ('normal', 'inverted' or false), scale, valueLabel and formatValueLabel, shouldSwapThumbs, size, color, error, disabled, name. Every name is the app's: getAriaLabel for each thumb and getAriaValueText for its value. In a FormControl it takes the field's size, color, error and disabled state, and its FormLabel targets and names the thumbs. theme.components.RangeInput styles root, rail, track, thumb, mark, markLabel, valueLabel and input. (#34) docs
  • TransferList - two lists, and the buttons that move the checked items from one to the other. Data-driven: items ({ value, label, disabled? }), value / defaultValue (the values on the right) and onChange. Each list is a role="list" of rows whose checkbox is the row - one tab stop, named by its label; move right and left act on the checked items and are off while none is checked, shouldShowMoveAll adds the buttons that move every enabled item, and hasSelectAll adds a header checkbox - all, some or none - with the count (getCountLabel). A disabled item stays where it is. Every name is the app's: leftLabel, rightLabel and the four buttons' labels. In a FormControl it takes the field's size, color, error and disabled state, and its FormLabel names the group. theme.components.TransferList styles root, list, header, title, count, items, item, controls and icon, with defaultProps for the buttons' labels, the move-all and select-all switches, size and color. (#35) docs
  • Collapse, Fade, Grow, Slide and Zoom - show and hide content by moving it. Each takes isShown, shouldAppear, shouldMountOnEnter, shouldUnmountOnExit, timeout (a number or { enter, exit }), easing and the lifecycle callbacks (onEnter ... onExited, each told the node), over one shared useTransition. Fade, Grow, Slide (with direction, mirrored right to left, measured from where the child sits) and Zoom wrap one child that holds a ref and takes a style; out, it is hidden. Collapse opens by the height - or, orientation="horizontal", the width - of its content, measured as it moves and left to the content once open, with a collapsedSize. Time and easing come from the new theme.transitions (duration: short, standard, long; easing: standard, enter, exit); a user who asked for less motion sees it all change at once, and the callbacks still fire. theme.components.Collapse styles root, wrapper and inner, with defaultProps for its orientation, collapsed size, time, easing and shouldAppear. (#41) docs
  • Accordion - a heading that opens and closes a region: AccordionSummary (a button in a heading - headingLevel, expandIcon), AccordionDetails, AccordionActions and AccordionGroup. The summary carries aria-expanded and aria-controls; the rest is a role="region" named by it, opened and closed by Collapse or the transition given. Open or closed it owns, until isExpanded takes it over (onChange(event, isExpanded)); defaultExpanded, disabled and hasGutters. In an AccordionGroup the arrows, Home and End move between summaries (one that is off is skipped), isExclusive keeps one open at a time and variant="attached" joins them into one block with no gap. theme.components styles Accordion (root, region), AccordionSummary (root, button, content, icon) and the root of the rest, with defaultProps for hasGutters, headingLevel, isExclusive and variant. (#42) docs
  • Chip - a compact tag: label (truncated when long), variant (contained, outlined or text), color, size, shape (square, rounded or pill, the default) with a borderRadius for the rounded shape, an icon or an avatar. onClick makes it a button - one tab stop; onDelete shows a delete control, named by deleteLabel, that is a button of its own and out of the tab order, and deletes on Backspace and Delete on the chip too (a chip that only deletes takes the focus for them). A filled chip is the theme's contrastText on the color's main. theme.components.Chip styles root, body, label, icon, avatar and deleteIcon, with defaultProps for variant, color, size, shape, borderRadius and deleteLabel. (#43) docs
  • Badge - a count or a dot over another element: badgeContent, max (a count past it shows max+), variant (standard or dot), color, isHidden, shouldShowZero (a count of zero shows no badge otherwise), anchorOrigin (the corner; end is the left right to left) and overlap (rectangular or circular). The badge is aria-hidden; badgeLabel is the text a screen reader hears, read with the child and, over one element, added to its aria-describedby. theme.components.Badge styles root, badge and label, with defaultProps for max, variant, color, anchorOrigin and overlap. (#44) docs
  • Alert (with AlertTitle) - an inline message with a severity (success, info, warning or error - picks the icon and the palette color), a variant (standard tinted, contained or outlined), an icon (false draws none), an action and, given onClose, a close control named by closeLabel. role is alert (announced at once) or status (politely). Tinted and outlined, its text is the theme's text.initial, checked against AA for each severity; filled, it is the severity's contrastText. Icon gains check_circle, error and info. theme.components.Alert styles root, icon, message, action and closeButton; AlertTitle its root; with defaultProps for severity, variant, closeLabel and role. (#45) docs
  • Fab - a floating action button: the one main action of a page, circular (its icon) or extended (a pill with a label and a startIcon). It is a Button - its focus ring, press state and disabled behavior - raised on a theme shadow, as tall as an avatar of its size, in the color given. Where it floats is the page's: no position is built in. A circular fab is named by aria-label or aria-labelledby. A fab keeps a touch target of at least theme.space[6] square, moves its shadow with theme.transitions (not at all under reduced motion) and is no higher pressed than at rest; margin and padding are theme.space keys that win over its own, and an extended one has space[2] before its icon, space[3] after its label, space[1.5] between, and a one-line label cut with an ellipsis. isCollapsed draws an extended fab as a circle - its label moving out over theme.transitions - and back, the label staying in the tree. theme.components.Fab styles root, label and labelText, with defaultProps for variant, color and size. (#46) docs
  • Snackbar - a transient message in a corner of the window, through a Portal: isOpen / defaultOpen, message and action (or children, such as an Alert), anchorOrigin (start and end swap right to left), a transition (Grow by default) and a layer - the new snackbar key of theme.zOrder, beside the new tooltip one. It hides itself after autoHideDuration; the clock stops while the pointer or the focus is on it and starts again for resumeHideDuration. onClose(event, reason) is told timeout, escapeKeyDown or clickaway; controlled, the owner closes it, uncontrolled it closes itself - but not on a click away. A role="status" that never takes the focus. theme.components.Snackbar styles root, content, message and action, with defaultProps for anchorOrigin, the two times and layer. (#47) docs
  • FabMenu, FabMenuItem - a Fab that opens a stack of actions from the corner of a page. The trigger is a circular or - with a label - extended fab, a disclosure button (aria-expanded, aria-controls) whose icon turns to a closeIcon while open; the actions are FabMenuItems, extended pills (a secondary Fab of the trigger's size, an icon and a label) that zoom in over theme.transitions, the one next to the trigger first, and are out of the tree while closed. isOpen / defaultOpen with onOpen and onClose, direction (up, down, left or right - a column or a row, the sides the page's start and end), align (start or end, for a column), color and size. Escape, a press away or an action chosen closes it, the focus returning to the trigger for the first and the last; the arrows of the axis it goes along (ArrowUp and ArrowDown, or ArrowLeft and ArrowRight) open a closed trigger and walk what is drawn, Home and End go to its ends. The actions are measured against the window as it opens and as the window or the page moves: a row wider than the room beside the trigger becomes a column (above it where there is more room, else below), a column is no wider than its room - a long label is cut with an ellipsis - and no taller, scrolling instead. theme.components.FabMenu styles root and list, with defaultProps for direction, align, color and size. A Fab is also the trigger of a Menu. docs
  • Tooltip - a short description of one child, shown on hover, keyboard focus and a long touch: a role="tooltip" in a Portal, placed beside its child (placement: a side - top, bottom, left, right, or auto for the side with the most room - and -start or -end; hasArrow points at the child's middle) and flipped for want of room, by the useAnchoredPlacement Menu shares. enterDelay, leaveDelay and the touch delays; it stays open while the pointer is on the tooltip, closes on Escape and never takes the focus. A text title names the child (aria-label); shouldDescribeChild describes it (aria-describedby) instead. isOpen / defaultOpen with onOpen and onClose, shouldOpenOnHover, shouldOpenOnFocus, shouldOpenOnTouch, layer, and slots and slotProps for its surface and arrow. theme.components.Tooltip styles root and arrow, with defaultProps for each prop but the title and isOpen. (#49) docs
  • Autocomplete - a field whose text finds an option in a list: a role="combobox" input with a role="listbox" in a Portal below it, placed by useAnchoredPlacement and kept as wide as the field, with aria-activedescendant on the highlighted option. One choice, or several with multiple as Chips (Backspace in the empty field removes one); shouldAllowFreeText takes a typed text as the value. options of any type with getOptionLabel, isOptionEqualToValue, filterOptions, groupBy and renderOption; value / defaultValue with onChange and inputValue with onInputChange, each with its reason; isLoading and loadingText, noOptionsText, clearLabel, openLabel, closeLabel - no built-in English. Arrow keys wrap, Home and End walk the list while it is walked, Enter chooses, Escape closes. In a FormControl it takes its id, size, color, error, disabled and helper text. theme.components.Autocomplete styles twelve slots, with defaultProps for the frame, the labels, shouldAllowFreeText and layer. (#50) docs
  • Surfaces take the theme's corners and colors - Snackbar also takes a shape (square, rounded, pill), with borderRadius setting a rounded one. Accordion, Alert, Snackbar, Tooltip and the Autocomplete list are square by default, as Paper is, and round with the theme's defaults.surfaceRadius or a borderRadius of their own. Accordion, Snackbar, Tooltip and the Autocomplete list take a bg, and Accordion, Snackbar, Tooltip and Autocomplete a textColor; Autocomplete takes a borderRadius on its field too. Each is a defaultProps key.
  • ScrollArea - a scrolling area whose scrollbars are drawn over its content rather than beside it, so a rounded panel keeps its corner clean, and show only while the pointer is over it or focus is inside. Scrolling stays native (wheel, keys, touch); each axis that overflows (axes: y, x or both) gets a track whose thumb drags and whose track pages. A maxHeight caps one that grows with its content; tabIndex={0} with an aria-label makes a scroller with nothing focusable keyboard scrollable. New theme tokens background.scrollbar and background.scrollbarTrack (required in a full theme; createTheme inherits them), the track and thumb sized from theme.space and the fade from theme.transitions. theme.components.ScrollArea styles root, viewport, track and thumb, with defaultProps for axes. docs
  • Menu and ContextMenu take a borderRadius - a key of theme.radii, by default the theme's defaults.surfaceRadius (square), so a menu is as round as the other surfaces; a defaultProps key.
  • For the studio's swaps - ScrollArea takes a viewportRef; MenuItem takes an aria-keyshortcuts of its own, which wins over a string shortcut, and shouldAnnounceSecondaryAction; Tabs takes shouldAnnounceClose, which names each closable tab's close button (and the overflow menu's) by its closeLabel for assistive technology - a sibling of the tab, out of the tab order; NumberInput takes startContent and endContent inside its frame (a new content slot) and reads 12px and 12 px as 12.
  • Inputs share a least width and one focus ring - TextInput, NativeSelect, Select, NumberInput and Autocomplete are at least theme.defaults.inputMinWidth wide (default 12rem) unless they fill their row, and a minWidth of the instance wins. Their keyboard focus ring is the border's color: the error color while in error, else the field's color - TextInput and Select used the theme's default color. A FormControl is a column - label, control, helper text - whose children keep their own width unless it is fullWidth; a FormLabel is a block above its control with space[0.5] under it and a FormHelperText the same above it. The arrow of Select and NativeSelect, and the chevrons of Autocomplete, are the field's icon size (theme.icon[size]) in the field's color, so they follow color and error; the stacked arrows of NumberInput are that size too and share the field's height. A Checkbox, Radio, Switch and TransferList take a textColor - from the field or their own - for their label text (a new label slot on the three), the mark keeping its color. A TextInput is one character wide, as every input is, so it is as wide as the others, not twenty characters.
  • Inputs follow their form - TextInput, NumberInput and NativeSelect set their value in their accent color when they have no textColor, as Select does, so a Form's color changes the text too. Switch takes its size, color and disabled state from its Form, and from its FormControl those too, with its id, required and helper text; its neutral label is text.secondary. Select is named by its FormLabel, and a FormLabel with an id of its own is the one a control points at.
  • @soroush.tech/design-system/catalog - every component as data, for tools that place and write components (a design tool's palette, a page's source writer): its category in the docs, the subpath it is imported from, what a freshly placed one is and its named looks (as { component, props, children } nodes), the props a picker offers with their theme scale or values and default, how its node props are written, its two-way prop and event, and the DOM events worth offering; plus styleScales, each styled-system prop's theme scale. Pure data: it loads in plain Node with no DOM, React or Emotion, and survives JSON unchanged. Each component's entry lives beside it in ComponentName.catalog.ts, typed against its props, and its stories read their args from it. docs
  • Nine icons - home, search, add_circle, inventory_2, palette, article, menu_book, light_mode and dark_mode, for a navigation rail and a theme toggle.
  • Sidebar as a navigation rail - layout="stacked" puts each label under its icon, always shown, in a rail of stackedWidth (5.5rem) that does not animate; the selected fill and the hover tint are on a pill behind the icon (a new pill slot of SidebarItem). isOpen is now optional, and ignored by a stacked rail. The default layout is the collapsible rail, unchanged. docs
  • A Sidebar panel rendered in place - panel is the second column's content, in the server-rendered markup, where hasPanel ports the selected item's children in on the client. SidebarItem documents its use as a link: href, with aria-current in place of aria-pressed. docs
  • NavTree, NavGroup and NavLink - a navigation tree for a site's pages: groups that may fold (a native details, so they work without script), and links with a note, an external arrow, and aria-current="page" on the current page. Put it in a Sidebar panel or a nav. docs

Changed

  • MenuItem's value is optional outside a Select (a menu row needs none).
  • Icon's size takes a theme icon size by name - sm, md, lg - as well as a length. size="sm" used to reach the svg as an unknown width, so the icon collapsed to nothing.
  • Button and Pressable keep a new tab at arm's length - rendered as an anchor with target="_blank" and no rel, they carry rel="noopener noreferrer", as Link has.
  • React 19.0 and up - the peer range is ^19.0.0; it had narrowed to ^19.2.8 through a dependency update, with nothing in the package needing more than 19.0.

Fixed

  • <Select native> keeps its accessible name. The native path dropped aria-label and labelId, so a native select had no name; it now passes them to the <select>, as the custom listbox does on its trigger.

BREAKING CHANGES

  • Responsive props switch at the window classes. Without theme.breakpoints, array props used styled-system's defaults (40em / 52em / 64em). A theme built from baseTheme now has xs 0, sm 600, md 840, lg 1200 and xl 1600px: display={['none', 'none', 'block']} shows from 840px instead of 832px, and a four-value array reaches lg. Migrate: move each call site to named keys ({ xs: 'none', lg: 'block' }) at the class you mean.
  • _ is no longer a responsive key - use xs, the base class.
  • Generated queries are range syntax (@media (width >= 840px)), through @soroush.tech/styled-system.
  • Drawer is an HTML popover. It opens from a DrawerToggle and closes on Escape or a click outside without JavaScript, and its content is always in the page. It needs an id; isOpen becomes optional (controlled use); onClose's reasons are 'escapeKey' and 'dismiss'; shouldUsePortal, portalContainer, shouldKeepMounted and shouldEnforceFocus are gone - the browser's top layer replaces the portal. Migrate: <Drawer id="nav"> with a <DrawerToggle drawer="nav">, or keep isOpen / onClose and add an id - popovertarget names its drawer by id, which is what works without JavaScript. In a browser without the Popover API (Safari before 17, Firefox before 125) the drawer stays closed instead of throwing. Unit tests in jsdom, which has no Popover API, install installPopover() from the new @soroush.tech/design-system/testing subpath.
  • Theme requires breakpoints and layout. A theme written out by hand as a full Theme object no longer typechecks. Migrate: build it with createTheme(baseTheme, ...).

Packaging

v1.3.4

Documentation only. No code and no API changes - the published bundle is identical to 1.3.3.

Added

  • Every component now owns its documentation page: <Name>.mdx sits beside the component's README.md and drives that component's page on docs.soroush.tech. The page renders the README's intro followed by every story in <Name>.stories.tsx as a live, editable demo.
  • The page is required - a component without one fails the docs build.
  • The README now explains how to point an AI assistant at this library: the hosted MCP server, the same server locally over stdio, and the Claude Code plugin that wraps it.
  • docs/consumer/ carries the consumer-facing setup guide, the layout-kit patterns, and copyable theme.ts / providers.tsx starters. The MCP server serves these to AI tools; they are repo files, not part of the npm tarball.

v1.3.3

Maintenance release. One rendered string changes; no API changes.

Changed

  • TablePagination's default displayed-rows label now reads 1-10 of 57 with a hyphen, where it used an en dash. labelDisplayedRows is a prop: pass your own to render it any other way.

No other component's rendered output changed.

v1.3.2

Maintenance release - the package gained a Storybook of its own. No public API, behavior, or emitted-output changes.

Packaging

  • storybook no longer delegates to the site's Storybook: the package has its own config in .storybook/, serving only this package's stories against the shipped baseTheme on port 6007 - the component library is now viewable without the site checked out. build:storybook and test:storybook scripts were added alongside it; the latter runs every story's play function and a11y check.
  • New dev dependencies: @storybook/addon-a11y ^10.5.8, @storybook/addon-docs ^10.5.8, @storybook/addon-vitest ^10.5.8.
  • Dev-dependency bumps: @storybook/react-vite and storybook ^10.5.7 → ^10.5.8, @testing-library/jest-dom ^7.0.0 → ^7.0.1.
  • jsdom stays on ^29.1.1, held repo-wide in renovate.json: jsdom 30 absolutises relative lengths in getComputedStyle, so 2rem resolves to 32px and -0.025em to -0.4px, and every toHaveStyle assertion written in rem or em fails.
  • No runtime, peer, or optional-peer dependency changes. The files: ["dist"] allowlist is unchanged, so .storybook/ is not part of the published tarball.

v1.3.1

Restores the style props on the published component types - they resolved to any for consumers on 1.2.0 and 1.3.0 - and refreshes dependencies. (#335)

Fixed

  • Style props are back on every component's published types. TypographyProps, ImageProps, TextInputProps and the rest inherit their space / layout / typography / flexbox / border / background / position / grid prop groups from @soroush.tech/styled-system, and the components used to reach for those types through this package's own barrel, which passes them on with export *. The d.ts bundler flattens that barrel into a declare namespace, which cannot carry an export *, so the names were referenced but never declared in the emitted types. Under the skipLibCheck consumers compile with, those dangling references degraded to any rather than erroring: every style prop disappeared (<Typography mt={2}>, <Image width="100%">), and TextInput's Pick<LayoutProps, 'width' | 'minWidth' | 'maxWidth'> turned those three into required props. Components now import the types straight from @soroush.tech/styled-system, so nothing routes through that namespace. The emitted JavaScript and the runtime API are unchanged - this release moves types only.

Packaging

  • Dev-dependency bumps: @storybook/react-vite ^10.5.6 → ^10.5.7, eslint-plugin-storybook ^10.5.6 → ^10.5.7, storybook ^10.5.6 → ^10.5.7, eslint ^10.8.0 → ^10.8.1, @types/node ^26.1.2 → ^26.2.0.
  • Repo-wide dependency overrides clear the vulnerable transitives the lockfile resolved for this package's dev tree (webpack forced to >=5, eslint-plugin-import to >=2.32.0, plus in-range patches for brace-expansion, dompurify, js-yaml, and postcss).
  • No runtime, peer, or optional-peer dependency changes.

v1.3.0

Code-quality release driven by static analysis (#333 follow-up): no new components, no behavior changes.

Changed

  • TextInput picks its input element (input / textarea / auto-resizing textarea) through a plain statement instead of a nested ternary. Rendered output is identical.

Breaking

  • ariaHidden(element, hide) is replaced by hideFromAria(element) and exposeToAria(element) on the Modal subpath. The boolean-selector helper was a leaked internal of ModalManager - no consumer in this repo imports it, and modal behavior is unchanged. Shipped in a minor deliberately, following the precedent of the styled-system themeGet root-export removal: the export only existed by accident. Migrate ariaHidden(el, true) → hideFromAria(el) and ariaHidden(el, false) → exposeToAria(el).

v1.2.0

Maintenance release - dependency refresh only. No new components, no API or behavior changes. (#280)

Packaging

  • react and react-dom peer floors raised ^19.0.0 → ^19.2.8. Still React 19 only - the range narrows to the patch line this release is built and tested against. Consumers on an earlier React 19 should upgrade before installing.
  • @soroush.tech/styled-system dependency floor moves to ^5.8.1 with that package's own maintenance release. @emotion/* dependencies are unchanged.
  • Dev-dependency bumps: @storybook/react-vite ^10.5.0 → ^10.5.6, @testing-library/jest-dom ^6.9.1 → ^7.0.0, @types/node ^26.1.1 → ^26.1.2, @types/react ^19.2.17 → ^19.2.18, @types/react-dom ^19.2.3 → ^19.2.4, eslint ^10.7.0 → ^10.8.0, eslint-plugin-storybook ^10.5.0 → ^10.5.6, globals ^17.7.0 → ^17.9.0, playwright ^1.61.1 → ^1.62.1, react/react-dom ^19.2.7 → ^19.2.8, storybook ^10.5.0 → ^10.5.6, tsdown ^0.22.7 → ^0.22.14.
  • jsdom stays on ^29.1.1. jsdom 30 absolutizes relative lengths in getComputedStyle (2rem resolves to 32px), which breaks the toHaveStyle assertions this package's tests write in rem/em; the upgrade is held in renovate.json until those assertions stop comparing against computed style. Test-only - it does not affect the published package.

v1.1.0

Features

  • Sidebar + SidebarItem - a collapsible vertical icon rail. Collapsed, it shows icon-only items; open, each item's label appears next to its icon, rendered away from the anchored edge (a right-anchored rail shows labels to the left of the icons), with the icon pinned so it holds its position across the transition. Purely controlled via isOpen - the menu toggle lives with the consumer (typically the app bar). Children compose freely (items, logos, footers); anchor, expandedWidth, collapsedWidth, and any Flex prop (bg, ...) shape the rail. Items are built on Pressable - real buttons carrying only the rail's own styling, with no uppercase, bold weight, or letter-spacing inherited from the button family. They own their selected fill and aria-pressed, take a size density token, accept custom children for their open-state content, and stay accessible collapsed through aria-labels. The item variant (text default, outlined, plain) can be set once on the rail - Sidebar's variant flows to every item through context, with an item's own variant winning. The rail is a labeled <nav> landmark, animates its width honoring prefers-reduced-motion, and registers the Sidebar and SidebarItem theme slots. With hasPanel, the rail gains a second column and the selected item's children render there instead of inside its row - an icon rail beside a detail panel, sized by panelWidth. The column appears only when the selected item has children, so nothing selected leaves no empty gap; it sits on the rail's inner side following anchor, and is independent of isOpen, so a collapsed icons-only rail can sit beside an open panel. The panel is a <section> named by the item's label, and that item becomes a disclosure carrying aria-expanded and aria-controls. Selection stays the consumer's - the rail only decides where the children go. Off by default, so children keep rendering inline in the row as before.
  • Pressable - an unstyled clickable surface: button semantics (keyboard activation, focus ring, disabled state) with none of a button's looks. It carries no padding, margin, border, background, or font of its own, so it wraps arbitrary content without shifting it - the primitive to use instead of an onClick on a div, which no keyboard or screen-reader user can reach. It renders a div by default, since a <button> may not legally contain a link, another button, or block-level markup; that div is given role="button", a tab stop, Enter/Space activation (Space suppressed on keydown so the page never scrolls, fired on keyup), and aria-disabled. as="button" opts into a native button when the content is phrasing-only, href renders an anchor, and both skip the shim. feedback picks what happens while it is held: none (default) leaves the content alone, opacity fades it to activeOpacity, and highlight tints the surface with color at 12.5% opacity. Feedback is press-only, so hover styling stays with the consumer. Its text is never selectable, so a drag or double-click presses rather than highlighting, as on a native button. Accepts the space, layout, border, and typography props, and registers the Pressable theme slot with themeable feedback/color/activeOpacity defaults.
  • Icon - three new registry entries: folder, history, and edit_note.

Changes

  • gap, rowGap, columnGap and aspectRatio now come from View for every primitive built on it, resolved by @soroush.tech/styled-system 5.8.0's own space and layout parsers instead of per-component system() mappings. Flex, Grid, Button, Link and Paper dropped that hand-wiring - same CSS, same theme scale, same DOM filtering, and the props now accept responsive arrays everywhere. Flex's and Grid's GapToken exports are unchanged. (#314)

Breaking

  • Link's gap narrows from number | string to the theme.space token union, matching every other component's gap. Token values (gap={2}, gap="auto") are unaffected; raw CSS values (e.g. gap="8px") no longer type-check. Shipped in a minor deliberately: the prop reached raw CSS only because Link hand-wired its own gap parser, which is the inconsistency this release removes, and no consumer in this repo passes one. Replace a raw value with the nearest theme.space token, or set gap through style/css if you need an off-scale length. (#314)

v1.0.0

First stable release (#303) - the soroush.tech design system as a standalone package: design tokens, consumer-owned theming, and the full component set, styled through its own engine. The headless markdown editor/renderer ships separately as its companion package, @soroush.tech/[email protected].

Features

  • A token-driven, consumer-extensible baseTheme with the full component set: layout & surfaces (View, Flex, Grid, Paper, Card, AppBar, Drawer), content & data display (Typography, Link, Image, Icon, Table, Quote, Avatar, Skeleton, Pagination), inputs & forms (Button, ButtonGroup, TextInput, NativeSelect, Select, MenuItem, Checkbox, Radio, Switch, ToggleButton, Form, FormControl, FormLabel, FormHelperText), feedback (CircularProgress, LinearProgress), and overlay primitives (Modal, Popover, Portal, Backdrop, FocusTrap) - each on its own subpath (@soroush.tech/design-system/Button, .../Table/TableCell, ...).
  • A self-owned styling API: styled, css, and keyframes from the root barrel against the package's own Theme type - the styling engine is an internal implementation detail, and nothing engine-specific leaks through the public surface. docs
  • Three settled entry points: .../theme (ThemeProvider, createTheme, baseTheme, useTheme, useDefaultProps, withTheme), the custom-CSS layer at .../style (useStyle, withStyles, StylesConsumer), and raw engine escape hatches at .../engine (Global, globalStyles, CacheProvider, styleCache for SSR critical-CSS extraction - plus the re-exported @soroush.tech/styled-system primitives).
  • Consumer-owned theming: ThemeProvider provides one theme (built-in dark by default); createTheme(base, overrides) deep-merges sparse patches; every scale is an open interface, extended via declare module '@soroush.tech/design-system/theme' - palettes, tokens, whole scales, and theme.components slots. docs
  • Overridable component defaults: literal fallbacks (size = 'md', variant = 'outside', ...) resolve through theme.defaults / the provider's defaults prop.
  • Per-component customization via theme.components - defaultProps, per-slot styleOverrides with { theme, ownerState } callbacks, and theme-contributed variants (Button is the reference implementation; augmentable ButtonVariants). Zero cost for themes that don't use it.
  • Test utilities under .../utils/test (renderWithTheme, stories helpers) for consumers testing themed components.

Packaging

  • Dual ESM/CJS build via tsdown with a subpath export for every component folder, including nested ones.
  • Peers are just react / react-dom ^19; everything else ships as a regular dependency - nothing to install alongside.
  • Supersedes the 0.1.0 preview release: the markdown suite (and CodeBlock) now live in @soroush.tech/markdown, and the former .../themes / .../colors subpaths are folded into .../theme.