fix(coss-ui): convert /settings/my-account/general to coss-ui (#26053)
* 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>
This commit is contained in:
co-authored by
unknown <>
eunjae@cal.com <hey@eunjae.dev>
eunjae@cal.com <hey@eunjae.dev>
eunjae@cal.com <hey@eunjae.dev>
eunjae@cal.com <hey@eunjae.dev>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
052e99f7ba
commit
25fa46fd34
@@ -1,20 +1,29 @@
|
||||
import { useState } from "react";
|
||||
import type { UseFormSetValue } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { TimezoneSelect } from "@calcom/features/components/timezone-select";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/dialog";
|
||||
import { Label, SettingsToggle, DateRangePicker, DatePicker } from "@calcom/ui/components/form";
|
||||
|
||||
import { DatePicker, SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { DatePickerWithRange as DateRangePicker } from "@calcom/ui/components/form/date-range-picker/DateRangePicker";
|
||||
import { Button } from "@coss/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogPanel,
|
||||
DialogPopup,
|
||||
DialogTitle,
|
||||
} from "@coss/ui/components/dialog";
|
||||
import { Label } from "@coss/ui/components/label";
|
||||
import { useIsMobile } from "@coss/ui/hooks/use-mobile";
|
||||
import { useState } from "react";
|
||||
import type { UseFormSetValue } from "react-hook-form";
|
||||
import type { FormValues } from "~/settings/my-account/general-view";
|
||||
|
||||
interface TravelScheduleModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
setValue: UseFormSetValue<FormValues>;
|
||||
existingSchedules: FormValues["travelSchedules"];
|
||||
}
|
||||
@@ -27,12 +36,14 @@ const TravelScheduleModal = ({
|
||||
}: TravelScheduleModalProps) => {
|
||||
const { t } = useLocale();
|
||||
const { timezone: preferredTimezone } = useTimePreferences();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(preferredTimezone);
|
||||
const [isNoEndDate, setIsNoEndDate] = useState(false);
|
||||
const [isDateRangeOpen, setIsDateRangeOpen] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const isOverlapping = (newSchedule: { startDate: Date; endDate?: Date }) => {
|
||||
@@ -64,6 +75,7 @@ const TravelScheduleModal = ({
|
||||
setEndDate(new Date());
|
||||
setSelectedTimeZone(preferredTimezone);
|
||||
setIsNoEndDate(false);
|
||||
setIsDateRangeOpen(false);
|
||||
};
|
||||
|
||||
const createNewSchedule = () => {
|
||||
@@ -75,7 +87,7 @@ const TravelScheduleModal = ({
|
||||
|
||||
if (!isOverlapping(newSchedule)) {
|
||||
setValue("travelSchedules", existingSchedules.concat(newSchedule), { shouldDirty: true });
|
||||
onOpenChange();
|
||||
onOpenChange(false);
|
||||
resetValues();
|
||||
} else {
|
||||
setErrorMessage(t("overlaps_with_existing_schedule"));
|
||||
@@ -83,65 +95,86 @@ const TravelScheduleModal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
title={t("travel_schedule")}
|
||||
description={t("travel_schedule_description")}
|
||||
type="creation">
|
||||
<div>
|
||||
{!isNoEndDate ? (
|
||||
<>
|
||||
<Label className="mt-2">{t("time_range")}</Label>
|
||||
<DateRangePicker
|
||||
dates={{
|
||||
startDate,
|
||||
endDate,
|
||||
}}
|
||||
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
|
||||
// If newStartDate does become undefined - we resort back to to-todays date
|
||||
setStartDate(newStartDate ?? new Date());
|
||||
setEndDate(newEndDate);
|
||||
<Dialog
|
||||
open={open}
|
||||
disablePointerDismissal={isDateRangeOpen}
|
||||
onOpenChange={(nextOpen) => {
|
||||
onOpenChange(nextOpen);
|
||||
if (!nextOpen) {
|
||||
setIsDateRangeOpen(false);
|
||||
}
|
||||
}}>
|
||||
<DialogPopup>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("travel_schedule")}</DialogTitle>
|
||||
<DialogDescription>{t("travel_schedule_description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogPanel>
|
||||
<div>
|
||||
{!isNoEndDate ? (
|
||||
<>
|
||||
<Label className="mt-2">{t("time_range")}</Label>
|
||||
<DateRangePicker
|
||||
dates={{
|
||||
startDate,
|
||||
endDate,
|
||||
}}
|
||||
popoverModal={isMobile}
|
||||
onPopoverOpenChange={setIsDateRangeOpen}
|
||||
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
|
||||
// If newStartDate does become undefined - we resort back to to-todays date
|
||||
setStartDate(newStartDate ?? new Date());
|
||||
setEndDate(newEndDate);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Label className="mt-2">{t("date")}</Label>
|
||||
<DatePicker
|
||||
minDate={new Date()}
|
||||
date={startDate}
|
||||
className="w-56"
|
||||
onDatesChange={(newDate) => {
|
||||
setStartDate(newDate);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="text-error mt-1 text-sm">{errorMessage}</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<SettingsToggle
|
||||
labelClassName="mt-1 font-normal"
|
||||
title={t("schedule_tz_without_end_date")}
|
||||
checked={isNoEndDate}
|
||||
onCheckedChange={(checked) => {
|
||||
setEndDate(!checked ? startDate : undefined);
|
||||
setIsNoEndDate(checked);
|
||||
if (checked) {
|
||||
setIsDateRangeOpen(false);
|
||||
}
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Label className="mt-2">{t("date")}</Label>
|
||||
<DatePicker
|
||||
minDate={new Date()}
|
||||
date={startDate}
|
||||
className="w-56"
|
||||
onDatesChange={(newDate) => {
|
||||
setStartDate(newDate);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="text-error mt-1 text-sm">{errorMessage}</div>
|
||||
<div className="mt-3">
|
||||
<SettingsToggle
|
||||
labelClassName="mt-1 font-normal"
|
||||
title={t("schedule_tz_without_end_date")}
|
||||
checked={isNoEndDate}
|
||||
onCheckedChange={(e) => {
|
||||
setEndDate(!e ? startDate : undefined);
|
||||
setIsNoEndDate(e);
|
||||
setErrorMessage("");
|
||||
}}
|
||||
</div>
|
||||
|
||||
<Label className="mt-6">{t("timezone")}</Label>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={({ value }) => setSelectedTimeZone(value)}
|
||||
menuPortalTarget={typeof document === "undefined" ? undefined : document.body}
|
||||
menuPlacement={isMobile ? "top" : "auto"}
|
||||
styles={{ menuPortal: (base) => Object.assign({}, base, { zIndex: 9999 }) }}
|
||||
className="mb-11 mt-2 w-full rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Label className="mt-6">{t("timezone")}</Label>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={({ value }) => setSelectedTimeZone(value)}
|
||||
className="mb-11 mt-2 w-full rounded-md text-sm"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter showDivider className="relative">
|
||||
<DialogClose />
|
||||
</DialogPanel>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="ghost" />}>{t("cancel")}</DialogClose>
|
||||
<Button
|
||||
disabled={isNoEndDate ? !startDate : !startDate || !endDate}
|
||||
onClick={() => {
|
||||
@@ -150,7 +183,7 @@ const TravelScheduleModal = ({
|
||||
{t("add")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</DialogPopup>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import { Skeleton } from "@coss/ui/components/skeleton";
|
||||
|
||||
export const SkeletonLoader = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
|
||||
<SkeletonContainer>
|
||||
<div className="space-y-6">
|
||||
<div className="border-subtle stack-y-6 rounded-b-xl border border-t-0 px-4 py-8 sm:px-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
|
||||
<SkeletonButton className="ml-auto h-8 w-20 rounded-md p-5" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="ml-auto h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
</div>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -204,8 +204,8 @@ const GeneralView = ({ user, travelSchedules }: GeneralViewProps) => {
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("timezone")}</>
|
||||
</Label>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-end">
|
||||
<div className="w-full sm:w-1/2">
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
value={value}
|
||||
@@ -219,6 +219,7 @@ const GeneralView = ({ user, travelSchedules }: GeneralViewProps) => {
|
||||
</div>
|
||||
{!watchedTzSchedules.length && (
|
||||
<Button
|
||||
className="w-full sm:w-1/2"
|
||||
color="secondary"
|
||||
StartIcon="calendar"
|
||||
onClick={() => setIsTZScheduleOpen(true)}
|
||||
@@ -413,7 +414,7 @@ const GeneralView = ({ user, travelSchedules }: GeneralViewProps) => {
|
||||
/>
|
||||
<TravelScheduleModal
|
||||
open={isTZScheduleOpen}
|
||||
onOpenChange={() => setIsTZScheduleOpen(false)}
|
||||
onOpenChange={setIsTZScheduleOpen}
|
||||
setValue={formMethods.setValue}
|
||||
existingSchedules={formMethods.getValues("travelSchedules") ?? []}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: "`@coss/ui` Migration Guide"
|
||||
description: "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:
|
||||
|
||||
- [Radix / shadcn migration guide](radix_shadcn_migration_guide.md)
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
- Identify all `@calcom/ui` usages in the scope of the migration.
|
||||
- Replace Radix `asChild` with Base UI `render` where 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: `render` for composability, `*Popup`/`*Panel` for 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` → `DialogPopup`
|
||||
- `AlertDialogContent` → `AlertDialogPopup`
|
||||
- `AccordionContent` → `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.
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```ts
|
||||
// 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:
|
||||
|
||||
```json
|
||||
// packages/ui/package.json - add direct export
|
||||
"./components/form/date-range-picker/DateRangePicker": "./components/form/date-range-picker/DateRangePicker.tsx"
|
||||
```
|
||||
|
||||
Then import directly:
|
||||
|
||||
```tsx
|
||||
// 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.
|
||||
@@ -0,0 +1,965 @@
|
||||
---
|
||||
title: Radix / shadcn Migration
|
||||
description: A comprehensive guide for migrating from Radix UI and shadcn/ui to coss ui components.
|
||||
---
|
||||
|
||||
This guide is designed for developers who already have applications built with **shadcn/ui** or **Radix UI** and want to adopt **coss ui** components. **coss ui is fundamentally built with Base UI from the ground up—it is not an adaptation from Radix UI.** Recognizing that many teams are migrating from Radix-based libraries, this page consolidates all migration instructions for a smooth transition.
|
||||
|
||||
## Overview
|
||||
|
||||
coss ui components are built on **Base UI**, not Radix. This means the architecture and API patterns are different by design. However, we've worked to make the migration path as clear as possible by:
|
||||
|
||||
- Providing detailed component-by-component migration guides below
|
||||
- Maintaining similar component names and structures where it makes sense
|
||||
- Offering clear prop mappings and code examples
|
||||
|
||||
## General Migration Patterns
|
||||
|
||||
### The asChild to render Pattern
|
||||
|
||||
The most common change across all components is replacing Radix UI's `asChild` prop with Base UI's `render` prop:
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="Radix / shadcn"
|
||||
// [!code word:asChild]
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Edit profile</Button>
|
||||
</DropdownMenuTrigger>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
<MenuTrigger render={<Button />}>Edit profile</MenuTrigger>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Component Naming Conventions
|
||||
|
||||
Some components have updated names for clarity and consistency:
|
||||
|
||||
- `*Content` → `*Popup` or `*Panel` (e.g., `DialogContent` → `DialogPopup`)
|
||||
- Legacy names are often kept for backward compatibility
|
||||
|
||||
## Component Migration Guides
|
||||
|
||||
### Accordion
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `type="multiple"` → `multiple={true}` on `Accordion`
|
||||
- Remove `type="single"` from `Accordion`
|
||||
- Remove `collapsible` from `Accordion`
|
||||
- Always use arrays for `defaultValue`
|
||||
- Use `AccordionPanel` going forward; `AccordionContent` remains for legacy
|
||||
- If you used `asChild` on parts, switch to the `render` prop
|
||||
|
||||
**Prop Mapping:**
|
||||
|
||||
| Component | Radix UI Prop | Base UI Prop |
|
||||
| ----------- | ----------------------------------------- | ------------------------------------- |
|
||||
| `Accordion` | `type` (enum, `"single"` or `"multiple"`) | `multiple` (boolean, default: `false`) |
|
||||
| `Accordion` | `collapsible` | _removed_ |
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:type="multiple"]
|
||||
// [!code word:collapsible]
|
||||
// [!code word:"item-1":1]
|
||||
<Accordion type="multiple" collapsible defaultValue="item-1">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Title</AccordionTrigger>
|
||||
<AccordionContent>Content</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:multiple={true}]
|
||||
// [!code word:\{\["item-1"\]\}]
|
||||
<Accordion multiple={true} defaultValue={["item-1"]}>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Title</AccordionTrigger>
|
||||
<AccordionPanel>Content</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Alert
|
||||
|
||||
**New Variants:**
|
||||
|
||||
We've added new colored variants for better semantic meaning:
|
||||
|
||||
| Variant | Description |
|
||||
| --------- | --------------------------------- |
|
||||
| `info` | Displays an info alert (blue) |
|
||||
| `success` | Displays a success alert (green) |
|
||||
| `warning` | Displays a warning alert (yellow) |
|
||||
| `error` | Displays a error alert (red) |
|
||||
|
||||
Ensure you have the following variables imported in your CSS file:
|
||||
|
||||
- `--destructive-foreground`
|
||||
- `--info`
|
||||
- `--info-foreground`
|
||||
- `--success`
|
||||
- `--success-foreground`
|
||||
- `--warning`
|
||||
- `--warning-foreground`
|
||||
|
||||
### Alert Dialog
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `AlertDialogTrigger` and closing buttons
|
||||
- Replace `AlertDialogAction` and `AlertDialogCancel` → `AlertDialogClose`
|
||||
- Prefer `AlertDialogPopup`; `AlertDialogContent` remains for legacy
|
||||
- Use `AlertDialogPanel` to wrap main content between `AlertDialogHeader` and `AlertDialogFooter`
|
||||
- If you used `asChild` on any other parts, switch to the `render` prop
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
// [!code word:AlertDialogCancel]
|
||||
// [!code word:AlertDialogAction]
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Show Alert Dialog</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button>Continue</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
// [!code word:AlertDialogClose]
|
||||
// [!code word:<AlertDialogPanel>Content</AlertDialogPanel>]
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
Show Alert Dialog
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPopup>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogPanel>Content</AlertDialogPanel>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogClose render={<Button variant="ghost" />}>
|
||||
Cancel
|
||||
</AlertDialogClose>
|
||||
<AlertDialogClose render={<Button variant="destructive" />}>
|
||||
Continue
|
||||
</AlertDialogClose>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogPopup>
|
||||
</AlertDialog>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Avatar
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `Avatar`
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Avatar asChild>
|
||||
<Link href="/profile">
|
||||
<AvatarImage src="avatar.jpg" alt="User" />
|
||||
<AvatarFallback>U</AvatarFallback>
|
||||
</Link>
|
||||
</Avatar>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
<Avatar render={<Link href="/profile" />}>
|
||||
<AvatarImage src="avatar.jpg" alt="User" />
|
||||
<AvatarFallback>U</AvatarFallback>
|
||||
</Avatar>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Badge
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `Badge`
|
||||
|
||||
**Size Comparison:**
|
||||
|
||||
Compared to shadcn/ui, our `Badge` component includes size variants for better density control. shadcn/ui badges have a fixed size, while our component offers flexible sizing with `sm`, `default`, and `lg` options.
|
||||
|
||||
So, if you want to preserve the original shadcn/ui badge size, you should use the `lg` size in coss ui.
|
||||
|
||||
**New Variants:**
|
||||
|
||||
We've added new colored variants to the existing ones (`default`, `destructive`, `outline`, `secondary`) for better semantic meaning and visual communication:
|
||||
|
||||
| Variant | Description |
|
||||
| --------- | ---------------------------------- |
|
||||
| `info` | Blue badge for information |
|
||||
| `success` | Green badge for success states |
|
||||
| `warning` | Yellow badge for warnings |
|
||||
| `error` | Red badge for errors |
|
||||
|
||||
Ensure you have the following variables imported in your CSS file:
|
||||
|
||||
- `--destructive-foreground`
|
||||
- `--info`
|
||||
- `--info-foreground`
|
||||
- `--success`
|
||||
- `--success-foreground`
|
||||
- `--warning`
|
||||
- `--warning-foreground`
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Badge asChild>
|
||||
<Link href="/new">New</Link>
|
||||
</Badge>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
<Badge render={<Link href="/new" />}>New</Badge>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Button
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `Button`
|
||||
|
||||
**Size Comparison:**
|
||||
|
||||
coss ui button sizes are more compact compared to shadcn/ui, making them better suited for dense applications. We also introduce new sizes (`xs`, `xl`, `icon-sm`, `icon-lg`) for more granular control:
|
||||
|
||||
| Size | Height (shadcn/ui) | Height (coss ui) |
|
||||
| --------- | ------------------ | ---------------- |
|
||||
| `xs` | - | 24px |
|
||||
| `sm` | 32px | 28px |
|
||||
| `default` | 36px | 32px |
|
||||
| `lg` | 40px | 36px |
|
||||
| `xl` | - | 40px |
|
||||
| `icon` | 36px | 32px |
|
||||
| `icon-sm` | - | 28px |
|
||||
| `icon-lg` | - | 36px |
|
||||
|
||||
So, for example, if you were using the `default` size in shadcn/ui and you want to preserve the original height, you should use the `lg` size in coss ui.
|
||||
|
||||
**New Variants:**
|
||||
|
||||
We've added a new `destructive-outline` variant for better UX patterns:
|
||||
|
||||
- **Primary actions**: Use `destructive` (solid red) for the main destructive action
|
||||
- **Secondary triggers**: Use `destructive-outline` (outline red) to avoid alarming red buttons in the main interface
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Button asChild>
|
||||
<Link href="/login">Login</Link>
|
||||
</Button>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
<Button render={<Link href="/login" />}>Login</Button>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Card
|
||||
|
||||
**Quick Checklist:**
|
||||
- Use `CardPanel` going forward; `CardContent` remains for legacy
|
||||
|
||||
### Checkbox
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `Checkbox`
|
||||
|
||||
|
||||
### Collapsible
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `CollapsibleTrigger`
|
||||
- Prefer `CollapsiblePanel`; `CollapsibleContent` remains for legacy
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button>Toggle</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>Content here</CollapsibleContent>
|
||||
</Collapsible>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
// [!code word:CollapsiblePanel]
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger render={<Button />}>Toggle</CollapsibleTrigger>
|
||||
<CollapsiblePanel>Content here</CollapsiblePanel>
|
||||
</Collapsible>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Command
|
||||
|
||||
The API is significantly different from shadcn/ui (cmdk). Please review both docs before migrating: [cmdk Docs](https://cmdk.paco.me/) and [shadcn/ui Command](https://ui.shadcn.com/docs/components/command), and our Base UI Autocomplete docs.
|
||||
|
||||
**Key Differences:**
|
||||
|
||||
- No `cmdk` dependency - built entirely with Base UI's Autocomplete and Dialog components
|
||||
- Data-driven approach - pass an `items` array to `Command` and use render functions instead of manually composing `CommandItem` children
|
||||
- Use `CommandCollection` within `CommandGroup` when rendering grouped data with the items pattern
|
||||
- Use `CommandDialog`, `CommandDialogTrigger`, and `CommandDialogPopup` for dialog functionality instead of composing separate Dialog components
|
||||
- `CommandGroup` uses `<CommandGroupLabel>` as a child instead of a `heading` prop
|
||||
|
||||
### Dialog
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `DialogTrigger` and closing buttons
|
||||
- Prefer `DialogPopup`; `DialogContent` remains for legacy
|
||||
- Use `DialogPanel` to wrap main content between `DialogHeader` and `DialogFooter`
|
||||
- If you used `asChild` on any other parts, switch to the `render` prop
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Show Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog Description</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
// [!code word:<DialogPanel>Content</DialogPanel>]
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant="outline" />}>
|
||||
Show Dialog
|
||||
</DialogTrigger>
|
||||
<DialogPopup>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog Description</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogPanel>Content</DialogPanel>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogPopup>
|
||||
</Dialog>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Group (Button Group)
|
||||
|
||||
**Quick Checklist:**
|
||||
- Prefer `Group*` component names; `ButtonGroup*` remain for compatibility
|
||||
- `GroupSeparator` is **always required** between controls, including outline buttons (unlike shadcn where separators are optional for outline buttons). This ensures consistent focus state handling and better accessibility
|
||||
- If you used `asChild` on `ButtonGroupText`, switch to the `render` prop for custom components
|
||||
|
||||
### Input
|
||||
|
||||
Compared to shadcn/ui, our `Input` component includes size variants for better density control. shadcn/ui inputs have a fixed height of 36px, while our component offers flexible sizing with `sm` (28px), `default` (32px), and `lg` (36px) options.
|
||||
|
||||
So, if you want to preserve the original shadcn/ui input height (36px), you should use the `lg` size in coss ui.
|
||||
|
||||
### Input Group
|
||||
|
||||
**Quick Checklist:**
|
||||
- No `InputGroupButton` component - use the regular `Button` component directly inside `InputGroupAddon` instead
|
||||
- To disable an input group, disable the `InputGroupInput` or `InputGroupTextarea` directly (and any Button inside it) - no need to add a `data-disabled` attribute on `InputGroup`.
|
||||
|
||||
### Menu
|
||||
|
||||
**Prop Mapping:**
|
||||
|
||||
| Component | Radix UI Prop | Base UI Prop |
|
||||
| ---------- | ------------- | ------------ |
|
||||
| `MenuItem` | `onSelect` | `onClick` |
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `MenuTrigger` and `MenuItem`
|
||||
- Replace `onSelect` → `onClick` on menu items
|
||||
- Update import path from `@/components/ui/dropdown-menu` → `@/components/ui/menu`
|
||||
- Prefer `Menu*` component names; `DropdownMenu*` remain for legacy
|
||||
- Prefer `MenuGroupLabel` instead of `DropdownMenuLabel`
|
||||
- Prefer `MenuPopup` instead of `DropdownMenuContent`
|
||||
- Prefer `MenuSubPopup` instead of `DropdownMenuSubContent`
|
||||
- If you used `asChild` on any other parts, switch to the `render` prop
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:onSelect]
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>Open menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
console.log("Dashboard")
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Sign out</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:onClick]
|
||||
<Menu>
|
||||
<MenuTrigger>Open menu</MenuTrigger>
|
||||
<MenuPopup>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
console.log("Dashboard")
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</MenuItem>
|
||||
<MenuItem>Settings</MenuItem>
|
||||
<MenuItem>Sign out</MenuItem>
|
||||
</MenuPopup>
|
||||
</Menu>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Popover
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `PopoverTrigger` and closing buttons
|
||||
- Prefer `PopoverPopup`; `PopoverContent` remains for legacy
|
||||
- If you used `asChild` on any other parts, switch to the `render` prop
|
||||
|
||||
**Additional Notes:**
|
||||
|
||||
Base UI introduces `PopoverTitle` and `PopoverDescription` to structure headings and helper text inside the popup. Base UI also introduces a `PopoverClose` component for adding close buttons to the popup.
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<h2>Popover Title</h2>
|
||||
<p>Popover Description</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:PopoverTitle]
|
||||
// [!code word:PopoverDescription]
|
||||
// [!code word:PopoverClose]
|
||||
// [!code word:render:2]
|
||||
<Popover>
|
||||
<PopoverTrigger render={<Button variant="outline" />}>
|
||||
Open Popover
|
||||
</PopoverTrigger>
|
||||
<PopoverPopup>
|
||||
<PopoverTitle>Popover Title</PopoverTitle>
|
||||
<PopoverDescription>Popover Description</PopoverDescription>
|
||||
<PopoverClose render={<Button variant="ghost" />}>Close</PopoverClose>
|
||||
</PopoverPopup>
|
||||
</Popover>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Preview Card
|
||||
|
||||
**Quick Checklist:**
|
||||
- Update import path from `@/components/ui/hover-card` → `@/components/ui/preview-card`
|
||||
- Prefer `PreviewCard*` component names; `HoverCard*` remain for legacy
|
||||
- Prefer `PreviewCardPopup` instead of `HoverCardContent`
|
||||
- If you used `asChild` on parts, switch to the `render` prop
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant="outline">Open Preview Card</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>Preview Card Content</HoverCardContent>
|
||||
</HoverCard>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
<PreviewCard>
|
||||
<PreviewCardTrigger render={<Button variant="outline" />}>
|
||||
Open Preview Card
|
||||
</PreviewCardTrigger>
|
||||
<PreviewCardPopup>Preview Card Content</PreviewCardPopup>
|
||||
</PreviewCard>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Progress
|
||||
|
||||
**Quick Checklist:**
|
||||
- Prefer `ProgressLabel` and `ProgressValue` for label/value instead of inline elements
|
||||
- If you render children inside `Progress`, you must include `ProgressTrack` and `ProgressIndicator` (otherwise the bar will not display). Without children, a default bar is rendered for you
|
||||
- If you used `asChild`, switch to the `render` prop
|
||||
|
||||
**Additional Notes:**
|
||||
|
||||
Base UI introduces separate parts — `ProgressLabel`, `ProgressValue`, `ProgressTrack`, and `ProgressIndicator` — which you compose inside `Progress` for greater flexibility.
|
||||
|
||||
### Radio Group
|
||||
|
||||
**Quick Checklist:**
|
||||
- Use `Radio` going forward; `RadioGroupItem` remains for legacy
|
||||
- Replace `asChild` → `render` on parts if used
|
||||
|
||||
### Scroll Area
|
||||
|
||||
**Quick Checklist:**
|
||||
- If you used `asChild` on parts, switch to the `render` prop
|
||||
|
||||
### Select
|
||||
|
||||
**Important:** Base UI changes how options are provided. Instead of deriving options from children only (Radix pattern), you should pass an `items` prop (array or record) so values and labels are known before hydration. This avoids SSR pitfalls and improves mount performance. Alternatively, provide a function child to `SelectValue` to format the label. See the [Base UI Select docs](https://base-ui.com/react/components/select).
|
||||
|
||||
**Prop Mapping:**
|
||||
|
||||
| Component | Radix UI Prop | Base UI Prop |
|
||||
| ------------- | ---------------------- | --------------- |
|
||||
| `Select` | `items` | `items` |
|
||||
| `SelectValue` | `placeholder` | _removed_ |
|
||||
| `SelectPopup` | `alignItemWithTrigger` | _no equivalent_ |
|
||||
|
||||
**Quick Checklist:**
|
||||
- Set `items` prop on `Select`
|
||||
- Remove `placeholder` from `Select`
|
||||
- Prefer `SelectPopup` instead of `SelectContent`
|
||||
- If you used `asChild` on parts, switch to the `render` prop
|
||||
|
||||
**Size Comparison:**
|
||||
|
||||
coss ui select sizes are more compact compared to shadcn/ui, making them better suited for dense applications:
|
||||
|
||||
| Size | Height (shadcn/ui) | Height (coss ui) |
|
||||
| --------- | ------------------ | ---------------- |
|
||||
| `sm` | 32px | 28px |
|
||||
| `default` | 36px | 32px |
|
||||
| `lg` | - | 36px |
|
||||
|
||||
So, for example, if you were using the `default` size in shadcn/ui and you want to preserve the original height, you should use the `lg` size in coss ui.
|
||||
|
||||
**Additional Notes:**
|
||||
|
||||
Base UI introduces the `alignItemWithTrigger` prop to control whether the `SelectContent` overlaps the `SelectTrigger` so the selected item's text is aligned with the trigger's value text.
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:placeholder="Select a framework"]
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a framework" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="next">Next.js</SelectItem>
|
||||
<SelectItem value="vite">Vite</SelectItem>
|
||||
<SelectItem value="astro">Astro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:alignItemWithTrigger={false}]
|
||||
// [!code word:items:2]
|
||||
<Select
|
||||
items={[
|
||||
{ label: "Select a framework", value: null },
|
||||
{ label: "Next.js", value: "next" },
|
||||
{ label: "Vite", value: "vite" },
|
||||
{ label: "Astro", value: "astro" },
|
||||
]}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectPopup alignItemWithTrigger={false}>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.value} value={item}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectPopup>
|
||||
</Select>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Sheet
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `SheetTrigger` and closing buttons
|
||||
- Prefer `SheetPopup`; `SheetContent` remains for legacy
|
||||
- Use `SheetPanel` to wrap main content
|
||||
- If you used `asChild` on any other parts, switch to the `render` prop
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">Open Sheet</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
Content here
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button>Close</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
// [!code word:<SheetPanel>Content here</SheetPanel>]
|
||||
<Sheet>
|
||||
<SheetTrigger render={<Button variant="outline" />}>
|
||||
Open Sheet
|
||||
</SheetTrigger>
|
||||
<SheetPopup>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetPanel>Content here</SheetPanel>
|
||||
<SheetFooter>
|
||||
<SheetClose render={<Button />}>Close</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetPopup>
|
||||
</Sheet>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Slider
|
||||
|
||||
**Quick Checklist:**
|
||||
- coss ui `Slider` uses Base UI's multiple value approach
|
||||
- Always pass values as arrays (e.g., `value={[50]}` instead of `value={50}`)
|
||||
- `onValueChange` receives an array of numbers
|
||||
- Multiple thumbs are supported natively via array length
|
||||
- Replace `asChild` → `render` on parts if used
|
||||
|
||||
### Switch
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `Switch` if used
|
||||
|
||||
### Tabs
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on parts if used
|
||||
- Use `TabsTab` going forward; `TabsTrigger` remains for legacy
|
||||
- Prefer `TabsPanel`; `TabsContent` remains for legacy
|
||||
|
||||
**Additional Notes:**
|
||||
|
||||
Compared to shadcn/ui, our `TabsList` component adds `variant` prop, which allows you to choose between `default` and `underline` styles.
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:TabsContent]
|
||||
<Tabs defaultValue="tab-1">
|
||||
<TabsList>
|
||||
<TabsTab value="tab-1">Tab 1</TabsTab>
|
||||
<TabsTab value="tab-2">Tab 2</TabsTab>
|
||||
<TabsTab value="tab-3">Tab 3</TabsTab>
|
||||
</TabsList>
|
||||
<TabsContent value="tab-1">Tab 1 content</TabsContent>
|
||||
<TabsContent value="tab-2">Tab 2 content</TabsContent>
|
||||
<TabsContent value="tab-3">Tab 3 content</TabsContent>
|
||||
</Tabs>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:TabsPanel]
|
||||
<Tabs defaultValue="tab-1">
|
||||
<TabsList>
|
||||
<TabsTab value="tab-1">Tab 1</TabsTab>
|
||||
<TabsTab value="tab-2">Tab 2</TabsTab>
|
||||
<TabsTab value="tab-3">Tab 3</TabsTab>
|
||||
</TabsList>
|
||||
<TabsPanel value="tab-1">Tab 1 content</TabsPanel>
|
||||
<TabsPanel value="tab-2">Tab 2 content</TabsPanel>
|
||||
<TabsPanel value="tab-3">Tab 3 content</TabsPanel>
|
||||
</Tabs>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Textarea
|
||||
|
||||
Compared to shadcn/ui, our `Textarea` component includes size variants (`sm`, `default`, `lg`) for better density control. For visual consistency, if you're using `size="lg"` on other form elements like inputs, you should add the same size to textareas as well.
|
||||
|
||||
### Toast
|
||||
|
||||
The API is significantly different from shadcn/ui (Sonner). Please review both docs before migrating: [Sonner Docs](https://sonner.emilkowal.ski/) and [shadcn/ui Sonner](https://ui.shadcn.com/docs/components/sonner), and our Base UI toast docs.
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `<Toaster />` component in layout → `<ToastProvider>` wrapper
|
||||
- Toast API calls differ significantly - see comparison below
|
||||
- Toast actions use different patterns
|
||||
|
||||
**Comparison Examples:**
|
||||
|
||||
**shadcn/ui (Sonner)**
|
||||
|
||||
```tsx title="app/layout.tsx"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<main>{children}</main>
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
toast("Event has been created", {
|
||||
description: "Sunday, December 03, 2023 at 9:00 AM",
|
||||
cancel: {
|
||||
label: "Undo",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**coss ui (Base UI)**
|
||||
|
||||
```tsx title="app/layout.tsx"
|
||||
import { ToastProvider } from "@/components/ui/toast"
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<ToastProvider>
|
||||
<main>{children}</main>
|
||||
</ToastProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
onClick={() => {
|
||||
const id = toastManager.add({
|
||||
title: "Event has been created",
|
||||
description: "Sunday, December 03, 2023 at 9:00 AM",
|
||||
type: "success",
|
||||
actionProps: {
|
||||
children: "Undo",
|
||||
onClick: () => toastManager.close(id),
|
||||
},
|
||||
})
|
||||
}}
|
||||
```
|
||||
|
||||
### Toggle
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `Toggle` if used
|
||||
|
||||
### Toggle Group
|
||||
|
||||
**Prop Mapping:**
|
||||
|
||||
| Component | Radix UI Prop | Base UI Prop |
|
||||
| ------------- | ----------------------------------------- | -------------------------------------- |
|
||||
| `ToggleGroup` | `type` (enum, `"single"` or `"multiple"`) | `multiple` (boolean, default: `false`) |
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `type="multiple"` → `multiple` on `ToggleGroup`
|
||||
- Remove `type="single"` from `ToggleGroup`
|
||||
- Always use arrays for `defaultValue`
|
||||
- Use `Toggle` going forward; `ToggleGroupItem` remains for legacy
|
||||
- Replace `asChild` → `render` on parts if used
|
||||
|
||||
**Size Comparison:**
|
||||
|
||||
coss ui toggle group sizes are more compact compared to shadcn/ui, making them better suited for dense applications:
|
||||
|
||||
| Size | Height (shadcn/ui) | Height (coss ui) |
|
||||
| --------- | ------------------ | ---------------- |
|
||||
| `sm` | 32px | 28px |
|
||||
| `default` | 36px | 32px |
|
||||
| `lg` | - | 36px |
|
||||
|
||||
So, for example, if you were using the `default` size in shadcn/ui and you want to preserve the original height, you should use the `lg` size in coss ui.
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:type="multiple"]
|
||||
<ToggleGroup type="multiple" defaultValue={["bold"]}>
|
||||
<ToggleGroupItem value="bold">B</ToggleGroupItem>
|
||||
<ToggleGroupItem value="italic">I</ToggleGroupItem>
|
||||
<ToggleGroupItem value="underline">U</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:multiple]
|
||||
<ToggleGroup multiple defaultValue={["bold"]}>
|
||||
<Toggle value="bold">B</Toggle>
|
||||
<Toggle value="italic">I</Toggle>
|
||||
<Toggle value="underline">U</Toggle>
|
||||
</ToggleGroup>
|
||||
```
|
||||
</span>
|
||||
|
||||
### Tooltip
|
||||
|
||||
**Quick Checklist:**
|
||||
- Replace `asChild` → `render` on `TooltipTrigger`
|
||||
- Prefer `TooltipPopup`; `TooltipContent` remains for legacy
|
||||
|
||||
**Comparison Example:**
|
||||
|
||||
<span data-lib="radix-ui">
|
||||
```tsx title="shadcn/ui"
|
||||
// [!code word:asChild]
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">Hover me</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
```
|
||||
</span>
|
||||
|
||||
<span data-lib="base-ui">
|
||||
```tsx title="coss ui"
|
||||
// [!code word:render]
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Button variant="outline" />}>
|
||||
Hover me
|
||||
</TooltipTrigger>
|
||||
<TooltipPopup>Tooltip content</TooltipPopup>
|
||||
</Tooltip>
|
||||
```
|
||||
</span>
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Base UI Documentation](https://base-ui.com/) - Official Base UI docs
|
||||
- [Component Documentation](/ui/docs/components) - Individual component docs with examples
|
||||
- [Styling Guide](/ui/docs/styling) - Learn about our color system and theming
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter issues during migration or have questions about specific components, please:
|
||||
|
||||
- Check the individual component documentation pages
|
||||
- Review the Base UI documentation for deeper understanding
|
||||
- Open an issue on our GitHub repository
|
||||
@@ -1,11 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { format, isBefore, isSameDay } from "date-fns";
|
||||
import { useState, useMemo, type HTMLAttributes } from "react";
|
||||
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
import { type HTMLAttributes, useMemo, useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { Calendar } from "./Calendar";
|
||||
import { calculateNewDateRange } from "./dateRangeLogic";
|
||||
@@ -17,6 +15,9 @@ type DatePickerWithRangeProps = {
|
||||
minDate?: Date | null;
|
||||
maxDate?: Date;
|
||||
withoutPopover?: boolean;
|
||||
popoverModal?: boolean;
|
||||
popoverOpen?: boolean;
|
||||
onPopoverOpenChange?: (open: boolean) => void;
|
||||
"data-testid"?: string;
|
||||
strictlyBottom?: boolean;
|
||||
allowPastDates?: boolean;
|
||||
@@ -30,6 +31,9 @@ export function DatePickerWithRange({
|
||||
onDatesChange,
|
||||
disabled,
|
||||
withoutPopover,
|
||||
popoverModal = true,
|
||||
popoverOpen,
|
||||
onPopoverOpenChange,
|
||||
"data-testid": testId,
|
||||
strictlyBottom,
|
||||
allowPastDates = false,
|
||||
@@ -56,7 +60,7 @@ export function DatePickerWithRange({
|
||||
setHoveredDate(undefined);
|
||||
}
|
||||
|
||||
const fromDate = allowPastDates && minDate === null ? undefined : minDate ?? new Date();
|
||||
const fromDate = allowPastDates && minDate === null ? undefined : (minDate ?? new Date());
|
||||
|
||||
const hoverRangeModifier = useMemo(() => {
|
||||
if (!dates.startDate || dates.endDate || !hoveredDate) {
|
||||
@@ -97,7 +101,7 @@ export function DatePickerWithRange({
|
||||
return (
|
||||
<div className={classNames("grid gap-2", className)}>
|
||||
{/* modal prop required for iOS compatibility when nested inside Dialog modals */}
|
||||
<Popover.Root modal>
|
||||
<Popover.Root modal={popoverModal} open={popoverOpen} onOpenChange={onPopoverOpenChange}>
|
||||
<Popover.Trigger asChild>
|
||||
<Button
|
||||
data-testid="date-range"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"./components/empty-screen": "./components/empty-screen/index.ts",
|
||||
"./components/errorBoundary": "./components/errorBoundary/index.ts",
|
||||
"./components/form": "./components/form/index.ts",
|
||||
"./components/form/date-range-picker/DateRangePicker": "./components/form/date-range-picker/DateRangePicker.tsx",
|
||||
"./components/form/timezone-select": "./components/form/timezone-select/index.ts",
|
||||
"./components/hover-card": "./components/hover-card/index.tsx",
|
||||
"./components/image-uploader": "./components/image-uploader/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user