From 51371c978281f472ae05313c568941234fd8e2b1 Mon Sep 17 00:00:00 2001 From: junwen-k <40173716+junwen-k@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:11:36 +0800 Subject: [PATCH 1/7] refactor(confirmer): compose action with AlertDialogAction Replace the raw Button with AlertDialogAction, move the description inside AlertDialogHeader to match the alert-dialog composition, and group exports at the bottom per shadcn convention. Co-Authored-By: Claude Fable 5 --- .../v4/src/registry/new-york/ui/confirmer.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/v4/src/registry/new-york/ui/confirmer.tsx b/apps/v4/src/registry/new-york/ui/confirmer.tsx index f662ef2..620528d 100644 --- a/apps/v4/src/registry/new-york/ui/confirmer.tsx +++ b/apps/v4/src/registry/new-york/ui/confirmer.tsx @@ -4,6 +4,7 @@ import { createCallable } from "react-call"; import { AlertDialog, + AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, @@ -11,7 +12,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; interface ConfirmOptions { title?: React.ReactNode; @@ -19,7 +19,7 @@ interface ConfirmOptions { cancelText?: React.ReactNode; actionText?: React.ReactNode; CancelProps?: React.ComponentProps; - ActionProps?: React.ComponentProps; + ActionProps?: React.ComponentProps; } const defaultOptions = { @@ -44,15 +44,20 @@ const CallableConfirm = createCallable( {options.title} + + {options.description} + - {options.description} {options.cancelText} - + @@ -61,6 +66,8 @@ const CallableConfirm = createCallable( UNMOUNTING_DELAY, ); -export const Confirmer = CallableConfirm.Root; +const Confirmer = CallableConfirm.Root; -export const confirm = CallableConfirm.call; +const confirm = CallableConfirm.call; + +export { Confirmer, confirm }; From 7157fcdc45e3eeb3afcb20887add703343387608 Mon Sep 17 00:00:00 2001 From: junwen-k <40173716+junwen-k@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:11:47 +0800 Subject: [PATCH 2/7] refactor(registry): align component code with shadcn idioms Drop the stray displayName from badge-group, use namespace React imports in description-list, switch time to React.ComponentProps, replace the hand-rolled label in the password-input checkbox example with the Label component, and pass the phone-input render prop inline instead of wrapping it in useMemo. Co-Authored-By: Claude Fable 5 --- .../src/components/examples/password-input-checkbox.tsx | 8 ++------ .../examples/phone-input-primitive-preferred-country.tsx | 4 +--- apps/v4/src/registry/new-york/ui/badge-group.tsx | 1 - apps/v4/src/registry/new-york/ui/description-list.tsx | 2 +- apps/v4/src/registry/new-york/ui/time.tsx | 2 +- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/v4/src/components/examples/password-input-checkbox.tsx b/apps/v4/src/components/examples/password-input-checkbox.tsx index 58d466e..56a5b8e 100644 --- a/apps/v4/src/components/examples/password-input-checkbox.tsx +++ b/apps/v4/src/components/examples/password-input-checkbox.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; import { PasswordInput, PasswordInputInput, @@ -22,12 +23,7 @@ export default function PasswordInputCheckbox() { checked={visible} onCheckedChange={(checked) => setVisible(Boolean(checked))} /> - + ); diff --git a/apps/v4/src/components/examples/phone-input-primitive-preferred-country.tsx b/apps/v4/src/components/examples/phone-input-primitive-preferred-country.tsx index 9db266f..db9e624 100644 --- a/apps/v4/src/components/examples/phone-input-primitive-preferred-country.tsx +++ b/apps/v4/src/components/examples/phone-input-primitive-preferred-country.tsx @@ -22,9 +22,7 @@ export default function PhoneInputPrimitivePreferredCountry() { preferredCountry="MY" defaultInternationalForPreferredCountry > - , [])} - /> + } />

Preferred country has been set to{" "} diff --git a/apps/v4/src/registry/new-york/ui/badge-group.tsx b/apps/v4/src/registry/new-york/ui/badge-group.tsx index f2f8083..07af2f8 100644 --- a/apps/v4/src/registry/new-york/ui/badge-group.tsx +++ b/apps/v4/src/registry/new-york/ui/badge-group.tsx @@ -121,7 +121,6 @@ function BadgeGroup({ ); } -BadgeGroup.displayName = "BadgeGroup"; interface BadgeGroupItemProps extends Toggle.Props { value: string; diff --git a/apps/v4/src/registry/new-york/ui/description-list.tsx b/apps/v4/src/registry/new-york/ui/description-list.tsx index 835c10a..c812241 100644 --- a/apps/v4/src/registry/new-york/ui/description-list.tsx +++ b/apps/v4/src/registry/new-york/ui/description-list.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/apps/v4/src/registry/new-york/ui/time.tsx b/apps/v4/src/registry/new-york/ui/time.tsx index 3797f90..08a2489 100644 --- a/apps/v4/src/registry/new-york/ui/time.tsx +++ b/apps/v4/src/registry/new-york/ui/time.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; export interface TimeProps - extends Omit, "children"> { + extends Omit, "children"> { children: Parameters[0]; dateTimeFormatStr?: Parameters[1]; formatStr?: Parameters[1]; From 1e97ea1fbe38a015c157aeba193151c421abcf2c Mon Sep 17 00:00:00 2001 From: junwen-k <40173716+junwen-k@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:11:58 +0800 Subject: [PATCH 3/7] fix(calendar): rebuild vendored calendar for react-day-picker v9 The vendored copy still used the react-day-picker v8 classNames API (caption, nav_button, head_row, cell) whose keys don't exist in the installed v9, so every class was dead and the calendar rendered on v9 defaults. Replace it with the current base-nova calendar (getDefaultClassNames, --cell-size/--cell-radius, captionLayout, CalendarDayButton), resolving the site-internal IconPlaceholder to lucide chevrons and the unreleased cn-calendar-* utilities to their released equivalents. Co-Authored-By: Claude Fable 5 --- apps/v4/src/components/ui/calendar.tsx | 262 +++++++++++++++++++------ 1 file changed, 198 insertions(+), 64 deletions(-) diff --git a/apps/v4/src/components/ui/calendar.tsx b/apps/v4/src/components/ui/calendar.tsx index 5a0a502..b2cb743 100644 --- a/apps/v4/src/components/ui/calendar.tsx +++ b/apps/v4/src/components/ui/calendar.tsx @@ -1,95 +1,229 @@ "use client"; import { - ChevronDown, - ChevronLeft, - ChevronRight, - ChevronUp, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, } from "lucide-react"; import * as React from "react"; -import { DayPicker } from "react-day-picker"; +import { + DayPicker, + getDefaultClassNames, + type DayButton, + type Locale, +} from "react-day-picker"; -import { buttonVariants } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; function Calendar({ className, classNames, showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + locale, + formatters, + components, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"]; +}) { + const defaultClassNames = getDefaultClassNames(); + return ( svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + )} + captionLayout={captionLayout} + locale={locale} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString(locale?.code, { month: "short" }), + ...formatters, + }} classNames={{ - months: "flex flex-col gap-2 sm:flex-row", - month: "flex flex-col gap-4", - caption: "relative flex w-full items-center justify-center pt-1", - caption_label: "text-sm font-medium", - nav: "flex items-center gap-1", - nav_button: cn( - buttonVariants({ variant: "outline" }), - "size-7 bg-transparent p-0 opacity-50 hover:opacity-100", - ), - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-x-1", - head_row: "flex", - head_cell: - "text-muted-foreground w-8 rounded-md text-[0.8rem] font-normal", - row: "mt-2 flex w-full", - cell: cn( - "[&:has([aria-selected])]:bg-accent relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected].day-range-end)]:rounded-r-md", - props.mode === "range" - ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" - : "[&:has([aria-selected])]:rounded-md", + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months, + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_next, + ), + month_caption: cn( + "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", + defaultClassNames.month_caption, + ), + dropdowns: cn( + "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + "border-input has-focus:border-ring has-focus:ring-ring/50 relative rounded-(--cell-radius) border shadow-xs has-focus:ring-[3px]", + defaultClassNames.dropdown_root, + ), + dropdown: cn( + "bg-popover absolute inset-0 opacity-0", + defaultClassNames.dropdown, + ), + caption_label: cn( + "font-medium select-none", + captionLayout === "label" + ? "text-sm" + : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-(--cell-radius) pr-1 pl-2 text-sm [&>svg]:size-3.5", + defaultClassNames.caption_label, + ), + month_grid: cn("w-full border-collapse", defaultClassNames.month_grid), + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal select-none", + defaultClassNames.weekday, + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-(--cell-size) select-none", + defaultClassNames.week_number_header, + ), + week_number: cn( + "text-muted-foreground text-[0.8rem] select-none", + defaultClassNames.week_number, ), day: cn( - buttonVariants({ variant: "ghost" }), - "size-8 p-0 font-normal aria-selected:opacity-100", - ), - day_range_start: - "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground", - day_range_end: - "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground", - day_selected: - "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", - day_today: "bg-accent text-accent-foreground", - day_outside: - "day-outside text-muted-foreground aria-selected:text-muted-foreground", - day_disabled: "text-muted-foreground opacity-50", - day_range_middle: - "aria-selected:bg-accent aria-selected:text-accent-foreground", - day_hidden: "invisible", + "group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" + : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", + defaultClassNames.day, + ), + range_start: cn( + "bg-muted after:bg-muted relative isolate z-0 rounded-l-(--cell-radius) after:absolute after:inset-y-0 after:right-0 after:w-4", + defaultClassNames.range_start, + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn( + "bg-muted after:bg-muted relative isolate z-0 rounded-r-(--cell-radius) after:absolute after:inset-y-0 after:left-0 after:w-4", + defaultClassNames.range_end, + ), + today: cn( + "bg-muted text-foreground rounded-(--cell-radius) data-[selected=true]:rounded-none", + defaultClassNames.today, + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside, + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled, + ), + hidden: cn("invisible", defaultClassNames.hidden), ...classNames, }} components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +

+ ); + }, Chevron: ({ className, orientation, ...props }) => { - switch (orientation) { - case "up": - return ( - - ); - case "down": - return ( - - ); - case "left": - return ( - - ); - case "right": - default: - return ( - - ); + if (orientation === "left") { + return ( + + ); + } + + if (orientation === "right") { + return ( + + ); } + + return ( + + ); + }, + DayButton: ({ ...props }) => ( + + ), + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ); }, + ...components, }} {...props} /> ); } -export { Calendar }; +function CalendarDayButton({ + className, + day, + modifiers, + locale, + ...props +}: React.ComponentProps & { locale?: Partial }) { + const defaultClassNames = getDefaultClassNames(); + + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); + + return ( +