Commit e92f5d4
authored
v2.0.0: migrate to Base UI, slim the bundle (#153)
* [migration] Phase 0: visual regression baseline + showcase preview
Setup Playwright visual regression testing as a gate for the upcoming
@mui/base -> @base-ui/react migration. 25 baseline screenshots cover all
components currently using @mui/base plus surrounding components.
- Expand preview-page from a single Button to a structured showcase with
data-testid markers per section. Floating UI (Modal, Menu, Select,
Tooltip, Snackbar) renders in dedicated showcases via ?show=... query
param so screenshots can capture each open state.
- Add @base-ui/react@^1.4 and react-textarea-autosize as direct deps so
individual component PRs can pull from them.
- Add @playwright/test, playwright.config.ts, and tests/visual/preview.spec.ts.
Tests run against vite preview build on port 4173.
- Add scripts: preview:build, test:visual, test:visual:update.
- Add about/migration-base-ui.md with the full plan (migration + slimming
@mui/material/@emotion + DatePicker rebuild epic).
Baseline bundle (for delta tracking): dist/overflow-ui.js 606.45 kB
(gzip 155.24 kB), dist/index.css 81.88 kB (gzip 10.59 kB).
* [migration] Phase 1: BaseButton, Switch, Input -> @base-ui/react
Migrate three low-risk components off @mui/base. Visual regression (25
snapshots) is identical to baseline.
- BaseButton: drop prepareForSlot wrapper. The wrapper was only needed
because @mui/base required slot-roots to be prepared this way; Base UI
uses render-props instead, so the BaseButton no longer needs slot
semantics. Replace <Button from '@mui/base/Button'> with native <button>
since our usage didn't depend on any @mui/base/Button-specific
features.
- Switch: replace <SwitchBase slotProps={...}> with Switch.Root +
Switch.Thumb compound. Base UI 1.x has no Switch.Track, so trackChildren
is rendered manually. Wraps the Root in an outer <span> with display:
contents render-prop trick to keep the existing CSS selectors working
(the hidden <input> stays a sibling of Root, so :has(input:checked) on
the wrapper still applies). Public API of BaseSwitchProps preserved.
- Input: drop slotProps. Base UI Input is a single component with no
startAdornment/endAdornment built in, so render adornments manually as
a wrapper <div>. Public API unchanged. types.ts no longer extends
@mui/base InputProps - uses native InputHTMLAttributes.
Library bundle delta: 606.45 kB -> 615.06 kB (gzip 155.24 -> 158.97).
Slight increase because @mui/base is still installed for the remaining
components; will reverse once the migration is complete.
* [migration] Phase 2: TextArea, Modal -> base-ui + drop @mui/material/@emotion
Migrate TextArea and Modal off @mui/base, simultaneously drop
@mui/material (only Fade was used) and the entire @emotion family
(peer deps of @mui/material with zero direct usage in src).
- TextArea: replace @mui/base/TextareaAutosize with the standalone
react-textarea-autosize (same author, drop-in API). One intentional
visual delta: react-textarea-autosize correctly honors minRows={2}
whereas @mui/base ignored it on first render - baseline updated for
the textarea section to reflect the corrected behavior.
- Modal: rewrite with @base-ui/react Dialog (Root + Portal + Backdrop
+ Popup compound). Drop the @mui/material Fade transition entirely
- Base UI handles enter/exit via data-starting-style / data-ending-style
data-attributes, but we omit the fade transition for now since
visual tests run with animations disabled. Existing CSS classes
(modal-base, backdrop, modal) work unchanged with the new DOM shape.
Dialog.Title and Dialog.Description rendered as <span> via render
prop to preserve the previous DOM (defaults are <h2>/<p>).
- Drop dependencies: @mui/material, @emotion/styled, @emotion/react.
No remaining direct or transitive usage.
Library bundle delta: 615.06 kB -> 577.88 kB (gzip 158.97 -> 148.24,
-10.73 kB gzip). 24/25 visual snapshots identical to baseline; textarea
baseline updated as documented above.
* [migration] Phase 3: Menu, Select -> @base-ui/react
Migrate the two high-risk components off @mui/base. All 25 visual
snapshots identical to baseline.
- Menu: rewrite from <Dropdown><MenuButton/><MenuBase/></Dropdown> to
<Menu.Root><Menu.Trigger render={children}/><Menu.Portal>
<Menu.Positioner side align sideOffset alignOffset><Menu.Popup>
<Menu.Item/></Menu.Popup></Menu.Positioner></Menu.Portal></Menu.Root>.
Floating-ui Placement is mapped to Base UI side+align via a small
helper. OffsetOptions is mapped to sideOffset/alignOffset. Public
API (items, size, placement, open, onOpenChange, offset, children)
unchanged. The slotProps escape hatch was dropped - no consumer in
the monorepo used it.
- create-trigger-button.tsx removed entirely. Base UI Menu.Trigger
natively merges its props and ref into the element passed via
render={children}, so the cloneElement+forwardRef helper became
redundant.
- Select: full compound rewrite with Select.Root + Trigger (rendering
our SelectButton) + Value (render-prop child) + Portal + Positioner
+ Popup + Item. Drop UseSelectParameters base type in favor of an
explicit SelectBaseProps interface. SelectButton simplified to a
forwardRef'd <button> taking native props (no more
SelectRootSlotProps with ownerState). SelectValue takes the raw
value rather than a SelectOption from @mui/base.
- select-button.module.css: replace .base--expanded selector with
[data-popup-open] (Base UI Trigger's open-state data attribute).
- select.module.css: width: 100% on .popup -> width: var(--anchor-width)
since the Positioner is now portaled to body and the old percentage
was relative to a position:relative container.
- list-box.module.css: replace .base--open visibility hack with
[data-open] attribute selector to match Base UI's open-state
signaling.
Library bundle delta: 577.88 kB -> 667.49 kB (gzip 148.24 -> 176.27).
Temporary increase because @mui/base is still pulled in for Snackbar
(the only remaining consumer) - that goes away in the next phase.
* [migration] Phase 4+5: Snackbar wrapper drop, @mui/base removed
Final phase. @mui/base no longer in dependencies; the migration off
the deprecated package is complete.
- Snackbar: drop the @mui/base/Snackbar wrapper. The wrapper was
little more than an open-gate around <div> with no functional
contribution to the visible component (no autohide, no transitions,
no portal). Replace with a plain <div role="status"
aria-live="polite"> that preserves the same DOM shape and
accessibility semantics. No public API change.
- Drop dependency: @mui/base. grep -r '@mui/base' packages/ui/src
returns nothing.
Library bundle delta: 667.49 kB -> 628.27 kB (gzip 176.27 -> 165.94).
Compared to the pre-migration baseline (606.45 kB / 155.24 kB gzip)
the bundle is +21.82 kB / +10.70 kB gzip - @base-ui/react is heavier
than the @mui/base subset we used because we now ship full Floating
UI for Menu/Select/Dialog. Net slimming will come from the separate
DatePicker epic which removes the Mantine block (~105 kB gzip).
* [research] Bundle slimming options + bundle-stats tooling
Four parallel research-agent reports plus synthesis covering:
- bundle composition (rollup-plugin-visualizer breakdown by package)
- DatePicker rebuild alternatives (react-day-picker vs react-aria etc.)
- Externalization strategies (peer deps scenarios A/B/C)
- Code-splitting / subpath exports (vite multi-entry feasibility)
Key findings (see about/research-final-recommendations.md for the full
synthesis):
- Floating UI exists in three independent copies in the bundle:
@floating-ui/react@0.26 (direct, our Tooltip + Mantine), the 1.7.x
stack pulled in transitively via @base-ui/react, and a vendored
copy inside @base-ui/react/esm/floating-ui-react/. ~50 kB realnych
gzip total.
- Mantine costs 53 kB gzip realnie (measured by physical rebuild with
vs without Mantine). DatePicker rebuild on react-day-picker would
net 5-20 kB gzip, not 80 kB as initially estimated.
- Tree-shaking effectively does NOT work for the current single-entry
monolithic bundle. A consumer importing only Button still pulls
~105 kB gzip; multi-entry subpath exports would drop that to
40-60 kB gzip.
- @base-ui/react accounts for ~125 kB gzip and is "honest cost" with
proper tree-shaking; further reduction in the library bundle
requires externalization as a peer dependency (scenario B major
v2.0.0 bump).
Tooling additions:
- rollup-plugin-visualizer in devDependencies
- packages/ui/scripts.build:stats produces dist/bundle-stats.html and
dist/bundle-stats.json. Gated on BUNDLE_STATS=1 env var so the
default build stays fast.
* [migration] Phase 6: Tooltip -> @base-ui/react/tooltip + drop @floating-ui/react
Migrate Tooltip off the custom @floating-ui/react implementation onto
Base UI's compound Tooltip primitives. Drop @floating-ui/react from
direct dependencies. All 25 visual snapshots identical to baseline.
- tooltip.tsx: wrap Tooltip.Root with delay=500ms / closeDelay=0ms to
preserve the previous timing. Two internal contexts bridge the API
shape: placement->side/align via TooltipPlacementContext, and
delay/closeDelay (Base UI puts these on Trigger, not Root) via
TooltipDelayContext.
- tooltip-trigger.tsx: use Base UI Tooltip.Trigger via render prop.
Without asChild it still renders <div>{children}</div> with
data-state="open|closed" so the existing DOM shape stays the same.
closeOnClick={false} preserves the prior behavior (clicking the
trigger did not close the tooltip).
- tooltip-content.tsx: replace the custom FloatingPortal/FloatingArrow
with Tooltip.Portal -> Positioner -> Popup -> Arrow. Same CSS classes
applied on the Popup so the visual output is unchanged. Arrow is a
small inline 10x4 SVG whose color follows the popup background via
currentColor + the new .arrow-default/.arrow-blue rules.
- use-tooltip.tsx: deleted - Base UI handles the floating-ui plumbing
internally.
- TooltipOptions moved from use-tooltip.tsx to tooltip.tsx; the
Placement union is locally defined (replaces import from
@floating-ui/react).
- menu.tsx: Placement and OffsetOptions are now locally defined types
(structurally compatible with floating-ui's, so no consumer breaks).
- Drop @floating-ui/react from packages/ui/dependencies. Mantine still
pulls it transitively, so it does not fully leave node_modules until
the DatePicker rebuild.
Library bundle delta: 628.27 kB -> 609.78 kB (gzip 165.94 -> 160.58,
-5.36 kB gzip).
* [migration] Phase 7: DatePicker rebuild on react-day-picker, drop @mantine/*
Replace the Mantine DatePickerInput with a react-day-picker calendar
composed inside a Base UI Popover. Drop @mantine/core and @mantine/dates
along with their entire transitive ecosystem (@mantine/hooks, dayjs,
react-remove-scroll, etc.). All 25 visual snapshots pass.
- date-picker.tsx: full rewrite. Popover.Root (controlled open) wraps a
custom <button> trigger (preserving the existing CSS classes for
size, font, error states) with sideOffset=4 between trigger and
calendar. Three branches for type='default'|'range'|'multiple' map
onto react-day-picker's discriminated mode prop. Custom Chevron
using @phosphor-icons/react CaretLeft/CaretRight. dayjsTokenToDateFns
helper preserves backwards compatibility for the default
valueFormat='DD/MM/YYYY'.
- types.ts: drop the Mantine prop dependency. New DatePickerProps is
a minimal subset (inputSize, disabled, readOnly, onChange, minDate,
maxDate, id, className, aria-*) covering the Mantine props anyone
in the monorepo actually used. The big "...rest" Mantine surface
(firstDayOfWeek, excludeDate, clearable etc.) is intentionally
removed - confirmed unused by grep across the repo.
- data-picker-mantine.css: rewritten in place against react-day-picker's
DOM (rdp-* classes + data-selected/today/outside/disabled attributes).
Same --ax-public-date-picker-* design tokens. Filename kept because
packages/website/docs/authored/ui/date-picker/date-picker-docs.tsx
literally references this path; an inline comment marks the legacy
name.
- date-picker.module.css: trigger now has display: flex with
justify-content: center / text-align: center so the rendered date
stays centered (matches the Mantine baseline 1:1).
- Drop dependencies: @mantine/core, @mantine/dates. With them gone,
@floating-ui/react/* (their transitive Popper) also leaves
node_modules.
Library bundle delta: 609.78 kB -> 481.99 kB (gzip 160.58 -> 127.55,
-33.03 kB gzip). Cumulative saving versus the pre-D baseline
(628.27 / 165.94 gzip): -146.28 kB raw / -38.39 kB gzip.
Note: the visual snapshot exercises only the closed-trigger state of
the DatePicker. The open calendar layout differs from the Mantine
original (different DOM, intentional design). A separate showcase for
the opened calendar can be added later if we want regression coverage
on the dropdown itself.
* [migration] Phase 8: multi-entry build with subpath exports
Refactor the library from a single ESM bundle into 21 entries (1 barrel
+ 20 per-component) with subpath exports, so consumers can either
import { Button } from '@synergycodes/overflow-ui/button' or keep using
the barrel. Backwards compatible.
- Add per-component src/components/<name>/index.ts re-exporting what
src/index.ts used to pull from individual files. The barrel itself
is rewritten to re-export from these new index files instead of
reaching into component internals.
- vite.config.mts: lib.entry is now a record of 21 entries. Per-entry
output via entryFileNames '[name].js'; shared modules go to
chunks/[name]-[hash].js; CSS goes to assets/. vite-plugin-lib-inject-css
v2 splits CSS per entry, vite-plugin-dts honors entryRoot and emits
one .d.ts tree under dist/components/<name>/.
- package.json:
- main / module / types now point at ./dist/index.js / ./dist/index.d.ts
(renamed from overflow-ui.js).
- exports map: '.' (barrel) plus one subpath per component, plus
'./tokens.css'. Each subpath has its own types path under
./dist/components/<name>/index.d.ts.
- All 25 visual snapshots pass — preview-page still imports from the
barrel and gets the same output.
Bundle measurements with a test consumer (lib-only delta after
subtracting React):
- 1 component (subpath or barrel — both work): ~33 kB JS gzip + ~3 kB CSS
- 5 components (subpath): ~48 kB JS gzip + ~5 kB CSS
- The barrel tree-shakes equally well now, but subpath imports document
intent and protect against bundler regressions.
The library "size" no longer maps to a single number because consumers
only pay for what they import. For comparison, the old monolithic
single-bundle was 481.99 kB raw / 127.55 kB gzip and was paid in full
by every consumer.
* [migration] Phase 9: enter/exit fade animations for Modal/Menu/Select/Tooltip
Restore the soft fade-in/scale-up enter/exit animations that we lost
when migrating off MUI. Base UI exposes [data-starting-style] and
[data-ending-style] data attributes during the open/close transition;
adding CSS transitions targeting those attributes is enough.
- modal.module.css: 200ms opacity + scale(0.96) transition on
.modal-base, opacity transition on .backdrop.
- list-box.module.css: 150ms opacity + scaleY(0.96) transition on
.popup, used by Menu and Select. transform-origin: top so it grows
out of the trigger.
- tooltip.module.css: 120ms opacity + scale(0.96) transition on the
popup container.
Visual regression remains green - playwright runs with
animations: 'disabled', so snapshots capture the steady state and
the new transitions don't affect them. The animations are visible
when actually using the components.
* [migration] Phase 10: externalize @base-ui/react + @phosphor-icons/react as peer deps
Move the two largest dependencies to peerDependencies and externalize
them in the Vite config. Bump to 2.0.0-beta.0 to mark this as a major
release. All 25 visual snapshots pass.
- @base-ui/react and @phosphor-icons/react are now peer dependencies
with broad version ranges (^1.0.0 and ^2.0.0). Consumers must install
them directly.
- vite.config.mts external is now a function that excludes both
packages plus their subpath imports (@base-ui/react/menu,
@phosphor-icons/react/X, etc.).
- README updated with the new install command and a note showing the
per-component subpath import pattern.
- version: 1.0.0-beta.26 -> 2.0.0-beta.0
Bundle delta:
- Total dist/*.js + dist/chunks/*.js raw: ~300 kB (was ~480 kB before
externalization)
- Heaviest single chunk: dist/chunks/date-picker-*.js at 107 kB raw /
26 kB gzip (react-day-picker + date-fns + our wrapper). Without that
chunk, every component is now under 4 kB raw / 2 kB gzip.
The Base UI and Phosphor weight that used to be inlined into the
library bundle is now paid by the consumer once via npm install -
deduplicated across the rest of their app and tree-shaken by their
bundler. For a typical consumer that imports a handful of components,
the total cost (library + peer deps) is meaningfully smaller than the
inlined v1 bundle.
Migration note for v1.x consumers:
pnpm add @base-ui/react @phosphor-icons/react
Then existing imports work unchanged.
* [migration] backwards-compat shims for v1 consumers
After the Phase 8 multi-entry refactor, two file paths that v1
consumers (e.g. workflow-builder via its LOCAL_OVERFLOW_UI dev alias
and overflow-ui-css CSS alias) hard-coded against the old layout
disappeared:
- dist/overflow-ui.js (renamed to dist/index.js)
- dist/index.css (split per-entry into dist/assets/<name>.css)
Add a small post-build plugin to vite.config.mts that recreates both:
- dist/overflow-ui.js: a one-line re-export of dist/index.js, plus a
matching dist/overflow-ui.d.ts. Enough to keep direct path imports
and aliases working.
- dist/index.css: a concatenation of every per-entry CSS asset under
dist/assets/. The new multi-entry build still emits per-entry CSS
for tree-shaking; this combined file exists alongside it for
consumers that want a single stylesheet (no exports map change).
These shims do not change the public API and add zero runtime cost
for new consumers using subpath imports. They simply avoid forcing
existing consumers to update hardcoded paths in their tooling.
* [migration] DatePicker pixel-perfect parity with Mantine
Bring the open-calendar look back to 1:1 parity with the previous Mantine
DatePickerInput. Adds a dedicated visual regression showcase for the
opened state and tightens the CSS so it matches Mantine pixel for pixel.
- preview-page: add ?show=date-picker-open showcase rendering DatePicker
with a default value; the visual test clicks the trigger to capture
the calendar.
- tests/visual: register date-picker-open in the floating-showcases list
so visual regression now covers both the closed trigger and the open
calendar.
- date-picker.tsx: weekStartsOn={1} on every <DayPicker> mode (default,
range, multiple) so the week order matches Mantine (Mo first instead
of Su first).
- data-picker-mantine.css:
- .rdp-month uses CSS Grid (auto 1fr auto / 2 rows) so the previous
button, caption and next button share one row, with the day grid
spanning all columns below. This fixes the previous bug where the
chevrons rendered in the middle of the day grid because of broken
absolute positioning.
- .rdp-month gets a subtle header background (--ax-ui-bg-tertiary, the
Mantine-equivalent gray-300), and each day row (.rdp-week) overrides
that with the popup background so only the caption + weekday rows
look "headed".
- day cells widened (1.875rem -> 2rem) and day buttons widened
(1.375rem -> 1.5rem) to match the Mantine sizing.
Visual regression: 26/26 pass (added the date-picker-open snapshot).
* [migration] tighten @base-ui/react peer range to ^1.4.0
The library is written against Base UI 1.4 APIs (Tooltip delay props,
eventDetails in onOpenChange). A consumer installing 1.0 would satisfy
^1.0.0 but break at runtime.
* [migration] Menu: honest onOpenChange signature (breaking)
Replace the (event, open) callback shape — which casted Base UI's
native event to a React synthetic event just to preserve the v1
signature — with (open: boolean, event?: Event). Consistent with
Tooltip's onOpenChange (open first) and truthfully typed.
No call sites in the repo used the old signature.
* [migration] DatePicker: own design-system styles, drop Mantine emulation
Replace data-picker-mantine.css (global stylesheet emulating the old
Mantine look, .mantine-Popover-dropdown hook included) with calendar
styles in the date-picker CSS module, scoped under a local .calendar
class. Simplifications:
- drop the dead --rdp-* variable block (react-day-picker's stylesheet
is not imported, so those vars affected nothing)
- drop the banded header background and its .rdp-week override hack
(uniform popup background instead); --ax-public-date-picker-header-
background is gone
- range selection uses plain full-cell backgrounds with rounded outer
corners instead of 50% linear-gradient seams
Behavior fixes surfaced by the new interaction tests:
- pass defaultMonth so the calendar opens at the month of the current
value instead of always today's month
- close the popover after a single-mode date pick (range/multiple stay
open for further picks)
date-picker-open.png snapshot updated intentionally; the closed
trigger snapshot is unchanged. Website docs + generated metadata
(css-variables, path-types, props) regenerated.
* [migration] Switch: restore keyboard accessibility (focusable root)
The Base UI Switch root was rendered as a span with display: contents,
which is not focusable in Chromium — keyboard users could not reach or
toggle the switch at all, and the focus ring CSS keyed off
:has(input:focus-visible) never fired because Base UI focuses the root,
not the hidden form input (which it renders as a *sibling* of the root,
outside the styled container).
Make the styled container span the Switch root itself (a real box,
focusable via Base UI's tabindex) with nativeButton={false} so Base UI
attaches Space/Enter handling to it. State selectors move from
:has(input:checked)/:has(input:disabled) to Base UI's [data-checked]/
[data-disabled] attributes in switch, switch-size and icon-switch CSS;
focus ring is now a plain :focus-visible on the root.
Caught by the new Playwright interaction tests; the switch visual
snapshot is byte-identical.
* [migration] add Playwright interaction tests for behavior and a11y
Visual snapshots prove the migration looks right; these prove it
behaves right. New ?show=interactive preview showcase renders stateful,
fully-wired components (nothing forced open) and interactions.spec.ts
drives them by mouse and keyboard:
- Modal: open/close via trigger, Escape (with focus restore), backdrop
click, X and footer buttons; focus trap (no interactive element
outside the dialog reachable while open — Base UI traps via inert,
so sentinel/body stops are allowed)
- Menu: click and full keyboard flow (Enter opens + highlights first
item, arrows, Enter activates), Escape restores trigger focus
- Select: mouse selection and full keyboard flow, value reported via
onChange
- Switch: click toggle and keyboard Space toggle
- Tooltip: opens on hover after delay and on keyboard focus
(focus-visible)
- DatePicker: pick-a-day updates trigger and closes; Escape closes
without changing the value
These tests caught the Switch focusability bug and both DatePicker
behavior gaps fixed in the previous commits.
* [migration] fix CSS layer order in every emitted stylesheet
Only the barrel entry imported src/styles/layers.css, so the
'@layer ui.base, ui.component;' ordering statement was absent from
per-component CSS assets and landed mid-file in the alphabetically
concatenated dist/index.css — after assets had already used both
layers, fixing the order as [ui.component, ui.base] and inverting the
cascade (ui.base rules beat ui.component). The website shadow-dom-css
plugin loads dist/index.css and was directly affected; subpath
consumers could hit the same inversion depending on import order.
Prepend the layer-order statement to every emitted CSS asset in
generateBundle — re-stating an established order is a no-op, so the
repetition is harmless and makes each asset self-sufficient.
* [migration] Menu: correct physical-to-logical offset mapping
Floating UI's crossAxis is a physical offset while Base UI's
alignOffset is logical (it inverts for align='end', matching Floating
UI's alignmentAxis — both proven from the installed @floating-ui/core
source, where alignOffset feeds alignmentAxis and end alignment
negates it). The previous mapping passed crossAxis straight through,
flipping the offset direction on every *-end placement, including the
component default 'bottom-end'. It also gave crossAxis precedence over
alignmentAxis, the reverse of Floating UI.
Negate crossAxis for end alignment, let alignmentAxis win, and drop
the dead trailing branch plus the type-erasing cast.
* [migration] DatePicker: fix selection semantics in all three modes
Found by code review; each fix is pinned by a new interaction test on
new range/multiple interactive showcases.
- single: add `required` so re-clicking the selected day confirms
instead of firing onChange(null) and wiping the value
(react-day-picker deselects by default; Mantine's allowDeselect was
off).
- range: the first click used to report a 'complete' [d, d] range
(addToRange with min=0 sets to=from), contradicting the documented
contract. min={1} makes the first click partial, and an internal
rangeDraft state renders the in-progress highlight that the public
[Date, Date] | null type cannot represent. Completing the range
fires onChange once and closes the popover (Mantine parity);
closing mid-pick abandons the draft.
- multiple: a selection of exactly 2 dates was misclassified by
isDateTuple (any 2-element Date array matches), passing
selected=undefined to DayPicker so the third pick erased the first
two. Guard by mode, not by shape.
* [migration] list items: restore selected/disabled styling via data attributes
list-item.module.css still keyed selected/disabled state on the
MUI-era base--selected/base--disabled classes, which Base UI
Menu.Item/Select.Item never apply — the rules were dead, so a Select
with a chosen value showed no selected highlight and disabled menu
items kept the hover background and pointer cursor.
Key the rules on Base UI's [data-selected]/[data-disabled] instead,
and add [data-highlighted] beside :focus-visible so keyboard
navigation has a visible cursor. The selected rule comes after the
highlight rule so a selected item keeps its selected colors while
highlighted (the outline alone marks the cursor).
The menu-open/select-open showcases now include a disabled item and a
preselected value, so these states are pinned by visual snapshots
(both updated intentionally).
* [migration] Input: restore the error prop and root state styling
The @mui/base Input applied base--error/base--disabled classes to its
root, which input-root.module.css still targets — after the migration
the plain root div never received them, so the rules were dead and
the public `error` prop (previously inherited from InputBaseProps)
disappeared from the API entirely.
Reintroduce error?: boolean on InputProps and apply the state classes
on the root (same convention TextArea already uses), restoring the
error border/background and the root-level disabled color that
adornments inherit. The input showcase gains an error variant so both
states are snapshot-pinned (input.png updated intentionally).
* [migration] Menu/Select: put open/close transitions on the Popup element
The Phase 9 enter/exit transition rules lived under .popup, which
menu.tsx and select.tsx apply to the Base UI *Positioner* — but Base UI
only sets data-starting-style/data-ending-style on the *Popup*
element, so the rules never fired and dropdowns opened with no
animation. The :not([data-open]) display:none rule on the positioner
additionally guaranteed any exit transition would be cut off.
Move the visual box (background, radius, shadow) and the transition
rules to .list-box (the Popup element — the same pattern
Tooltip.Popup/Dialog.Popup already use), leaving the positioner with
only margin and z-index. Verified at runtime: the popup opacity ramps
0 to 1 over 150ms on open.
Note: the exit transition is declared correctly but Base UI 1.4.1's
unmount gating (useAnimationsFinished checking getAnimations() at rAF
time) races with the browser creating the CSS transition at style
recalc — the close is often instant. Forcing a style flush (e.g. any
getComputedStyle call between data-ending-style and the rAF) makes it
animate, which confirms the race is in Base UI, not in this CSS.
Snapshots are unaffected (the suite runs with animations disabled).
* [migration] Tooltip: controlled-mode hover parity + composing trigger props
Two regressions vs the Floating UI implementation, both pinned by new
interaction tests:
- Controlled tooltips reacted to hover/focus: the old hook passed
enabled: controlledOpen == null to useHover/useFocus, so a parent
controlling `open` had sole authority (dismissal stayed active).
Base UI routes every interaction through onOpenChange, so the
wrapper now filters the trigger-hover/trigger-focus/focus-out
reasons when `open` is controlled — Escape still propagates.
- TooltipTrigger spread child/user props AFTER Base UI's trigger
props, so a child with its own onMouseEnter/onFocus silently
replaced the tooltip's interaction handlers (the old code composed
via getReferenceProps). Use Base UI's mergeProps, which composes
event handlers and merges className/style.
The interactive showcase gains a controlled-tooltip section and the
asChild trigger records its own hover, so both behaviors are covered:
hover must both fire the child handler and open the tooltip; a
controlled tooltip must ignore a 900ms hover yet follow its open prop
and close on Escape.
* [migration] regenerate website prop docs (Input error prop)
* fix(build): externalize react-textarea-autosize in Vite config
The browser variant of react-textarea-autosize has module-level
document access (IE detection, hidden textarea creation) that crashes
Docusaurus SSG. Externalizing it lets each consumer's bundler resolve
the right variant for its target environment.
* remove internal research docs from the repo
* remove migration planning doc from the repo
* move @phosphor-icons/react from peer to bundled dependency
Phosphor icons are used internally (CaretDown, X, Check, etc.) -
implementation details, not public API. Consumers should not be
forced to install them.
* fix(ui): consistent CSS layers, handle positioning, and PR cleanup
- Wrap 7 component CSS files in @layer ui.component (avatar, checkbox,
collapsible, date-picker, radio-button, snackbar, tooltip)
- Fix top/bottom handle positioning to center on node border
- Cap docs preview container at max-width: 50rem
- Remove visual test snapshots, specs, and playwright config from PR
* refactor(ui): replace explicit subpath exports with wildcard pattern
Reduces 80 lines of per-component export entries to a single
"./*" pattern. New components added to vite.config.mts are
automatically resolvable without touching package.json.
* chore: update lockfile after removing @playwright/test
* fix(ui): clean up preview page and fix missing CSS in date-picker/tooltip
- Remove test infrastructure from preview page (prefers-reduced-motion
hack, forced-open showcases, InteractiveShowcase, data-testid attrs,
?show= routing)
- Convert all px values to rem in preview-page CSS
- Add missing .calendar, .trigger-label, .container--placeholder CSS
classes in date-picker
- Add missing variables.css import in date-picker
- Remove dead arrow class references in tooltip-content
* fix(ui): default Button type to "button" to prevent implicit form submit
A plain <button> inside a <form> defaults to type="submit", so DS buttons
would submit the form on click. The old @mui/base Button defaulted to
type="button"; restore that default while keeping it overridable via props.
* fix(ui): parse date-only DatePicker strings as local time
new Date("2026-05-05") parses as UTC midnight and renders one day earlier in
negative-UTC-offset timezones. Detect the YYYY-MM-DD shape and build a local
date instead; other formats keep the native parser.
* feat(ui): expose styles.css entry for subpath consumers
Per-component "./*" imports inject only that component CSS, not the layer
order / globals / typography that the barrel pulls in via src/index.ts. Emit a
standalone dist/styles.css (layer order first) and add the ./styles.css export.
* docs(ui): add 2.0 CHANGELOG and correct README to Base UI
Document the v1->v2 breaking changes (no changesets tooling in the repo),
document the new styles.css import, and fix the stale README note that still
claimed the library stays on MUI Base.
* refactor(ui): derive Menu side/align from Base UI types
Drop the hand-redefined BaseUiSide/BaseUiAlign unions in favour of types
derived from MenuBase.Positioner props, so they track Base UI instead of
drifting. Extract the OffsetAxes alias and remove the inline offset comment.
* fix(ui): type Switch onChange event as the native Event
Base UI hands onCheckedChange a native DOM Event, never a React
ChangeEvent<HTMLInputElement>, so the old type was wrong and forced an
"as unknown as" cast. Type it honestly and drop the cast. Move the thumb
display:contents to a CSS class and remove narration comments.
* refactor(ui): drive Tooltip arrow and variants from CSS
Replace the inline SVG arrow with a CSS clip-path arrow and select the
color variant via a data-tooltip-type attribute instead of conditional
classes in the markup. Express TooltipPlacement as a template-literal type
and remove migration-narration comments.
* refactor(ui): drop migration-narration comment from Input
* refactor(ui): extract combine-css-bundle plugin from vite config
Move the post-build CSS plugin into its own module with named helpers, name
the config helper functions, and move the CSS @layer-order rationale into
docs/css-layers.md so the config stays clean.
* docs(ui): recommend barrel import, note subpath as an option
* refactor(ui): move css-layers doc to package root
Keep the doc directly in packages/ui instead of a separate docs/ folder.
* refactor(ui): update css-layers doc reference after move
* refactor(ui): establish CSS layer order via styles.css contract
Drop the per-asset @layer prepend. Per-component stylesheets no longer
repeat the layer declaration; the order is established by importing
styles.css first (or the barrel, which imports it). index.css keeps the
declaration since it is consumed standalone. Docs updated accordingly.
* chore(ui): set version to 1.0.0-beta.28
Keep the package on the 1.0.0 beta line instead of jumping to 2.0; align
the CHANGELOG heading and README note accordingly.1 parent 1928c2d commit e92f5d4
76 files changed
Lines changed: 2925 additions & 1895 deletions
File tree
- packages
- ui
- preview-page
- src
- components
- accordion
- avatar
- button
- base-button
- checkbox
- collapsible
- date-picker
- edge
- input
- modal
- node
- node-panel
- radio-button
- segment-picker
- select
- select-button
- select-option
- select-value
- separator
- snackbar
- status
- switch
- icon-switch
- text-area
- tooltip
- website
- docs/authored/ui/date-picker
- generated
- src/components/component-utils/preview
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
4 | 6 | | |
5 | 7 | | |
6 | 8 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
| 48 | + | |
47 | 49 | | |
48 | | - | |
| 50 | + | |
49 | 51 | | |
50 | 52 | | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
| 57 | + | |
| 58 | + | |
55 | 59 | | |
56 | 60 | | |
57 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
34 | 36 | | |
35 | 37 | | |
36 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
37 | 45 | | |
38 | 46 | | |
39 | 47 | | |
40 | 48 | | |
41 | 49 | | |
42 | | - | |
| 50 | + | |
43 | 51 | | |
44 | 52 | | |
45 | 53 | | |
| |||
88 | 96 | | |
89 | 97 | | |
90 | 98 | | |
91 | | - | |
92 | 99 | | |
93 | 100 | | |
94 | 101 | | |
| |||
98 | 105 | | |
99 | 106 | | |
100 | 107 | | |
| 108 | + | |
101 | 109 | | |
102 | 110 | | |
103 | 111 | | |
| |||
129 | 137 | | |
130 | 138 | | |
131 | 139 | | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
| 140 | + | |
141 | 141 | | |
142 | | - | |
143 | | - | |
| 142 | + | |
144 | 143 | | |
145 | | - | |
| 144 | + | |
146 | 145 | | |
147 | 146 | | |
148 | 147 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | | - | |
| 38 | + | |
| 39 | + | |
38 | 40 | | |
39 | | - | |
| 41 | + | |
40 | 42 | | |
41 | 43 | | |
42 | 44 | | |
43 | | - | |
| 45 | + | |
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
47 | 49 | | |
48 | | - | |
| 50 | + | |
49 | 51 | | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
50 | 57 | | |
51 | 58 | | |
52 | 59 | | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
61 | 65 | | |
62 | 66 | | |
63 | | - | |
64 | 67 | | |
65 | 68 | | |
66 | 69 | | |
| |||
70 | 73 | | |
71 | 74 | | |
72 | 75 | | |
| 76 | + | |
73 | 77 | | |
74 | 78 | | |
75 | 79 | | |
76 | 80 | | |
77 | 81 | | |
78 | 82 | | |
79 | 83 | | |
| 84 | + | |
80 | 85 | | |
81 | 86 | | |
82 | 87 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | 3 | | |
5 | 4 | | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
| 5 | + | |
0 commit comments