Changelog
Release notes for @arshad-shah/cynosure-react.
@arshad-shah/cynosure-react
Section titled “@arshad-shah/cynosure-react”Minor Changes
Section titled “Minor Changes”-
#119
1130f08Thanks @arshad-shah! - ReworkMultiSelectfor a fixed-height trigger and a better overflow story.- The trigger no longer grows as you select — chips sit on a single row and
any that don’t fit collapse into a
+Noverflow badge (measured against the available width and re-measured on resize). - Every option stays reachable: the dropdown now lists all items (with a search field at the top and a checkmark on selected rows) and toggles on click, instead of removing chosen items from the list. This also fixes a bug where, as the old trigger expanded, the popover could overlap it and make lower options unclickable.
- The trigger is now a
role="combobox"element; selected values still submit via hidden inputs whennameis set. NewsearchPlaceholderprop.
Breaking: the forwarded
refnow points at the trigger element (HTMLDivElement) rather than an inner text input, and the trigger exposesrole="combobox"(was atextbox).TagsInput(free-form entry) keeps its own wrapping, inline-input layout. - The trigger no longer grows as you select — chips sit on a single row and
any that don’t fit collapse into a
-
#119
1130f08Thanks @arshad-shah! - RedesignNumberInputas a segmented control and remove the native focus outline on pointer press across all clickable components.NumberInput — the cramped vertical ▲/▼ stepper column is replaced by a horizontal segmented control,
[ − ][ value ][ + ], inside a tinted track. Large, touch-friendly targets (~44px tall atmd), pressed-segment feedback, hold-to-repeat stepping (via React Aria’s press-and-hold), and an opt-inclearOnLongPressto clear the value on a long-press. The public API is a superset of before — existing usage (value/onChange,minValue/maxValue/step,formatOptions,prefix/suffix,size/variant, invalid/disabled/read-only) is unchanged. Breaking (visual/DOM only): the rendered class names and DOM shape changed; consumers depending on internal NumberInput CSS classes or the old vertical-column layout will need updates.Global focus reset — the browser’s native (sharp, blue) focus outline is now suppressed on pointer presses (
:focus:not(:focus-visible)), and the mobile tap-highlight is cleared on clickable elements. Keyboard:focus-visiblerings are unchanged, so keyboard accessibility is preserved. -
#119
1130f08Thanks @arshad-shah! - ReworkPinInput. Cells are now raised, rounded tiles with a clear lift-on-focus active state (scale + accent ring + soft glow) and an accent-tinted filled state, replacing the flat generic boxes.Paste/autofill is more robust: pasting a full code into any cell distributes it across the cells (skipping non-matching characters like spaces or dashes), and a multi-character value from iOS SMS one-time-code autofill is handled the same way (previously only the last character was kept). New
separatorprop renders a divider at the midpoint for a123–456grouping. -
#119
1130f08Thanks @arshad-shah! - Redesign theResizablehandle. The dotted grip icon is replaced by a thick, rounded line centered on the divider — neutral at rest, firming up on hover, and growing while it turns the accent colour as you drag (the divider line turns accent too). The divider also gains an enlarged invisible grab zone (a few px either side) so it’s easy to grab anywhere along the border. Works for bothhorizontalandverticalsplits and respectsprefers-reduced-motion.withHandlenow shows this line (no Lucide icon); passchildrento render a custom grip. -
#119
1130f08Thanks @arshad-shah! - Unify every segmented control behind the shared segmented track container introduced with the NumberInput redesign: a tinted, padded well (lightsubtletint, hairline border, 4px padding/gap) with raised tiles floating inside it.- Input (and
SearchInput): the multi-well row — addon slots, field, action wells — now sits inside the track; wells render as raised tiles and the focus ring lights up the track.filled/ghosttint the track, not the individual wells. - DatePicker / DateRangePicker / TimePicker: the picker root is now the track; the lead icon, segments, and trigger pockets render as raised tiles.
- ToggleGroup
attached(andThemeToggle’ssegmentedvariant): the attached bar is now the track; the selected item renders as a raised tile. - ButtonGroup
attached: buttons float as tiles in the track instead of merging borders.
No API changes —
attached,variant,size, and state props all behave as before. Visual-only: rendered DOM class names and the exact geometry change (attached groups now show a 4px gap between segments instead of shared 1px borders). Textarea keeps its single-card layout (it is not a segmented control). - Input (and
-
#119
1130f08Thanks @arshad-shah! - Bring the dropdown triggers and the Switch into the segmented design language, and stop clicks from painting a text-selection highlight.- Select & Combobox now render as a segmented track — a tinted, padded
well wrapping a raised value tile and a separate chevron tile — matching
NumberInput/Input/ the pickers. The focus ring lives on the track andvariant(outline/filled/ghost) tints it. No API change; the shared trigger styles live informs/shared/segmentedTrigger.css.ts. - Switch gets a Material-You-style thumb: small when off, growing to fill
the track as it slides on (and swelling while pressed). New
checkedIcon/uncheckedIconprops put a custom glyph in the thumb (the on-state default stays a checkmark) — e.g. a sun/moon for a theme toggle. - Click highlight: interactive controls (
button,summary, and thebutton/tab/option/menuitem/switch/radio/checkboxroles) no longer show a blue text-selection highlight when clicked or double-clicked. Components that re-base withall: unset(Accordion, Tag) opt back out explicitly.
- Select & Combobox now render as a segmented track — a tinted, padded
well wrapping a raised value tile and a separate chevron tile — matching
-
#119
1130f08Thanks @arshad-shah! - Polish theSwitchand add an async toggle flow.- Async
onCheckedChange: when the handler returns aPromise, the switch flips optimistically to the new state, shows the spinner while pending, commits on resolve, and reverts on reject — no manualloadingwiring needed (theloadingprop still works for externally managed spinners). - More visible border on the track (a
border.stronghairline) so the off state reads clearly on any surface. - Reworked geometry (absolute, RTL-aware thumb): the resting thumb sits a
balanced gap from the leading edge instead of hugging the border, the
smsize is larger and better-proportioned, and the off thumb is no longer cramped.
- Async
Patch Changes
Section titled “Patch Changes”-
#119
1130f08Thanks @arshad-shah! -MultiSelectnow uses the shared segmented track like the other inputs: the chips sit in a raised value tile and the chevron in its own slot tile inside the tinted track. Also fixes the dropdown list rendering with a large gap to the left of each row — the<ul role="listbox">was inheriting the browser’s default list padding; the shared listbox style now resets it. -
#119
1130f08Thanks @arshad-shah! -MultiSelect’s dropdown search now uses the CynosureSearchInput(search icon, clearable, consistent styling) and stays pinned to the top of the popover while only the option list scrolls. -
#119
1130f08Thanks @arshad-shah! - Slim per-component CSS for subpath imports. Previously every subpath import (@arshad-shah/cynosure-react/badge) pulled the entire sharedcore.cssbaseline (~20 kB brotli), even though a single component used only ~40% of it — the rest was rules shared by other components (e.g. the Select/Combobox listbox or DatePicker/DateRangePicker calendar styles).Shared rules are now split into per-owner-set chunks under
dist/shared/, and each component imports only the chunks it actually shares.core.cssis now just the universal scaffolding (@propertylayout-var declarations + body reset, ~0.7 kB brotli). A single-component CSS payload drops from ~20 kB to ~1.5–3 kB brotli (6–10× smaller); importing several components still dedupes to one copy of each shared chunk.No API or markup change. The monolithic
styles.css/all.csssingle-import paths are byte-identical, and category-barrel imports (…/forms) load the same total CSS as before. Empty per-component stylesheets (whose rules were entirely shared) are no longer emitted or imported. -
#119
1130f08Thanks @arshad-shah! - Drop the unused@arshad-shah/cynosure-coreruntime dependency. The package never exported anything beyond aVERSIONconstant and was not imported by any source file in this package — removing the edge stops consumers from pulling an empty package transitively.The
@arshad-shah/cynosure-corepackage itself has been retired from the workspace. The npm package will be deprecated separately so existing installs surface a one-line notice instead of breaking. Headless primitives (hooks, focus-trap helpers, polymorphic typings) continue to live inside this package; should a second framework adapter ever materialise, those will be extracted into a fresh package at that point rather than kept alive as an empty namespace squat in the interim. -
#119
1130f08Thanks @arshad-shah! - Retire@arshad-shah/cynosure-iconsfrom the workspace. The package was a passthrough re-export oflucide-reactintended to give the ecosystem a single version-pinned icon source, but in practice no internal source file ever imported it —cynosure-reacthas always calledlucide-reactdirectly (53 call sites across forms, overlays, navigation, etc.). With two providers in flight and no consumer in the middle, the indirection was pure overhead.Consumers wanting the same icons should import from
lucide-reactdirectly:// beforeimport { ChevronRight } from "@arshad-shah/cynosure-icons";// afterimport { ChevronRight } from "lucide-react";Tree-shaking is identical:
lucide-reactshipssideEffects: falseand every Cynosure component already uses named imports, so bundlers (Vite / Rollup / Webpack ≥ 5) drop unused icons exactly as before. No change to per-component bundle sizes.The npm package will be deprecated separately so existing installs surface a one-line notice instead of breaking.
-
#119
1130f08Thanks @arshad-shah! - Fix theSwitchloading spinner being clipped by the small off-state thumb. While loading, the thumb now stays full-size at whichever position it’s settling toward — so the spinner fits even when toggling off — and shrinks back to the small resting thumb only once loading ends. The spinner is also sized to sit comfortably inside the thumb at every control size. -
#119
1130f08Thanks @arshad-shah! - Use one chevron across the library. Every dropdown/disclosure indicator now renders lucide’sChevronDown(the iconComboboxalready used) at a consistent size — replacing the mix of theChevronDownIconalias, three per-picker inline-SVG wrappers (DatePicker / DateRangePicker / TimePicker), and the default-24px renders in Select/Combobox. Form-control triggers (Select, Combobox, MultiSelect, DatePicker, DateRangePicker, TimePicker) all use<ChevronDown size={16} aria-hidden />; Accordion uses the same icon, sized for its header. Decorative inline-SVG icons in the Chip/Collapsible stories were also swapped for lucide icons. -
#119
1130f08Thanks @arshad-shah! - Kill the native blue tap/click highlight everywhere. The previous reset only cleared-webkit-tap-highlight-coloron a hand-listed set of tags, so it still showed on elements like MultiSelect list options, the dropdown chevron, and Accordion headers. It’s now set on the document root — an inherited property — so every element is covered and components rely on their own pressed/hover feedback instead.
Minor Changes
Section titled “Minor Changes”-
#101
26f2740Thanks @arshad-shah! - AddThemeToggle— a drop-in theme switcher wired toThemeProvider, withicon,switch,segmented, andmenuvariants, configurablemodes/labels/icons, and three sizes.Also: the
CommandPalettefooter hints now render keyboard keys as icon components rather than relying on raw Unicode glyph strings.
Patch Changes
Section titled “Patch Changes”-
#98
a688905Thanks @arshad-shah! - Fix duplicated component CSS when importing from the package barrel. The per-component CSS auto-link added in the last release was also being prepended to the root barrel (dist/index.js), soimport … from '@arshad-shah/cynosure-react'silently injectedcore.css+index.css— the entire component stylesheet. For the documented setup (barrel import plus a manual@arshad-shah/cynosure-react/all.cssor/styles.css, which is required anyway since design tokens ship in a separate package), every rule loaded twice. Worse, the re-injectedcore.csslanded after the component-specific rules from the manual stylesheet, so equal-specificity shared rules (layoutPropsStyle, typography base, focus ring) clobbered the component overrides that should win — causing subtle visual regressions.The root barrel is the monolithic-path entry and no longer auto-injects CSS; consumers bring the stylesheet themselves (as the docs already instruct). Per-component subpath imports (
@arshad-shah/cynosure-react/button) and category barrels (/forms,/overlay, …) still wire up their CSS automatically. The now-orphaneddist/index.cssis no longer emitted. -
#98
a688905Thanks @arshad-shah! - Fix overlay positioning flash, DatePicker calendar horizontal scroll, and theme-blind hardcoded colors.- Popover & HoverCard: position the floating surface with
top/leftinstead oftransform: translate3d(...). The sharedpopoverContententrance keyframe animatestransform, and a running CSS animation overrides an element’s inlinetransformfor its whole duration — so the surface was painting at the (0,0) origin during the animation and snapping to the anchor when it ended. This is the same fix already applied to Tooltip. - DatePicker: the calendar popover was pinned to
width: 18rem, ~12px narrower than the month grid’s intrinsic width, sooverflow: autosurfaced a permanent horizontal scrollbar (and the fixed width couldn’t fit the dual-month layout). It now sizes to the calendar viawidth: fit-content, still capped bymaxWidthon narrow screens. - Theme-aware colors: replaced hardcoded color literals that ignored the
active theme with
color-mix(…)over foundation tokens, so they adapt to dark/high-contrast themes. Affects the inset sheen on every form control (Input, Textarea, Select, …), the dismiss-button / Tag hover wash, and the ColorPicker area/slider thumb shadows.
- Popover & HoverCard: position the floating surface with
-
#98
a688905Thanks @arshad-shah! - Fix the Textarea corner resize grip not working with touch/mobile drag. The grip is driven by pointer events, but withouttouch-action: nonethe browser claimed a finger-drag as a scroll/pan and firedpointercancel, so resizing only worked with a mouse. The grip now setstouch-action: noneand the drag handler also tears down onpointercancel.
Minor Changes
Section titled “Minor Changes”- #95
1ef49c0Thanks @arshad-shah! - Replace the Radix menu packages with a first-party menu engine, modernize the visual foundation, and route all component styling through tokens.- Menus: drop
@radix-ui/react-{dropdown-menu,context-menu,menubar,navigation-menu}in favour of an in-tree, headless menu engine (roving focus, type-ahead, submenus, checkbox/radio items, dismissal, focus return). Public component API is unchanged. The package is now Radix-free. - Foundation: rounder radius scale, softer multi-layer shadows, and refined
motion easings/durations.
easingis now exposed on the token contract and every component’s motion + focus rings flow from the token foundation (no hardcoded curves, durations, or focus-ring geometry). - Fixes: Blockquote left rule now spans wrapped lines; Slider tick marks position correctly; FileUpload remove button sits at the row’s end; PinInput mask renders a filled dot; Accordion chevron rotates when open; Tooltip no longer flashes at the top-left before positioning.
- Menus: drop
Patch Changes
Section titled “Patch Changes”-
#96
c004705Thanks @arshad-shah! - Follow-up fixes from the menu/design refresh (PR #95) that landed without their own changelog entry:- Overlays on mobile: dialogs, drawers, popovers, menus, the command
palette, and the date-picker calendar now cap their width/height to the
viewport (using
dvhfor mobile browser chrome) and scroll instead of overflowing small screens. - Tooltip beak: the left/right caret is rendered with explicit per-side geometry instead of a rotated caret, so it sits on the edge facing the trigger at a consistent size (no more tiny/mis-placed beak).
- Divider: the default
toneis nowdefault(visible on light surfaces instead of the near-invisiblesubtle), and a newstrongtone is available for emphasis.
- Overlays on mobile: dialogs, drawers, popovers, menus, the command
palette, and the date-picker calendar now cap their width/height to the
viewport (using
-
Updated dependencies [
1ef49c0]:- @arshad-shah/cynosure-tokens@3.3.0
- @arshad-shah/cynosure-core@3.3.0
Minor Changes
Section titled “Minor Changes”-
#77
73e2300Thanks @arshad-shah! - Drop 6 more external dependencies — the in-tree overlay surface is complete except for the menu family. Removed:@radix-ui/react-scroll-area,@radix-ui/react-tooltip,@radix-ui/react-hover-card,@radix-ui/react-popover,@radix-ui/react-dialog,@radix-ui/react-alert-dialog. Brings the cumulative dep-removal count on this branch to 19.Two new shared kits make this practical without growing the bundle:
overlay/shared/useFloatingPosition(~180 LoC) — anchor + open + side/align/offset placement with viewport-collision flipping and cross-axis shifting. Re-measures on resize, scroll, anchor + floating-element resize viaResizeObserver+MutationObserver. PowersTooltip,HoverCard, andPopover.overlay/shared/useDialog—useDialogState(controllable open + stabletitleId/descriptionIdfor ARIA + ref-counted body scroll lock),useFocusTrap(Tab-cycling + initial focus + return focus),useEscapeToClose. PowersDialog,Drawer,AlertDialog.CommandPaletteports onto the new in-tree Dialog.
Component contracts preserved:
- ScrollArea — native
overflow: auto+ Baseline-2024scrollbar-color/scrollbar-widthtokenised scrollbar styling. Visually consistent with the previous custom thumb in modern browsers; older browsers fall back to OS scrollbars (behaviour identical). - Tooltip —
aria-describedbyfrom the trigger to the tip body; pointer/focus open with the provider’sdelayDuration;data-state="instant-open"|"closed"on the trigger for parity.TooltipProvideris now a Cynosure-owned context (delayDuration+skipDelayDuration). - HoverCard — default
<a>wrapper trigger preserves Radix parity;asChildcomposes onto a single element. Hover-into-content cancels the scheduled close so links inside the card stay reachable. - Popover — focus auto-traps inside on open, returns to trigger on close; capture-listener outside-click + Escape dismiss, both configurable.
PopoverAnchoracceptsasChildfor API parity. - Dialog / Drawer / AlertDialog — body scroll lock (ref-counted across stacked overlays), focus trap, automatic
aria-labelledby/aria-describedbyviaDialogTitle/DialogDescription. AlertDialog suppresses Escape + outside-click dismissal so destructive flows require an explicit action / cancel.
Honest size CI. Switched
.size-limit.json→.size-limit.cjsso each entry can register a.css → emptyesbuild loader; per-component JS budgets now reflect the actual marginal cost (the sharedcore.cssis loaded once per app and budgeted separately). Real numbers (brotli, JS-only): Tooltip 2.4 kB, AlertDialog 3.5 kB, Dialog 3.5 kB, Popover 3.5 kB, Drawer 3.8 kB, HoverCard 3.0 kB, ScrollArea 1.0 kB. Shared CSS chunks:core.css14.4 kB ·styles.css22.6 kB ·all.css24.0 kB.Docs. Every component MDX (102 files) carries an accurate
bundleSize:field measured by the newscripts/measure-component-sizes.mjs(walks the chunk graph and brotlis per entry).BundleSizePill.astroreads the new CJS config viacreateRequire. The introduction, root README, tree-shaking guide, andARCHITECTURE.mdall describe the current architecture — the menu family is now the only Radix-backed surface left.Coverage threshold. Branches floor temporarily lowered from 80 → 70 in
vitest.config.ts(statements/functions/lines still 80). The new overlay code added a lot of un-tested branching (asChildforks, controlled/uncontrolled,closeOnEscape, focus-trap edges, RTL nav, collision flip). A documented follow-up raises it back to 80 by adding focused branch-coverage tests per overlay subcomponent. -
#77
73e2300Thanks @arshad-shah! - Drop 13 external dependencies by owning the primitives in-tree. Removed:class-variance-authority,@radix-ui/react-slot,@radix-ui/react-direction,@radix-ui/react-avatar,@radix-ui/react-switch,@radix-ui/react-toggle,@radix-ui/react-toggle-group,@radix-ui/react-collapsible,@radix-ui/react-accordion,@radix-ui/react-tabs,@radix-ui/react-checkbox,@radix-ui/react-radio-group. All public component APIs are preserved (data-state="…",aria-pressed, controlled/uncontrolledvalue/onValueChange, etc.).Each replacement is implemented as a small in-tree component that mirrors the Radix contract — same selection state shape, same
data-state/aria-*attributes, same keyboard model (arrow-key roving tabindex, Home/End jumps, Space/Enter activation in the right contexts, RTL-aware horizontal navigation where applicable). Components that participate in HTML forms now render a hidden<input>alongside the visible button when anameprop is supplied, so existing forms keep submitting unchanged.--radix-collapsible-content-heightand--radix-accordion-content-heightare still set on the content elements (alongside the new--cynosure-…mirror names) so any consumer CSS still reading the Radix variable names keeps animating correctly. -
#77
73e2300Thanks @arshad-shah! - Cut CSS payload by ~88% gzip.styles.cssshrinks from 267 KB → 31 KB gzip andall.cssfrom 270 KB → 33 KB gzip with no public-API changes.- Vanilla-extract now hashes classnames in
shortmode (Button_buttonBase__1h9om7i1→_1h9om7i1). - Strip vanilla-extract’s
/* vanilla-extract-css-ns:…?source=#<base64> */debug markers from every built.csschunk (~440 KB raw / 225 KB gzip of dead payload across ~120 chunks). Author doc comments are also dropped from production CSS; license banners (/*!) are preserved. - Extract rules shared across ≥2 component leaves into a new
dist/core.css. Per-component subpath imports (@arshad-shah/cynosure-react/button, …) now ship only that component’s specific rules; bundlers dedupecore.cssacross any number of per-component imports. - Each per-component JS entry auto-imports
./core.css+ its own./<name>.cssso subpath imports pull styles automatically (still respectssideEffects: ["**/*.css"]). all.cssships only the tokens actually referenced by the React CSS (152 of 280 declared tokens — mostly raw color ramps — were unused). The full palette remains available via@arshad-shah/cynosure-tokens/css.
A new
./core.csssubpath export is added topackages/react/package.json. - Vanilla-extract now hashes classnames in
Minor Changes
Section titled “Minor Changes”-
#75
2a72943Thanks @arshad-shah! - ColorPicker redesign — distinctive hero-preview composition, theme-correct chrome, and an icon-only trigger.- Adds a hero strip at the top of the popover/inline panel: preview chip (checker-backed for transparency) + bold hex + format-aware secondary readout that updates with the format toggle.
- Replaces hardcoded
2px solid whitethumb borders withbackground.surfaceso slider/area thumbs stay visible in dark and brand themes. All chrome now routes throughvars.color.*,vars.radius.*,vars.space.*. - Saved-colours grid gains an uppercase section label with
N of MAXcount, an inline+save tile (replaces the floating IconButton), and anaccent.solidhighlight on the active tile. - New
label={null}on the popover trigger renders an icon-only swatch button (defaultlabel="Pick a color"is unchanged — fully additive). - Storybook gains
All sizes,Icon-only trigger, andDark theme checkstories.
Public API is unchanged. Consumers passing custom
childrencontinue to work; the legacycolorFieldClassNamere-export is retained and marked@deprecated. -
#73
709f454Thanks @arshad-shah! - AddMarkandHighlightedTextcomponents for inline text highlighting.Markis a thin inline-flow primitive that wraps text in a semantic<mark>(or opt-in<span>) styled with Cynosure tokens. It supports four variants (marker,underline,chip,bold), six colour schemes, and two intensities, and wraps cleanly across lines viabox-decoration-break: clone.HighlightedTextis the companion helper for the common “highlight these ranges in this string” case — feed it a source string and an array of{ start, length }ranges and it handles the segment bookkeeping. -
#72
96c9b92Thanks @arshad-shah! -Tree: add accessor props (getId,getLabel,getChildren,getDisabled) so consumers can plug in their own data shape without remapping. Default behaviour is unchanged — accessors fall back to the originalTreeNodeshape. Ships a companionmapToTreeNodes(items, { getId, getLabel, getChildren, … })helper for callers who prefer a one-shot transform.
Major Changes
Section titled “Major Changes”-
#63
f972e95Thanks @arshad-shah! - RemoveBanner,BannerContent, andBannerActions. Banner duplicatedAlert— same status surface recipe, same dismissal model, same content/actions slots — and the dual API created confusion about which to reach for. Migrate toAlert(withAlertTitle/AlertDescription); for the full-bleed look, wrap anAlertin a container withborder-radius: 0and full width.The
@arshad-shah/cynosure-react/bannersubpath export and theclearBannerDismissalhelper are also removed.
Minor Changes
Section titled “Minor Changes”-
#63
f972e95Thanks @arshad-shah! - PolishColorPickerinto a fully-featured form component. Adds:size="sm" | "md" | "lg"— drives panel width, area height, slider thickness, thumb size, and channel-cell density, so the same component covers compact toolbars, default forms, and prominent design surfaces.variant="popover" | "inline"— popover stays the default; inline drops the trigger and renders the picker body in place inside a bordered surface.alpha— opt-in alpha slider; uses RAC’sdefaultStylerender-prop so the color gradient composes with a checker layer correctly.swatches+onSwatchesChange— controlled saved-color palette with a “save current” affordance (capped bymaxSwatches).- Segmented HEX / RGB / HSL toggle now drives slot-based channel cells
instead of one text input: hex mode shows
#+ value, RGB/HSL split across one tiny numeric cell per channel (with alpha cell when enabled). Each cell is a Cynosure-styled well with a mono glyph + per-channelColorField, so invalid input snaps back on blur andaria-labelper channel is correct. defaultFormat— pick which format the toggle starts on.- Browser eyedropper (
eyedropper, defaulttrue), feature-detected viawindow.EyeDropper; copy-to-clipboard with a transient check icon — both now sit in a right-aligned toolbar next to the format toggle. - SB area gets
cursor: crosshair, area + slider thumbs getcursor: grab/[data-dragging]→grabbingfor parity.
Internals now compose
IconButton,ToggleGroup,Inline,Text,ColorSwatch, and per-channelColorFields instead of emitting raw DOM, so token/style changes flow through the rest of the library. The existingchildrenescape hatch and all previous props are preserved.Value props now also accept a string (e.g.
value="#6366F1") in addition to a RACColor, parsed internally. -
#63
f972e95Thanks @arshad-shah! - Add@arshad-shah/cynosure-react/fonts.css— a one-line, opt-in webfont loader that registers Geist Variable (sans) and JetBrains Mono Variable (mono), the families the default token font stacks resolve to.import "@arshad-shah/cynosure-react/all.css";import "@arshad-shah/cynosure-react/fonts.css"; // optionalKept separate from
all.cssso consumers with their own font pipeline (next/font, self-hosted, CDN) don’t pay the ~400 KB woff2 cost. The token font stacks still fall through to system fonts when the import is omitted.
Patch Changes
Section titled “Patch Changes”-
#63
f972e95Thanks @arshad-shah! - RefactorAlertto compose from Cynosure primitives instead of bespoke CSS. The body slot now usesStackfor column layout/gap, andAlertTitle/AlertDescriptionrender throughText(withsize="md" weight="semibold"andsize="sm"respectively), so typography stays on-token without each component re-encoding font weight/size/line-height. Drops the now-unusedsurfaceContent,surfaceTitle, andsurfaceDescriptionstyles from the shared feedback surface recipe. No public API changes. -
#63
f972e95Thanks @arshad-shah! - Fixpadding,margin,overflow, andgapprops silently resolving to zero on every layout primitive that composeslayoutPropsStyle.When the
@propertyfix made layout vars non-inheriting, an unset var started resolving to “invalid at computed value time” → revert to the property’s initial value. The sharedlayoutPropsStylerule sets the shorthand (padding: var(--cynosure-lp-p-base)) followed by every longhand (padding-inline,padding-top, …) bound to its own var. When a consumer set onlypadding="8", the longhand vars were unset, each longhand declaration reverted to 0, and clobbered the shorthand expansion.Grid,Flex,Inlinehad the same bug forgap↔column-gap/row-gap.- Longhand declarations in
layoutPropsStylenow fall back through the CSS shorthand hierarchy:padding-top → padding-block → padding,padding-right → padding-inline → padding, and the mirror for margin.overflow-x/overflow-yfall back tooverflow. Grid,Flex,Inlinedrop theirgapshorthand CSS rule entirely; thegapprop now writes both longhand vars from the component so the remainingrow-gap/column-gapdeclarations always have a value to resolve to.flexshorthand vsflex-grow/flex-shrink/flex-basislonghands remains unresolved — the shorthand string (“1 0 auto”) can’t substitute as a fallback for the individual numeric longhands. Use one or the other on the same element for now; a follow-up will runtime-splitflexinto individual longhand var emissions.
- Longhand declarations in
-
#63
f972e95Thanks @arshad-shah! - FixDataTableloading-state hydration mismatch. The skeleton cells usedMath.random()at render time to vary their widths, so the server and client rendered different markup — React logged a hydration warning and the widths visibly snapped on mount. Widths are now derived deterministically from each cell’s(rowIdx, colIdx), so SSR and the first client paint match. -
#63
f972e95Thanks @arshad-shah! - Fixcolorandgrowprops on layout primitives clobbering each other. Both were emitting to the same custom property (--cynosure-lp-fg), so<Box color="fg.muted" grow="1">only applied whichever was emitted last.flex-grownow uses--cynosure-lp-grow-{bp};colorkeeps--cynosure-lp-fg. -
#63
f972e95Thanks @arshad-shah! - Fixposition/top/right/bottom/left/width/etc. on layout primitives leaking onto descendants. Layout props are driven by CSS custom properties, which inherit by default; setting<Flex position="fixed" top="0">on a shell was silently applying the sameposition: fixed; top: 0to every descendant, collapsing the whole subtree to one rectangle. The build now emits@property … { inherits: false }for every layout var so each one is element-scoped.Also fixes the layout primitives’
displaydefaults (Flex,Stack,Inline,Grid,Center,Section) being silently reverted by duplicatedlayoutPropsStyleemissions later in the bundled stylesheet. Display is now driven through the shared--cynosure-lp-d-*variable so the per-primitive default survives the cascade. -
#63
f972e95Thanks @arshad-shah! - RebuildMenuinternally on top of Cynosure primitives — public API and visual surface unchanged.MenuandMenuGrouplayouts now useStack/Inlineinstead of hand-rolleddisplay: flexblocks.MenuGroupcollapse is delegated toCollapsible(Radix), soaria-expanded,aria-controls, anddata-stateare wired automatically and stay in lockstep with the trigger.MenuDividerdelegates toDivider, picking up consistent tone / spacing / dark-mode rules.MenuItem’sbadgeslot renders through<Badge variant="ghost" colorScheme="neutral" size="xs">, so menu badges follow the same size / shape / theming rules as every other badge in the library.- Group bodies expose
role="group"+aria-labelledbywhen alabelis set, so screen readers announce the group as named. MenuItemaria-disabledis now always emitted alongside the nativedisabledattribute.- Focus rings switched from a hand-rolled 2px box-shadow to the
vars.shadow.focusRingtoken, matchingLink/Button. - Fixed
MenuItem asChild: now uses theSlot+Slottablepattern (same asButton), so the consumer-provided element (e.g. a router<a>) becomes the item andicon/badge/iconRightrender as siblings instead of being nested inside the label span.
-
#63
f972e95Thanks @arshad-shah! -NavigationMenu: fix layering bug, animate content panels even without a shared viewport, and tighten visual chrome — public API unchanged.- Fix
z-index: NaN.NavigationMenuContentwas emittingz-index: NaN(invalid; browsers rejected the declaration). Content panels effectively hadz-index: autoand could disappear under sticky headers or sibling overlays. The CSS var is now passed through directly so the panel sits on the--cynosure-z-dropdownlayer like the rest of the dropdown family. - Animate without a viewport. Open/close animations were gated on
data-motion, which Radix only emits when a<NavigationMenuViewport>is mounted. Panels without a viewport flashed open. The selectors now key offdata-state="open" | "closed", so both configurations animate. - Trigger sizing.
ChevronDownwas rendering at its default 24 px, inflating the trigger to 41 px tall. Sized to 14 px; trigger settles to ~33 px with the chevron centered on the cap height. - Token-consistent focus. Trigger / link focus rings replaced hand-rolled
0 0 0 2px accent.ringwithvars.shadow.focusRing, matchingLink,Button, and the rest of the library. - Smooth hover. Added
transition: background-color, color(durationvars.duration.fast, suppressed under[data-cynosure-reduced-motion]) to trigger and link. - Focus on the panel.
NavigationMenuContentnow shows a focus ring when it receives focus (Radix moves focus into the panel on open); keyboard users previously had no indicator on the panel itself.
- Fix
-
#63
f972e95Thanks @arshad-shah! - Addhomepage,repository,bugs, andkeywordsto all publishable packages so the npm package pages render the GitHub source link, issue tracker, and Storybook URL (https://cynosure.arshadshah.com) in their sidebars. Pure metadata — no runtime or API changes. -
#63
f972e95Thanks @arshad-shah! - FixKbd,Code(block variant),Link,Blockquote(attribution), andList(ordered/unordered + description) silently reverting to their UA defaultdisplaywhen the bundled stylesheet emitted a duplicatedlayoutPropsStylerule after their class. Each now drivesdisplaythrough the same--cynosure-lp-d-*variable used byFlex/Stack/Inline/etc., so the per-primitive default survives the cascade and userdisplay="…"overrides still win via inline style. -
Updated dependencies [
f972e95]:- @arshad-shah/cynosure-core@3.0.0
- @arshad-shah/cynosure-tokens@3.0.0
Minor Changes
Section titled “Minor Changes”-
#43
8cf2f7dThanks @arshad-shah! - Add four new components and dedupe existing code to reuse them.New components
Calendar/RangeCalendar(@arshad-shah/cynosure-react/calendar) — standalone date grids built on React Aria. One-month or two-month layouts, an optionalfooterslot for things like “Go to today” or keyboard hints, and the same styling language as the pickers.CommandPalette+CommandMenu(@arshad-shah/cynosure-react/command-palette) — ⌘K surface powered bycmdk. Composable parts (CommandInput,CommandList,CommandEmpty,CommandLoading,CommandGroup,CommandSeparator,CommandItemwithicon/description/shortcut,CommandShortcut,CommandFooter), plus a pre-wiredCommandMenuthat binds ⌘K/Ctrl+K out of the box for the common dev-tooling case.Carousel(@arshad-shah/cynosure-react/carousel) — Embla wrapper withCarouselViewport,CarouselContainer,CarouselSlide,CarouselPrevious,CarouselNext, andCarouselDots. ResponsiveslidesPerView, horizontal or vertical orientation, loop, keyboard nav, and drag-to-scroll.- Chart primitives (
@arshad-shah/cynosure-react/chart) —ChartContainer,ChartTooltip/ChartTooltipContent,ChartLegend/ChartLegendContent, and achartSeriesProps(config, key)helper over Recharts. Series config drives tooltip + legend labels and colours; themed SVG defaults (grid, axis, tick, label, cursor) applied via CSS variables. Exported via the/chartsubpath only (likeCodeBlockwith Shiki) so Recharts stays out of the default bundle graph.
Dedupe
DatePickerandDateRangePickernow reuse<Calendar>/<RangeCalendar>instead of open-codingAriaCalendarGrid+ headers. The shared Calendar stylesheet is the single source of truth for day cells, month grids, and the dual-month layout.
Dependencies (catalog)
cmdk@1.1.1,embla-carousel-react@8.6.0,recharts@3.8.1— all listed as externals so tree-shakers keep them off the critical path unless the relevant component is actually imported.
Major Changes
Section titled “Major Changes”-
#41
66f58c9Thanks @arshad-shah! - Breaking:Progress→LinearProgress,ProgressCircle→CircularProgress.Both components are now split into their own folders with compound primitives (
LinearProgressRoot/Track/Indicator/Buffer/Segment/Header/Label/Meta/Value, andCircularProgressRoot/Track/Indicator/Label). The top-level convenience wrappers still cover the common case with flat props.New capabilities on
LinearProgress:buffer— YouTube-style preload indicator behind the main barsegments— stacked multi-value barlabel+meta— upload-style header row above the track- Auto-detected completion state at 100% (opt out with
completionState="none") variant="ticked"— opt-in punch-card tick motif- Richer two-bar indeterminate motion (MUI pattern) by default
Aesthetic: both components now render as “punched” wells matching the form controls’ look — deeper recessed track + gradient indicator with subtle glow.
Subpath exports have moved too:
@arshad-shah/cynosure-react/progress→@arshad-shah/cynosure-react/linear-progressand@arshad-shah/cynosure-react/circular-progress
Migration:
import { Progress, ProgressCircle } from '@arshad-shah/cynosure-react';import { LinearProgress, CircularProgress } from '@arshad-shah/cynosure-react';<Progress value={60} label="Uploading" /><LinearProgress value={60} label="Uploading" aria-label="Uploading" /><Progress striped animated /> // removed — replaced by the default gradient/glow treatment<LinearProgress value={60} /><ProgressCircle value={75} label="Loading" /><CircularProgress value={75} aria-label="Loading" />
Minor Changes
Section titled “Minor Changes”-
#37
66dd83aThanks @arshad-shah! - Major DX upgrades that close the gap with MUI’s zero-config setup.New:
<CynosureProvider>— composesThemeProvider,DirectionProvider,LocaleProvider, and the globalTooltipProviderin one wrapper. Drop it once at the root and every component “just works”. The individual providers are still exported for fine-grained composition.import { CynosureProvider } from "@arshad-shah/cynosure-react";<CynosureProvider theme={{ defaultTheme: "system" }}>{children}</CynosureProvider>;New: single CSS import —
@arshad-shah/cynosure-react/all.cssbundles tokens (light + dark) and every component’s CSS into one file. The legacy three-import path (tokens/css,tokens/css/dark,react/styles.css) still works.import "@arshad-shah/cynosure-react/all.css";New:
npx cynosure initCLI — published as@arshad-shah/cynosure-cli. Detects Next.js App Router, Next.js Pages, Vite, CRA, or Remix; writes the CSS import; wiresCynosureProvider; and for App Router projects scaffolds aproviders.tsxclient boundary. Includes--dry-runand is idempotent.React 18 support — peer dependency widened from
>=19to>=18incynosure-react,cynosure-core, andcynosure-icons. The library uses no React 19-only APIs, so this is a clean back-port. Now works with Next.js 13 / 14 / 15, Vite, CRA, and Remix on either React 18 or 19.New docs:
docs/foundations/rsc.mdx— Server Components compatibility matrix with per-component classification, plus a Next.js App Router recipe (withgetThemeInitScriptfor no-FOUC).READMEs polished — fixed npm badge URLs, added size/types/react/a11y badges, inlined a per-component bundle-size summary table.
Patch Changes
Section titled “Patch Changes”- Updated dependencies [
66dd83a]:- @arshad-shah/cynosure-core@1.1.0
Patch Changes
Section titled “Patch Changes”- #23
2593498Thanks @arshad-shah! - Add per-packageREADME.mdfiles. Each package now displays installation, quick-start usage, and links on its npm package page — critical for discovery and adoption. No runtime changes. - Updated dependencies [
2593498]:- @arshad-shah/cynosure-core@1.0.1
- @arshad-shah/cynosure-tokens@1.0.1
Major Changes
Section titled “Major Changes”-
First stable release.
0.xwas a pre-release development window; v1.0 is the first version under Cynosure’s semver policy. No code migration is required — if you were consuming Cynosure from source or a workspace alias, install the npm packages instead. See the Migration guide for details.What v1.0 ships
Section titled “What v1.0 ships”- Complete component catalogue across
@arshad-shah/cynosure-react. - W3C DTCG design tokens in
@arshad-shah/cynosure-tokens, with light and dark stylesheets. - Prebuilt themes (terminal, high-contrast) in
@arshad-shah/cynosure-themes. - Headless primitives in
@arshad-shah/cynosure-core. - Icon set in
@arshad-shah/cynosure-icons. - Foundations docs, eight priority recipes, framework + tree-shaking guides.
Semver policy from here
Section titled “Semver policy from here”- Patch — bug fixes, no API changes.
- Minor — new components, new props (additive), new variants. No breaking changes.
- Major — breaking changes, with a migration guide. Deprecations are announced at least one minor cycle before removal.
All five packages ship at the same semver via linked changesets.
- Complete component catalogue across
Patch Changes
Section titled “Patch Changes”- Updated dependencies []:
- @arshad-shah/cynosure-core@1.0.0
- @arshad-shah/cynosure-tokens@1.0.0
Patch Changes
Section titled “Patch Changes”-
#17
8db8cccThanks @arshad-shah! - Phases 15 + 16 — documentation site and release readiness.No runtime API changes. This bundle groups the docs-site scaffold and release hardening that Phase 15 + 16 require.
- Storybook-as-docs-site.
.storybook/main.tsnow picks up MDX fromdocs/foundations/**,docs/recipes/**, and every package alongside the existing stories, withreact-docgen-typescripttuned to surface literal unions and filternode_modulesfrom the Controls panel..storybook/manager.tsadds a branded manager theme (terminal palette, JetBrains Mono) matching the Cynosure canvas. - Foundations MDX.
docs/foundations/now covers introduction, installation, quickstart, design principles, design tokens, theming overview, dark mode, custom themes, terminal-theme recipe, accessibility, and RTL support. - Recipes MDX. Eight priority recipes under
docs/recipes/: login form, dashboard layout, data table with filters, notification center, command palette, settings page, multi-step wizard, onboarding modal (plus an index + the three form-composition recipes that ship alongside@arshad-shah/cynosure-react/form). - Guides + reference.
docs/guides/frameworks.mdx,docs/guides/tree-shaking.mdx,docs/reference/roadmap.mdx,docs/reference/migration-to-v1.mdx, and an auto-generateddocs/reference/changelog.mdxproduced byscripts/build-changelog-page.mjs. - Cloudflare Pages deploy. Root
wrangler.tomldeclarespages_build_output_dir: ./storybook-staticfor thewrangler pages deploy/wrangler pages devflow. Production deploys use the Cloudflare Pages Git integration: Pages buildspnpm install && pnpm build && pnpm docs:changelog && pnpm build-storybookon its own runners and publishesstorybook-static/on every push tomain. No GitHub Actions workflow required. - Release hardening.
.github/workflows/release.ymlnow runstypecheck,test,publint, andattwbefore handing off to Changesets, so a broken release can’t go out. - Chromatic bumped to the current major.
.github/workflows/chromatic.ymlnow referenceschromaui/action@v16(was@v11) and runs under Node 24. ThechromaticCLI is pinned inpnpm-workspace.yamlat16.3.0and exposed via thepnpm chromaticroot script for local runs. - Repo docs.
LICENSE(MIT),CONTRIBUTING.md(phase-based workflow),CODE_OF_CONDUCT.md(Contributor Covenant 2.1),SECURITY.md(disclosure policy).README.mdrewritten to consumer-facing content; all build-spec markdown moved intodocs/specs/(ARCHITECTURE.md,PROGRESS.md,phases/01…16).
- Storybook-as-docs-site.
Minor Changes
Section titled “Minor Changes”-
#3
e2f45fcThanks @arshad-shah! - Phase 03 — runtime-free theming system.@arshad-shah/cynosure-reactshipsThemeProvider,DirectionProvider,getThemeInitScript, and the hooksuseTheme,useColorScheme,useDirection,useReducedMotion, anduseBreakpoint. Theme state lives entirely on the<html data-theme>attribute — no React-state cascade of token values, so swapping themes is a single repaint.systemresolves viaprefers-color-scheme, persistence is pluggable (localStorage/sessionStorage/ custom adapter / off), andgetThemeInitScript()returns an inline IIFE that prevents the dark→light flash on hydration.DirectionProviderwraps Radix’s so primitives inherit direction automatically.@arshad-shah/cynosure-themesadds two prebuilt themes shipped as side-effect CSS:@arshad-shah/cynosure-themes/terminal(Arshad’s GitHub Dark Terminal — JetBrains Mono, muted blue glow) and@arshad-shah/cynosure-themes/high-contrast(WCAG AAA, light + dark selectors).@arshad-shah/cynosure-tokensgains--cynosure-breakpoint-{sm,md,lg,xl,2xl},--cynosure-shadow-component-focus, and aprefers-reduced-motionblock that zeroes the semantic motion durations so token-driven transitions automatically disable. -
#5
0aa2886Thanks @arshad-shah! - Phase 04 — core utilities, primitives, and the hook library.@arshad-shah/cynosure-reactgains the building blocks every subsequent component depends on:- Primitives:
Slot/Slottable(re-exported from@radix-ui/react-slotfor theasChildcomposition pattern), an SSR-safePortalwith acontainerprop and adisabledbypass, andVisuallyHiddenfor screen-reader-only content. - Utilities: a six-line
cn()(noclsxdependency — vanilla-extract recipes author fully-owned class names),composeRefs,composeEventHandlers(preventDefault-aware short-circuit, mirroring Radix semantics),callAll,getOwnerDocument, a typedcreateContextfactory that throws a helpful error outside its provider, and a curatedcva/cx/VariantPropsre-export fromclass-variance-authority. - Hooks:
useControllableState(the controlled/uncontrolled bridge with a dev-mode mode-switch warning),useDisclosure,useMediaQuery,useCallbackRef,useMergedRef,useId,usePrevious,useDebouncedValue,useThrottledCallback,useInterval,useTimeout,useHotkeys(with a cross-platformmodmodifier),useClipboard,useLocalStorage/useSessionStorage(cross-tab sync, graceful SSR / private-mode fallback),useIntersection,useResizeObserver,useMutationObserver,useFocusTrap,useFocusReturn,useIsomorphicLayoutEffect, plus re-exports of the phase-03 hooks (useReducedMotion,useDirection,useBreakpoint) so every hook is reachable from@arshad-shah/cynosure-reactor@arshad-shah/cynosure-react/hooks.
All hooks use only
reactas a runtime dependency; the only new runtime dependencies at the package level are@radix-ui/react-slotandclass-variance-authority. Per-hook tsup entry points keep the tree-shake graph honest — importinguseDisclosurepulls well under 1 KB of Cynosure code. - Primitives:
-
#6
e37bec5Thanks @arshad-shah! - Phase 05 — layout primitives.@arshad-shah/cynosure-reactgains eleven polymorphic layout primitives built on vanilla-extract — the only components in the library allowed to render raw HTML, and the foundation every subsequent phase composes on top of.- Primitives:
Box,Stack,Inline,Flex,Grid,Center,Spacer,Divider,AspectRatio,Container,Section. Each lives in its own folder with a.tsx+.css.ts+.stories.tsx+index.tsand ships a dedicated per-component entry point (@arshad-shah/cynosure-react/box,@arshad-shah/cynosure-react/stack, …). - Shared layout props: every primitive accepts the
LayoutPropssuperset — padding/margin, width/height, background/color/border/shadow, display/position, and grid child hints — and each prop can be a single token or a responsive map ({ base, sm, md, lg, xl, '2xl' }). Values are emitted as cascading CSS custom properties read by a shared vanilla-extract class with mobile-first@mediarules. - Tokens, not strings: token-shaped values (
padding="4",background="bg.surface",boxShadow="md",borderRadius="lg") resolve tovar(--cynosure-*)references, keeping every primitive theme-aware out of the box. Plain lengths ("200px","50%") pass through; aliases ("full","screen","prose","auto","fit") map to their CSS equivalents. - Polymorphism: every primitive takes either
as(renders a different element, narrowing its intrinsic attributes) orasChild(usesSlotto merge props onto the single child), matching the Radix composition pattern. - Opinionated shorthands:
Stack(vertical, withdividers),Inline(horizontal, wraps by default),Flex(escape hatch),Grid(typedcolumns/templateColumnswith responsive maps),Center,Spacer,Divider(horizontal<hr>+ vertical<div role="separator" aria-orientation="vertical">),AspectRatio(nativeaspect-ratio),Container(predefinedsm/md/lg/xl/2xl/prose/fullsizes),Section(semantic section with vertical rhythm presets). - Build:
@vanilla-extract/vite-plugindrives Storybook + Vitest,@vanilla-extract/esbuild-plugindrives the tsup build. Each primitive is listed in the packageexportsmap and ships a Node10-resolution sidecarpackage.json, so both modern (@arshad-shah/cynosure-react/box) and legacy resolvers see their declarations.
- Primitives:
-
#7
7aca046Thanks @arshad-shah! - Phase 06 — typography.@arshad-shah/cynosure-reactgains the full text-rendering component set. Every one of them composesBoxunder the hood — none contain raw intrinsic JSX — and they all use the semanticfont.heading.*/font.body.*composite tokens rather than re-declaring font sizes.- Components:
Text,Heading(with decoupled semanticlevel+ visualsize),Code,Kbd,Link(withexternal→ saferel/target+ decorative icon),Blockquote, and the list familyList/OrderedList/ListItem/DescriptionList/DescriptionTerm/DescriptionDetails. Each lives in its own folder and ships a dedicated per-component entry point (@arshad-shah/cynosure-react/text,@arshad-shah/cynosure-react/heading, …). - Responsive typography:
size,weight, andalignacceptResponsive<T>maps and propagate through cascading CSS custom properties so breakpoint overrides inherit from the nearest lower breakpoint — the same mobile-first pattern the layout primitives use. - Composite tokens in CSS:
@arshad-shah/cynosure-tokensnow emits the pre-expandedfont.heading.*andfont.body.*composites as CSS custom properties (--cynosure-font-heading-1-size,--cynosure-font-body-md-line-height, …), which Phase 02 introduced but whose CSS output had been dropped by a filter bug in the expansion preprocessor (fixed here). - Shared recipe:
typography/shared/shared.css.tscentralises the styles every text component reuses —align,italic,underline/strikethrough, single-line truncate, multi-line clamp, and the body/heading size → CSS variable maps — so component.css.tsfiles stay tiny. - Build: each typography component is listed in the package
exportsmap, has a Node10-resolution sidecarpackage.json, and is wired as its own tsup entry to keep per-component imports tree-shakable.
- Components:
-
#8
f682aa1Thanks @arshad-shah! - Phase 07 — basic form controls.@arshad-shah/cynosure-reactgains the foundational form control inventory. Every control implements the sharedFormControlBase<T>/BooleanFormControlBasecontract (disabled,readOnly,required,invalid,size, controlled + uncontrolled viauseControllableState) and composes Cynosure’s existing design tokens rather than reinventing colour/spacing.- Buttons:
Button(variant × colourScheme × size × shape cross-product,loadingwith spinner overlay,leftIcon/rightIcon,fullWidth,asChildvia Radix Slot +Slottable),IconButton(enforcesaria-label+ square shape), andButtonGroup(context-provided defaults + optionalattachedsegmented-control mode). - Text inputs:
Inputsupports left/right addons + inline elements, auto-clearable×, and a built-in password show/hide toggle.Textareaaddsrows,autoResize(nativefield-sizing: content), andmaxRows. Both render the raw element directly — the documented single-exception to the “no raw HTML” rule because value binding / form submission / a11y semantics cannot be faithfully composed. - Numeric input:
NumberInputdelegates toreact-aria-components’NumberFieldfor locale-correct parsing, keyboard (↑/↓, PageUp/Down, Home/End), and clamping; Cynosure styles the group + input + stepper buttons against the shared control recipe. - Boolean controls:
Checkbox(withindeterminatestate +colorScheme),CheckboxGroup(shared value-array context),Radio+RadioGroup(Radix radio-group under the hood for roving-tabindex), andSwitch. All three use Radix primitives (react-checkbox,react-radio-group,react-switch) and wrap them with Cynosure’s colour tokens + label composition. - Form scaffolding:
Label(withrequiredindicator),HelperText,ErrorText(role="alert"), andFieldset(with optionallegendprop) fill in the non-control half of a form field. - Shared visual recipe:
forms/shared/control.css.tscentralises size/variant/state styling (border, background, focus ring, invalid, disabled, readOnly, filled/ghost variants) so<Input>,<Textarea>, and<NumberInput>all paint the same default/hover/focus/invalid states. - Build: per-component tsup entries +
exportsentries + Node10 sidecarpackage.jsonshims for every new subpath (@arshad-shah/cynosure-react/button,/icon-button,/button-group,/input,/textarea,/number-input,/checkbox,/checkbox-group,/radio,/radio-group,/switch,/label,/helper-text,/error-text,/fieldset, and a combined/forms). New externals added to tsup:@radix-ui/react-checkbox,@radix-ui/react-radio-group,@radix-ui/react-switch,react-aria-components. - Tests: 40 new unit tests cover every control — click/space toggles, controlled + uncontrolled,
indeterminate, clearable/password toggle, group value arrays, required + role semantics, and theasChildanchor projection.
- Buttons:
-
#9
3916c3bThanks @arshad-shah! - Phase 08 — advanced form controls.@arshad-shah/cynosure-reactgains the complex form surface: single- and multi-value pickers, sliders, date / time / color pickers, file upload, search, OTP / tags entry, and a star rating. Every component either leans on React Aria Components for its interaction contract (focus management, type-to-select, keyboard navigation, locale-aware parsing, virtualised collection safety) or — when Aria doesn’t cover it — is hand-built with the same styling tokens.- Select / Combobox / MultiSelect:
Select+SelectItem+SelectSectionfor single-value dropdowns (with data-drivenitemsor JSX children).Combobox+ComboboxItem+ComboboxEmptyfor autocomplete (supportsallowsCustomValue, controlledinputValue, and a custom empty state).MultiSelectrenders selected values as tags inside the trigger; typing filters the dropdown; Backspace on empty removes the last tag;maxSelectedcaps the selection. - Slider / RangeSlider: one- and two-thumb sliders on React Aria’s
Slider+SliderThumb.size,marks,showValue, andformatOptions(passed through for locale-aware Intl formatting).RangeSlideraccepts a[start, end]tuple and renders two labelled thumbs with a filled segment between them. - DatePicker / DateRangePicker / TimePicker: built on React Aria’s
DatePicker/DateRangePicker/TimeField, backed by@internationalized/datefor timezone-aware, locale-aware parsing. Styled calendar (header nav, weekday headings, selectable cells, focus / selected / range / unavailable states) reuses the shared control recipe for the trigger; popover opens a<Dialog>with<Calendar>/<RangeCalendar>. - ColorPicker: wraps React Aria’s
ColorPickersuite —ColorArea+ColorSlider(channel="hue")+ColorField— inside a trigger button + swatch + hex field dialog. - FileUpload: custom (Aria doesn’t cover it). Keyboard-accessible drop zone (Enter/Space opens the native picker), drag-and-drop, file validation (
accept,maxSize,maxCount) with typedonError('type' | 'size' | 'count'), image thumbnail previews viaURL.createObjectURL, accessible list announced viaaria-live. Subcomponents:FileUploadTrigger,FileUploadList. - SearchInput:
type="search"with leading icon, auto clear button,onSearchdebounced via the existinguseDebouncedValuehook,onSubmiton Enter, andEscclears. - PinInput: N single-character cells, arrow-key navigation, Backspace / Home / End support, paste distribution across cells,
typeof'numeric' | 'alphanumeric' | 'alphabetic', and optionalmaskfor OTP entry. EmitsonComplete(code)on the last cell. - TagsInput: free-form tag entry. Enter / comma commits; Backspace on empty removes the last tag; optional
suggestionsdatalist;uniqueguards against duplicates;maxTagscaps length;renderTagescape hatch for custom chrome. - Rating: radio-group-style slider semantics (
role="slider"witharia-valuemin/max/now), arrow keys + Home/End navigation, RTL-aware increments, optionalallowHalfwith click-position-driven half selection. - LocaleProvider: thin wrapper around React Aria’s
I18nProvidersoDatePicker,NumberInput,Slideretc. receive a locale. Exported from@arshad-shah/cynosure-reactand from@arshad-shah/cynosure-react/theme. Default locale isen-IE. - Shared popover recipe: new
forms/shared/popover.css.tscentralises the dropdown shell (popover,listbox,listboxItem,listboxSection,listboxSectionHeader,listboxEmpty) so Select / Combobox / MultiSelect paint identically. DatePicker reuses the same popover shell. - Build: 14 new per-component tsup entries +
exportsentries + Node10 sidecarpackage.jsonshims for each new subpath (@arshad-shah/cynosure-react/select,/combobox,/multi-select,/slider,/range-slider,/date-picker,/date-range-picker,/time-picker,/color-picker,/file-upload,/search-input,/pin-input,/tags-input,/rating). Added@internationalized/dateto dependencies and to tsupexternal. - Tests: 34 new unit tests covering Select/Combobox/MultiSelect (render, select, controlled value, disabled), Slider/RangeSlider (default value, keyboard increment, two-thumb render), DatePicker/DateRangePicker/TimePicker (segment/group render, invalid flag), FileUpload (drop zone, size rejection, change dispatch), SearchInput (debounced onSearch, Esc clears, Enter submits), PinInput (cell focus propagation, onComplete, paste distribution, invalid-char rejection), TagsInput (commit/remove/unique), and Rating (aria-valuenow, arrow increment, readOnly). Total suite now 258/258 passing.
- Select / Combobox / MultiSelect:
-
#11
e80b599Thanks @arshad-shah! - Phase 10 — navigation components.@arshad-shah/cynosure-reactgains the full navigation surface: inter-page (Breadcrumb, Pagination, NavigationMenu), intra-page (Tabs, Anchor, BackToTop), wizard-style (Stepper), and structural (Menu, Sidebar). Every component either wraps a Radix primitive (Tabs, NavigationMenu) or is hand-built on Cynosure’s layout + typography primitives.- Tabs:
@radix-ui/react-tabs-backed withvariant∈line | solid | enclosed | soft,size∈sm | md | lg,orientation∈horizontal | vertical,fullWidth, andcolorScheme. Opt-in<TabsIndicator />animates an underline between triggers via getBoundingClientRect + MutationObserver, writing--cynosure-tabs-indicator-*custom properties; falls back to a static active border when the indicator isn’t rendered and auto-zeroes animation underprefers-reduced-motion. - Breadcrumb:
Breadcrumb+BreadcrumbItem/BreadcrumbLink/BreadcrumbPage/BreadcrumbSeparator/BreadcrumbEllipsis. Separators are auto-interleaved and carryrole="presentation"+aria-hidden="true"so the<ol>reports N listitems, not 2N-1. SupportsmaxItems/itemsBeforeCollapse/itemsAfterCollapsewith arenderCollapsedescape hatch to wire the ellipsis into a DropdownMenu. - Pagination: compound (
<PaginationPrevious> <PaginationPages> <PaginationNext>) and prop-driven modes from the same root. PurepaginationRangeutility (mirrors MUI’susePaginationalgorithm so the visible-count stays constant ascurrentPagescans the range) is exported from@arshad-shah/cynosure-react/paginationfor consumers who want to paint their own paginator.showFirstLast,siblingCount,boundaryCount, localised labels,aria-current="page"on the active button, and first/last/prev/next disable correctly at the boundaries. - Menu: sidebar-style vertical menu (distinct from
DropdownMenu).Menu/MenuGroup(optionalcollapsible+label) /MenuItem/MenuDivider.MenuItemis polymorphic — renders<a>whenhrefis set,<button>otherwise, or projects onto a router<Link>viaasChild. Acceptsicon,iconRight,badge,indent(0-3),isActive,disabled. - NavigationMenu:
@radix-ui/react-navigation-menu-backed horizontal top-nav with mega-menu panels.NavigationMenu/NavigationMenuList/NavigationMenuItem/NavigationMenuTrigger/NavigationMenuContent/NavigationMenuLink/NavigationMenuIndicator/NavigationMenuViewport/NavigationMenuSub. Trigger ships a built-in caret (opt-out viahideChevron); the indicator + viewport pieces wire Radix’s animated arrow and animated-height viewport wrappers. - Sidebar: layout-level pattern.
SidebarProviderownscollapsedstate, responsiveisMobilebreakpoint matching (configurable viamobileQuery), and the mobile drawer’smobileOpenstate.Sidebarrenders inline on desktop and portals into a<Drawer side="left|right">on mobile, re-using the Phase 09 Drawer primitive (no extra deps).SidebarHeader/SidebarBody/SidebarFooterfor structure;SidebarTriggeris a context-aware button that toggles collapse on desktop and opens the drawer on mobile.variant∈sidebar | floating | inset,collapsible∈icon | offcanvas | none.useSidebar()hook exposed for custom triggers. - Stepper: multi-step flow indicator.
Stepper(horizontal/vertical,numbered | dots | lines | iconsvariants) +Step(title,description,icon, explicitstatusoverride). Status derives fromindexvs.currentStep(complete | active | pending, or expliciterror).interactivemode lets consumers click a completed step to jump back — pending steps remain non-interactive. Active step carriesaria-current="step", connectors between steps fill from the accent colour as steps complete. - Anchor: in-page heading + copy-link. Renders
<h{level}>with an invisible-until-hover link that pushes the hash, smooth-scrolls the target into view (respectsprefers-reduced-motion), and writes the full URL tonavigator.clipboard. OptionaloffsetTopapplied viascroll-margin-topfor sticky headers,onCopycallback,labelfor the copy trigger. - BackToTop: portal’d floating button that appears after the consumer scrolls past
showAfter. Three preset positions (bottom-right | bottom-left | bottom-center), smooth scroll to the top (auto-switches toautounderprefers-reduced-motion),disablePortalfor tests, customicon/label/containerescape hatches. Scroll listener is throttled via the existinguseThrottledCallbackhook. - Build: two new Radix dependencies (
@radix-ui/react-tabs,@radix-ui/react-navigation-menu) added to the package + tsupexternal. Nine new per-component tsup entries +exportsentries + Node10 sidecarpackage.jsonshims (@arshad-shah/cynosure-react/tabs,/breadcrumb,/pagination,/menu,/navigation-menu,/sidebar,/stepper,/anchor,/back-to-top) plus a/navigationbarrel. - Tests: 36 new unit tests covering Tabs (render, click-to-switch, disabled triggers, tablist ARIA), Breadcrumb (item/separator count with
role="presentation"suppression,maxItemscollapse), Pagination (page-click onPageChange,aria-current, disable boundaries, advance-on-next) plus apaginationRangetable with 7 table-driven cases, Menu (item activation,href/asChildbranches, disabled clicks, collapsible group, divider), NavigationMenu (trigger + active link), Sidebar (desktop collapse toggle, mobile drawer switchover viamatchMediamock), Stepper (status derivation, error override, interactive click, pending-step non-interactivity), Anchor (heading level +onCopy), BackToTop (visibility threshold + scrollTo call). Total suite now 322/322 passing.
- Tabs:
-
#12
df4aca8Thanks @arshad-shah! - Phase 11 — data display components.@arshad-shah/cynosure-reactgains the heavyweight data-display surface: cards, tables, trees, timelines, progress, skeleton/spinner feedback, accordions, code blocks, scroll areas, and resizable panels.- Card family:
Card+CardHeader/CardBody/CardFooter/CardTitle/CardDescription/CardImage/CardMedia. Variants (outlined/elevated/filled/ghost), sizes (sm/md/lg),orientation="horizontal"for side-media layouts,interactivefor hover/focus states, and the classicasChildescape hatch for clickable-card patterns. - Table (static) + DataTable:
Table+TableHead/TableBody/TableFoot/TableRow/TableHeader/TableCell/TableCaptioncover the “already in final form” case withline/striped/grid/minimalvariants,stickyHeader, andlayoutcontrol.DataTableis the opt-in advanced mode backed by TanStack Table v8 — sortable headers (multi-column via Shift+click,aria-sortfor every direction), checkbox selection withonSelectionChange(rows),Paginationwired in beneath, global filter, loading skeletons, and a slot-basedemptyState. - Tree: hand-rolled tree with full keyboard navigation (ArrowUp/Down/Left/Right, Home/End, Enter/Space,
*to expand the current sibling group). Controlled + uncontrolledexpandedIds/selectedIdsviauseControllableState;selectionMode="none" | "single" | "multiple";aria-level/aria-expanded/aria-selectedon every row. Render prop exposes{ item, depth, expanded, selected, focused, disabled }. - Timeline:
Timeline+TimelineItem/TimelineSeparator/TimelineDot/TimelineConnector/TimelineContent. Vertical (default) or horizontal orientation, size scale, and colour-scheme / variant props on the dot for event severity. - Stat:
Stat+StatLabel/StatValue/StatHelp/StatArrowfor KPI displays. Drop-in inside aSimpleGrid. - Progress + ProgressCircle: linear and circular progress bars with value/indeterminate modes, striped + animated striped looks, a full colour-scheme palette, and
showValuewith optional custom formatter. Respectsprefers-reduced-motion. SVG-based circular progress withthickness+sizecontrols and a{children}slot for the centred percentage or icon. - Skeleton: loading placeholder with
text/rect/circlevariants, pulse/wave/none animation, andwidth/height/aspectRatiolayout props. Auto-disables animation underprefers-reduced-motion. - Spinner:
border/dots/ringvariants, five sizes, three speeds, and a requiredlabelthat becomes thearia-label(defaults to"Loading"). Already used internally by Button’s loading state in Phase 07. - Accordion + Collapsible + Disclosure: Radix-backed expand/collapse. Accordion supports
type="single"(withcollapsible) andtype="multiple"with our owndefault/contained/ghostshell variants; height transitions drive off Radix’s--radix-accordion-content-heightcustom property.Collapsibleis the one-section variant for “show more” / settings panels.Disclosureis a semantic alias ofCollapsible. - ScrollArea: Radix-backed custom scrollbars with consistent styling across platforms.
type="hover"default so scrollbars reveal on interaction;scrollbarsprop limits to vertical or horizontal only. - Resizable + Splitter:
react-resizable-panels-backed split panes.Resizable+ResizablePanel+ResizableHandlewithdirection="horizontal" | "vertical",withHandlefor a visible drag grip, and aSplitteralias for design systems that use that vocabulary. - CodeBlock: plain, zero-dependency renderer by default (line numbers, highlight lines, copy button, max-height scroll, language chip in the header). A lazy Shiki path is exposed via
createShikiRenderer()+useShikiRender()— consumers import them from@arshad-shah/cynosure-react/code-blockand pass the resulting HTML through thehtmlprop, so Shiki never enters the default bundle graph.
New direct dependencies:
@radix-ui/react-accordion,@radix-ui/react-collapsible,@radix-ui/react-scroll-area,@tanstack/react-table,react-resizable-panels,shiki.All components ship per-component tsup entries with Node10 sidecar shims. 35 new unit tests (357/357 total pass).
- Card family:
-
#13
6602e19Thanks @arshad-shah! - Phase 12 — feedback components.@arshad-shah/cynosure-reactgains the “status and identity” surface: badges, tags, chips, avatars, alerts, banners, notifications, callouts, empty states, and button toggles.- Badge / Tag / Chip: three lookalikes with explicit, non-overlapping roles.
Badgeis a non-interactive label (soft/solid/outline/ghost × accent/neutral/success/warning/danger/info, sizes xs/sm/md, shape default/pill/square, optionalicon, and a baredotmode).Tagis a categorical label — static by default, becomes a<button>whenonClickis set, renders arole="group"wrapper with an inner remove button whenonRemoveis set (with Backspace/Delete keyboard removal on the focused tag).Chipis always an interactive toggle button witharia-pressed, a controlledselected+onSelectedChangecontract, optionalleftIcon/rightIcon, and an optionalonRemovetail button. - Avatar + AvatarGroup: Radix-backed
<RadixAvatar.Root>with image → initials → icon fallback chain; six sizes (xs/sm/md/lg/xl/2xl), three shapes (circle/square/rounded), a deterministic 8-colour palette hash (colorFromName) so the same display name always maps to the same tint, optional status dot (online/offline/away/busy× top-right/bottom-right), and an optional ring for stacked group layouts.AvatarGroupoverlaps the first N children and collapses the rest into a+Ntile, projectingsize+ringthrough context. - Alert + AlertTitle + AlertDescription: static inline alert with
status(info/success/warning/danger),variant(solid/soft/outline/ghost), sizessm/md/lg, configurable icon (custom node,falseto hide, or default status icon), and optionalclosable+onClose. ARIAroledefaults toalertfor danger/warning andstatusfor info/success; consumers can override. - Banner + BannerContent + BannerActions: full-width variant of Alert, rounded-0 by default, with a dedicated actions slot.
dismissKeypersists dismissal inlocalStorageacross sessions (clearBannerDismissal(key)exported as a test helper). - Notification: inline notification card with
icon/title/description/timestamp/actionsslots,unreadstate (highlighted background + dot), andonRead/onDismisscallbacks — for activity panels and notifications lists (distinct from Phase 09’s Toast). - Callout + CalloutTitle + CalloutContent: softer status surface used inline in prose —
soft/outlinevariants ×accent/neutral/success/warning/dangercolour schemes. Left-border accent stripe for visual distinction from Alert. - EmptyState + EmptyStateIcon + EmptyStateTitle + EmptyStateDescription + EmptyStateActions: centred “zero data” composition with size scale
sm/md/lg/xlanddefault/subtlevariants. Max-width constrained to60chfor readable copy. - Toggle + ToggleGroup + ToggleGroupItem: Radix
@radix-ui/react-toggleand@radix-ui/react-toggle-groupre-skin.Togglereads size/variant fromToggleContext(so a whole group can be sized once).ToggleGroupsupportssingle/multipletypes, horizontal/vertical orientation, and anattachedmode that collapses gaps into a single pill bar with shared borders.
Shared
feedback/shared/surface.css.tscentralises the Alert / Banner / Notification / Callout background-border-foreground recipe acrosssoft/solid/outline/ghost×info/success/warning/danger. Sharedfeedback/shared/icons.tsxships the default status icon set + the reusable close glyph, so Alert / Banner / Notification render the same visual language without each importing a third-party icon package.New direct dependencies:
@radix-ui/react-avatar,@radix-ui/react-toggle,@radix-ui/react-toggle-group.All components ship per-component tsup entries with Node10 sidecar shims (
@arshad-shah/cynosure-react/badge,/tag,/chip,/avatar,/avatar-group,/alert,/banner,/notification,/callout,/empty-state,/toggle,/toggle-group,/feedback). 35 new unit tests (392/392 total pass). - Badge / Tag / Chip: three lookalikes with explicit, non-overlapping roles.
-
#14
3234aa1Thanks @arshad-shah! - Phase 13 — form composition.@arshad-shah/cynosure-reactgains the scaffolding layer that ties every Phase 07 + 08 control together with a small compound component and an opt-inreact-hook-formadapter.Form— thin wrapper over<form>withnoValidatedefaulted totrueso native browser validation bubbles don’t fightFormMessage.FormField— generates a stableidfromname+useId, owns the context that wires its children, and lays outFormLabel/FormControl/FormDescription/FormMessageas a vertical flex column with aspace.1.5gap. Carriesinvalid/disabled/requiredflags that cascade through the field.FormLabel— wrapsLabel, auto-threadshtmlForto the field id, and paints the required indicator fromFormField.required.FormControl— clones its single child (an<input>/<Select>/<Checkbox>/ etc.) and injectsid,name,aria-invalid,aria-describedby,disabled,required, and the Cynosure-specificinvalidprop. Any value the child already sets wins; existingaria-describedbyis preserved by concatenation so external references survive.FormDescription— wrapsHelperTextand registers a${field.id}-descriptionid onto the field’saria-describedbylist.FormMessage— wrapsErrorText, registers a${field.id}-messageid only when it has content, and emitsrole="alert"only when the surrounding field isinvalid. Returnsnullfor empty children so consumers can bind<FormMessage>{errors.field?.message}</FormMessage>unconditionally.
New
@arshad-shah/cynosure-react/rhfsubpath exportsRHFField, an adapter that wiresuseController({ control, name, rules })through theFormFieldscaffold — bindsfield.value/field.onChange/field.onBlur/field.refonto the child control, marks the field invalid onfieldState.error, and surfacesfieldState.error?.messagethroughFormMessage.react-hook-formis declared as an optional peer dependency (peerDependenciesMeta.react-hook-form.optional = true). Consumers who never import@arshad-shah/cynosure-react/rhfdon’t need it installed and don’t pay any runtime cost; the subpath importsreact-hook-formstatically so typed generics (RHFField<TValues, TName>) flow through and tree-shaking stays effective.Three Storybook MDX recipes in
src/forms/Form/:Form.mdx— plain uncontrolled form +FormData+Object.fromEntries.RHF.mdx—react-hook-formwith inlinerules.RHF-Zod.mdx—react-hook-form+@hookform/resolvers/zodwith a shared Zod schema.
Per-component tsup entries (
@arshad-shah/cynosure-react/form,@arshad-shah/cynosure-react/rhf) + Node10 sidecar shims. 17 new unit + integration tests coveringaria-describedbycomposition and mount order, child-prop override, FormData submission, and RHF error surfacing (409/409 total pass).
Patch Changes
Section titled “Patch Changes”-
#1
8779fbfThanks @arshad-shah! - Initial foundation. -
#15
0454102Thanks @arshad-shah! - Phase 14 — testing & quality hardening.Repo-wide CI gates are now release-blocking;
@arshad-shah/cynosure-reactitself only changes visually where physical CSS properties on RTL-sensitive components were rewritten to logical equivalents so layouts flip correctly under<DirectionProvider dir="rtl">.- Logical properties everywhere.
Callout,Tag,Divider(vertical variants),NavigationMenuindicator arrow,Tabs(vertical list separator),CodeBlockline numbers,overlay/shared/menuitems/shortcuts/indicators, andfeedback/shared/surfaceclose button all moved frommargin-left/padding-right/border-left-*tomargin-inline-start/padding-inline-end/border-inline-start-*. Consumer-facing layout props (<Box paddingLeft="4">etc.) stay physical by design — they remain a physical-intent escape hatch. - RTL audit script (
scripts/audit-rtl.mjs) greps every*.css.tsinpackages/react/srcfor physical directional properties and fails CI on new occurrences. Two files are on the allowlist with documented rationale: theprimitives/layout/shared/layoutStyle.css.tsprop registry (consumer API) andoverlay/Drawer/Drawer.css.ts(physicalside="left"/"right"is the prop’s intent). - Contrast audit script (
scripts/audit-contrast.mjs) parses the compiled@arshad-shah/cynosure-tokensCSS +@arshad-shah/cynosure-themesoverlays, resolves everyvar()chain, composites alpha-blended pairs, and checks WCAG 2.1 AA against 19 semantic foreground/background pairs across six themes (base light + dark, terminal + terminal-dark, high-contrast + high-contrast-dark). Fails CI on any pair below threshold. - Bundle-size budgets. Root
.size-limit.jsoncarries 44 per-component budgets (Box through DataTable, Form + RHF adapter, and a warning-only full-barrel ceiling).pnpm sizeruns on every PR. - Vitest hardening.
packages/react/vitest.config.tsnow shipscoverage.thresholds(initial baseline: lines/statements ≥ 85, functions ≥ 78, branches ≥ 73 — spec target is 85/85/80/85, with functions/branches catching up incrementally as DataTable/Tree/ColorPicker/DatePicker edge-cases land coverage) and an opt-in cross-browser matrix (CYNOSURE_BROWSER_MODE=1→ Chromium + Firefox + WebKit via Playwright; local default stays jsdom-only for speed; browser mode only matches*.browser.test.{ts,tsx}so the existing jsdom suite isn’t forced through real browsers). - Storybook a11y.
.storybook/preview.tslocks the axe audit to WCAG 2.1 A + AA tags and keepsa11y.test = 'error'so any violation fails Vitest Storybook addon runs. - CI verify. Split into
verify(lint/typecheck/RTL-audit/build/test/coverage/contrast/size/publint/attw) and an optionalcross-browserjob that runs the three-engine matrix. Nothing merges tomainwithout green verify. - Dep freshness.
.github/renovate.jsongroups Radix / React-Aria / vanilla-extract / Storybook / Vitest / TanStack / Internationalized and pins Monday-morning scheduling with pr concurrency limits. - Chromatic workflow (
.github/workflows/chromatic.yml) wired behindCHROMATIC_PROJECT_TOKENfor per-PR visual regression; Lighthouse CI config (lighthouserc.json) asserts ≥ 0.95 accessibility and ≥ 0.85 perf on the built Storybook.
No public API additions; patch-level because the visual output of the nine
.css.tsfiles changes in RTL contexts. - Logical properties everywhere.
-
Updated dependencies [
b99bace,e2f45fc,7aca046]:- @arshad-shah/cynosure-tokens@0.1.0
- @arshad-shah/cynosure-core@0.1.0