Skip to content

Commit e92f5d4

Browse files
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ packages/website/docs/generated-docs/*
2222
packages/website/.docusaurus
2323
packages/website/.cache-loader
2424
packages/website/static/css/*
25+
26+
# Playwright
27+
packages/ui/playwright-report
28+
packages/ui/test-results
29+
.playwright-mcp

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Add files here to ignore them from prettier formatting
22
/dist
33
/coverage
4+
.claude/worktrees
5+
.claude/launch.json
46

57
packages/tokens/tokens.json
68
packages/tokens/tokens.json

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ Developed and maintained by **[Synergy Codes](https://www.synergycodes.com/)**.
88

99
### 📦 Installation
1010

11-
Use one of the commands below to add **Overflow UI** to your project:
11+
Overflow UI relies on `@base-ui/react` (headless component primitives) and `@phosphor-icons/react` (icon set) as peer dependencies. Install all three at once:
1212

1313
```bash
14-
npm install @synergycodes/overflow-ui
14+
npm install @synergycodes/overflow-ui @base-ui/react @phosphor-icons/react
1515
```
1616

1717
```bash
18-
pnpm add @synergycodes/overflow-ui
18+
pnpm add @synergycodes/overflow-ui @base-ui/react @phosphor-icons/react
1919
```
2020

2121
```bash
22-
yarn add @synergycodes/overflow-ui
22+
yarn add @synergycodes/overflow-ui @base-ui/react @phosphor-icons/react
2323
```
2424

2525
### 🎨 Import styles
@@ -44,14 +44,18 @@ To make the styles use proper variables, include `data-theme` (`light` or `dark`
4444

4545
### 🎛️ Use components
4646

47+
The library has a per-component subpath export, so you can import only what you use and let your bundler tree-shake the rest:
48+
4749
```tsx
48-
import { Input } from '@synergycodes/overflow-ui';
50+
import { Input } from '@synergycodes/overflow-ui/input';
4951

5052
//
5153

5254
<Input value={value} onChange={onChange} />;
5355
```
5456

57+
The barrel import (`from '@synergycodes/overflow-ui'`) is also supported and tree-shakes equally well in modern bundlers.
58+
5559
### Customization
5660

5761
Each Overflow UI component uses CSS variables that are derived from primitive values.

packages/ui/CHANGELOG.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Changelog
2+
3+
All notable changes to `@synergycodes/overflow-ui` are documented in this file.
4+
5+
## 1.0.0-beta.28
6+
7+
This beta rebuilds the library on [Base UI](https://base-ui.com/) and removes
8+
the previous MUI / Mantine / Emotion / Floating UI stack. It contains breaking
9+
changes relative to `1.0.0-beta.27`; read the sections below before upgrading.
10+
11+
### Breaking changes
12+
13+
#### Dependencies
14+
15+
- Removed `@mui/material`, `@mui/base`, `@mantine/core`, `@mantine/dates`,
16+
`@emotion/*`, and `@floating-ui/react`.
17+
- `@base-ui/react` (`^1.4.0`) is now a **peer dependency** - consumers must
18+
install it alongside this package.
19+
- `DatePicker` is rebuilt on `react-day-picker` + `date-fns`; `TextArea` on
20+
`react-textarea-autosize`. `date-fns`, `react-day-picker`, and `clsx` are
21+
bundled into the package output.
22+
23+
#### Packaging
24+
25+
- The build moved from a single bundle to a **multi-entry build** with a
26+
per-component subpath export: `@synergycodes/overflow-ui/<component>` (e.g.
27+
`@synergycodes/overflow-ui/date-picker`).
28+
- Importing from the package root still injects all required styles. Importing
29+
per-component entries injects only that component's CSS, so add
30+
`@synergycodes/overflow-ui/styles.css` once for the global layer order,
31+
reset, and typography.
32+
- The old `overflow-ui.js` bundle filename is retained as a compatibility shim
33+
that re-exports the new `index.js` entry, so hard-coded paths keep resolving.
34+
35+
#### Component API
36+
37+
- **DatePicker**: the prop surface no longer forwards the full Mantine prop set.
38+
It now accepts a curated list: `value`, `defaultValue`, `type`,
39+
`valueFormat`, `placeholder`, `error`, `size`, `disabled`, `minDate`,
40+
`maxDate`, `onChange`, `id`, `className`, `aria-label`, `aria-labelledby`.
41+
- `valueFormat` uses `date-fns` tokens (e.g. `dd/MM/yyyy`). The legacy
42+
`DD/MM/YYYY` (dayjs) default is accepted and converted for compatibility.
43+
- In `range` mode, `onChange` fires `null` while a range is mid-selection and
44+
emits the completed `[from, to]` tuple once both ends are picked.
45+
- **Menu**: `onOpenChange` signature is now `(open: boolean, event?: Event)`.
46+
The MUI `slotProps` / passthrough surface is no longer available.
47+
- **Select**: `onChange` signature is `(event, value)` - the event is the first
48+
argument, the selected value the second.
49+
- **Switch**: `onChange` is `(checked: boolean, event: Event)`. The second
50+
argument is the native DOM event (previously typed as a React
51+
`ChangeEvent<HTMLInputElement>`, which never matched the value passed at
52+
runtime).
53+
- Transitions moved to the popup element, where Base UI sets
54+
`data-starting-style` / `data-ending-style`.
55+
56+
### Added
57+
58+
- `@synergycodes/overflow-ui/styles.css` - standalone global stylesheet (layer
59+
order, reset, typography) for per-component / subpath consumers.
60+
- `Input` gains a typed `error` prop wired to the error state.
61+
- `Snackbar` exposes proper `role="status"` / `aria-live` semantics.

packages/ui/README.md

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ yarn add @synergycodes/overflow-ui
2424

2525
### 🎨 Import styles
2626

27-
Add to your style sheet or component:
27+
Import components from the package root (the recommended default). Their styles,
28+
including the global layer order, reset, and typography, are injected
29+
automatically, so you only need to add the design tokens:
2830

2931
```css
3032
@import '@synergycodes/overflow-ui/tokens.css';
@@ -34,12 +36,18 @@ Add to your style sheet or component:
3436
import '@synergycodes/overflow-ui/tokens.css';
3537
```
3638

39+
> **Subpath imports.** You can also import a single component directly, e.g.
40+
> `import { DatePicker } from '@synergycodes/overflow-ui/date-picker'`. That
41+
> injects only the component's own CSS, so import the global stylesheet once and
42+
> **before any component**, so the cascade layers are ordered correctly:
43+
> `import '@synergycodes/overflow-ui/styles.css'`.
44+
3745
### 🎛️ Apply the Theme
3846

3947
To make the styles use proper variables, include data-theme (light or dark) attribute in <html>:
4048

4149
```html
42-
<html data-theme="light">
50+
<html data-theme="light"></html>
4351
```
4452

4553
### 🧱 Use components
@@ -88,7 +96,6 @@ or a derived value used by the selected component:
8896

8997
Overflow UI uses [CSS layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) to separate its styles from yours. By default, CSS styles outside of any layer take precedence over what Overflow UI defines, so your styles will always win the specificity war. You can customize Overflow UI components with simple `input {}`.
9098

91-
9299
```css
93100
@layer ui.component {
94101
.separator {
@@ -98,6 +105,7 @@ Overflow UI uses [CSS layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@
98105
```
99106

100107
Default Overflow UI order:
108+
101109
```css
102110
@layer ui.base, ui.component;
103111
```
@@ -129,20 +137,11 @@ Edit `ui/preview-page/preview-page.tsx` to display desired components.
129137

130138
### 📣 Important Note on Underlying Technology
131139

132-
> **Overflow UI is built on top of [MUI Base](https://v6.mui.com/base-ui/getting-started/), a headless component library that focuses on accessibility and logic, while leaving the styling up to us.**
133-
>
134-
> Thanks to MUI Base, Overflow UI provides components that are **accessible by default** and **fully customizable** through our design tokens.
135-
>
136-
> We are aware that **MUI Base has been deprecated**, and the MUI team recommends migrating to [Base UI](https://base-ui.com).
137-
> However, after careful evaluation, we've chosen to **stay with MUI Base** for now because:
138-
>
139-
> ***Base UI is not yet mature enough** for our needs.
140-
> * ✅ We want to ensure a stable, well-tested experience for Overflow UI users.
140+
> **Overflow UI is built on top of [Base UI](https://base-ui.com), a headless component library that focuses on accessibility and logic, while leaving the styling up to us.**
141141
>
142-
> This is a **conscious and informed decision**.
143-
> We will continue to monitor Base UI's progress and will consider migrating when we feel it's the right time, ensuring a smooth and thoughtful transition for Overflow UI users.
142+
> Thanks to Base UI, Overflow UI provides components that are **accessible by default** and **fully customizable** through our design tokens.
144143
>
145-
> If you have any questions or concerns, feel free to reach out — we’re happy to share our reasoning and plans in more detail!
144+
> Earlier `1.0.0` betas were built on the now-deprecated [MUI Base](https://v6.mui.com/base-ui/getting-started/). From `1.0.0-beta.28` the library is built on Base UI; see [CHANGELOG.md](./CHANGELOG.md) for the full list of changes.
146145
147146
## Showcase
148147

packages/ui/combine-css-bundle.mts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import fs from 'node:fs';
2+
import { resolve } from 'node:path';
3+
import type { Plugin } from 'vite';
4+
5+
/**
6+
* Post-build CSS steps for the multi-entry library bundle. See css-layers.md.
7+
*
8+
* - emits `index.css` (all component styles, prefixed with the @layer order)
9+
* and `styles.css` (the global layer order, reset and typography);
10+
* - emits an `overflow-ui.js` shim re-exporting the renamed `index.js` entry.
11+
*
12+
* Per-component stylesheets do not carry the @layer declaration; consumers
13+
* establish the order by importing `styles.css` first (or the barrel).
14+
*/
15+
export function combineCssBundle(rootDir: string): Plugin {
16+
const distDir = resolve(rootDir, 'dist');
17+
const stylesDir = resolve(rootDir, 'src/styles');
18+
19+
return {
20+
name: 'overflow-ui:combine-css-bundle',
21+
apply: 'build',
22+
closeBundle() {
23+
writeCombinedStylesheet(distDir, stylesDir);
24+
writeGlobalStylesheet(distDir, stylesDir);
25+
writeLegacyEntryShim(distDir);
26+
},
27+
};
28+
}
29+
30+
function readLayerOrder(stylesDir: string): string {
31+
return fs.readFileSync(resolve(stylesDir, 'layers.css'), 'utf-8').trim();
32+
}
33+
34+
function writeCombinedStylesheet(distDir: string, stylesDir: string) {
35+
const assetsDir = resolve(distDir, 'assets');
36+
if (!fs.existsSync(assetsDir)) return;
37+
38+
const styles = fs
39+
.readdirSync(assetsDir)
40+
.filter((file) => file.endsWith('.css'))
41+
.sort()
42+
.map((file) => fs.readFileSync(resolve(assetsDir, file), 'utf-8'))
43+
.join('\n');
44+
45+
// index.css is consumed standalone, so it declares the @layer order itself.
46+
const combined = `${readLayerOrder(stylesDir)}\n${styles}`;
47+
fs.writeFileSync(resolve(distDir, 'index.css'), combined);
48+
}
49+
50+
function writeGlobalStylesheet(distDir: string, stylesDir: string) {
51+
const globals = ['layers.css', 'globals.css', 'typography.css']
52+
.map((file) => fs.readFileSync(resolve(stylesDir, file), 'utf-8'))
53+
.join('\n');
54+
55+
fs.writeFileSync(resolve(distDir, 'styles.css'), globals);
56+
}
57+
58+
function writeLegacyEntryShim(distDir: string) {
59+
fs.writeFileSync(
60+
resolve(distDir, 'overflow-ui.js'),
61+
`export * from './index.js';\n`,
62+
);
63+
fs.writeFileSync(
64+
resolve(distDir, 'overflow-ui.d.ts'),
65+
`export * from './index';\n`,
66+
);
67+
}

packages/ui/css-layers.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# CSS layers
2+
3+
Overflow UI emits all of its styles into two ordered cascade layers:
4+
5+
```css
6+
@layer ui.base, ui.component;
7+
```
8+
9+
`ui.base` holds resets and primitives; `ui.component` holds component styles.
10+
Declaring the order once, before any rule from either layer, guarantees that
11+
`ui.component` always wins over `ui.base`, and that unlayered consumer styles
12+
win over both.
13+
14+
## Establishing the order
15+
16+
The order is fixed by the **first** `@layer` declaration the browser sees, so
17+
the declaration must load before any component rule. The declaration lives in
18+
`styles.css` (and in the package barrel, which imports it first), so consumers
19+
establish it by either:
20+
21+
- importing from the package root - the barrel imports the declaration first; or
22+
- importing `@synergycodes/overflow-ui/styles.css` **before** any component when
23+
using per-component subpath imports.
24+
25+
Per-component stylesheets deliberately do **not** repeat the declaration. If one
26+
loads before `styles.css`, the first use of a layer fixes the order: a
27+
`ui.component` rule seen before any `ui.base` rule locks it as
28+
`[ui.component, ui.base]`, inverting the cascade. Importing `styles.css` first
29+
avoids this.
30+
31+
The combined `index.css` (consumed standalone, e.g. by Workflow Builder) carries
32+
the declaration at its top for the same reason.

packages/ui/package.json

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@synergycodes/overflow-ui",
33
"type": "module",
4-
"version": "1.0.0-beta.27",
4+
"version": "1.0.0-beta.28",
55
"description": "A React library for creating node-based UIs and diagram-driven applications. Perfect for React Flow users, providing ready-to-use node templates and components that work seamlessly with React Flow's ecosystem.",
66
"keywords": [
77
"react",
@@ -32,35 +32,38 @@
3232
"dev": "vite build --watch",
3333
"check:built-css": "tsx ./scripts/check-built-css.ts",
3434
"preview": "vite --config preview-page/vite.preview.config.ts",
35+
"preview:build": "vite build --config preview-page/vite.preview.config.ts",
3536
"lint": "eslint",
3637
"lint:fix": "eslint --fix",
37-
"typecheck": "tsc --noEmit --pretty"
38+
"typecheck": "tsc --noEmit --pretty",
39+
"build:stats": "BUNDLE_STATS=1 vite build"
3840
},
39-
"module": "./dist/overflow-ui.js",
41+
"module": "./dist/index.js",
4042
"files": [
4143
"dist"
4244
],
43-
"main": "./dist/overflow-ui.js",
45+
"main": "./dist/index.js",
4446
"types": "./dist/index.d.ts",
4547
"exports": {
4648
".": {
4749
"types": "./dist/index.d.ts",
48-
"import": "./dist/overflow-ui.js"
50+
"import": "./dist/index.js"
4951
},
52+
"./*": {
53+
"types": "./dist/components/*/index.d.ts",
54+
"import": "./dist/*.js"
55+
},
56+
"./styles.css": "./dist/styles.css",
5057
"./tokens.css": "./dist/tokens.css"
5158
},
5259
"dependencies": {
53-
"@emotion/styled": "^11.14.0",
54-
"@floating-ui/react": "^0.26.28",
55-
"@mantine/core": "^7.17.2",
56-
"@mantine/dates": "^7.17.2",
57-
"@mui/base": "5.0.0-beta.62",
58-
"@mui/material": "^6.4.7",
59-
"@phosphor-icons/react": "^2.1.7",
60-
"clsx": "^2.0.0"
60+
"@phosphor-icons/react": "^2.0.0",
61+
"clsx": "^2.0.0",
62+
"date-fns": "^4.1.0",
63+
"react-day-picker": "^9.14.0",
64+
"react-textarea-autosize": "^8.5.6"
6165
},
6266
"devDependencies": {
63-
"@emotion/react": "^11.14.0",
6467
"@synergycodes/overflow-ui-tokens": "workspace:*",
6568
"@types/react": "^19.1.8",
6669
"@types/react-dom": "^19.1.6",
@@ -70,13 +73,15 @@
7073
"postcss": "^8.5.3",
7174
"react": "^18.3.1",
7275
"react-dom": "^18.3.1",
76+
"rollup-plugin-visualizer": "^7.0.1",
7377
"typescript": "^5.6.3",
7478
"vite": "^6.2.2",
7579
"vite-plugin-dts": "^4.5.3",
7680
"vite-plugin-lib-inject-css": "^2.2.1",
7781
"vite-plugin-static-copy": "^2.3.1"
7882
},
7983
"peerDependencies": {
84+
"@base-ui/react": "^1.4.0",
8085
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
8186
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
8287
},

packages/ui/preview-page/main.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,5 @@
11
import { createRoot } from 'react-dom/client';
22
import { PreviewPage } from './preview-page';
3-
import styles from './preview-page.module.css';
43

54
const root = createRoot(document.querySelector('#root') as HTMLElement);
6-
7-
root.render(
8-
<PreviewWrapper>
9-
<PreviewPage />
10-
</PreviewWrapper>,
11-
);
12-
13-
function PreviewWrapper({ children }: { children: React.ReactNode }) {
14-
return (
15-
<div className={styles['preview-container']}>
16-
<div className={styles['preview-header']}>Components Testing</div>
17-
<div className={styles['preview-content']}>{children}</div>
18-
</div>
19-
);
20-
}
5+
root.render(<PreviewPage />);

0 commit comments

Comments
 (0)