* fix(coss-ui): convert /settings/my-account/general to coss-ui * update guide * fix: add menuPosition fixed to TimezoneSelect in TravelScheduleModal Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: use menuPortalTarget to render TimezoneSelect dropdown above dialog footer Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: use Object.assign for type-safe styles in TimezoneSelect Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * revert comment * fix: guard document access for SSR in TravelScheduleModal Addresses Cubic AI review feedback: menuPortalTarget={document.body} will throw during server rendering where document is undefined. Added typeof check to guard against SSR. Co-Authored-By: unknown <> * change dialog backdrop from blur to dim * fix: add menuPlacement auto to open dropdown upward on mobile Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: use menuPlacement top on mobile for TimezoneSelect dropdown Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: make timezone button stack vertically on mobile to prevent overflow Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: make timezone select and button 50/50 width on larger viewports Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix date range picker issue on mobile * prevent travel schedule dialog dismissal with date picker --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
4.8 KiB
title, description
| title | description |
|---|---|
| `@coss/ui` Migration Guide | Entry point for migrating to `@coss/ui` (Base UI), with troubleshooting, dos and don'ts, and links to component-level guides. |
Start Here
This is the entry point for migrating to @coss/ui. It covers shared guidance, pitfalls, and troubleshooting. For component-by-component details and Radix/shadcn patterns, use the general guide:
Quick Checklist
- Identify all
@calcom/uiusages in the scope of the migration. - Replace Radix
asChildwith Base UIrenderwhere needed. - Update naming differences (
*Content→*Popup/*Panel). - Validate dialog, menu, and popover behavior (portals and focus).
- Run type checks and targeted UI smoke tests.
Do and Don't
Do
- Prefer Base UI mental models:
renderfor composability,*Popup/*Panelfor content. - Use direct component imports instead of barrel imports.
- Keep diffs small and scoped to a single feature or page.
- When converting a shared component, audit other pages/components that consume it and either migrate them in the same PR or explicitly test them.
Don't
- Don’t keep lazy-loaded components inside Base UI dialogs without verifying render behavior.
- Don’t rely on Radix-specific props (
asChild,type="single",collapsible) without mapping them. - Don’t assume API parity; check the component guide first.
- Don’t over-migrate in a single change; prioritize targeted conversions and verification.
Knowledge Base
Base UI vs Radix UI mental model
@coss/ui is built on Base UI from the ground up. Many component names are similar, but the APIs are not drop-in compatible. Base UI favors render props for composition and often exposes *Popup or *Panel instead of *Content.
Render vs asChild
Radix’s asChild becomes Base UI’s render. This affects triggers, close buttons, and other slots that used to render children directly.
Naming conventions
Common renames to expect:
DialogContent→DialogPopupAlertDialogContent→AlertDialogPopupAccordionContent→AccordionPanel
Legacy names may remain for compatibility, but new usage should follow Base UI naming.
Troubleshooting
Base UI Dialog closes when a Radix Popover is opened
Symptom: A Base UI Dialog closes when you open a Radix Popover inside it and then click anywhere in the dialog. This is most visible in Chrome.
Root cause: Radix popovers render in a portal outside the dialog DOM tree. Base UI's dialog uses outside-interaction detection to dismiss itself. In Chrome, focus and pointer events from the popover portal can be interpreted as outside presses, so the dialog closes even though the user is still interacting with the dialog.
Solution: Disable pointer dismissal while the Radix popover is open or migrate the popover to Base UI. For the travel schedule modal, we track the Radix popover open state and pass disablePointerDismissal to the dialog while the calendar is open.
// apps/web/components/settings/TravelScheduleModal.tsx
const [isDateRangeOpen, setIsDateRangeOpen] = useState(false);
<Dialog open={open} disablePointerDismissal={isDateRangeOpen} onOpenChange={onOpenChange}>
<DateRangePicker onPopoverOpenChange={setIsDateRangeOpen} />
</Dialog>
Infinite loop with lazy-loaded components inside Base UI Dialog
Symptom: When opening a coss-ui Dialog (Base UI) containing a lazy-loaded component (via next/dynamic), an infinite render loop occurs:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
Root cause: Components wrapped in next/dynamic have an async mounting lifecycle. When combined with Base UI Dialog's portal mounting, this creates a conflict that triggers infinite re-renders.
For example, DateRangePicker from @calcom/ui/components/form is lazy-loaded:
// packages/ui/components/form/date-range-picker/index.ts
export const DateRangePickerLazy = dynamic(() =>
import("./DateRangePicker").then((mod) => mod.DatePickerWithRange)
);
Solution: Import the component directly without the lazy loading wrapper. Add a direct export path to the package.json if needed:
// packages/ui/package.json - add direct export
"./components/form/date-range-picker/DateRangePicker": "./components/form/date-range-picker/DateRangePicker.tsx"
Then import directly:
// Instead of:
import { DateRangePicker } from "@calcom/ui/components/form";
// Use:
import { DatePickerWithRange as DateRangePicker } from "@calcom/ui/components/form/date-range-picker/DateRangePicker";
Note: Regular Radix components (like SettingsToggle, DatePicker, etc.) work fine inside Base UI Dialog as long as they're not lazy-loaded.