From 180e7e8be1cac1d416d15712b2cf1a3246be74c0 Mon Sep 17 00:00:00 2001 From: pasqualevitiello Date: Thu, 30 Jul 2026 17:06:26 +0200 Subject: [PATCH 1/4] refactor(ui): migrate Button to Base UI Button primitive Replace the custom useRender wrapper with @base-ui/react/button and style links via buttonVariants instead of Button render={}, matching shadcn guidance. Disabled styles use data-disabled so focusableWhenDisabled loading states stay consistent. Co-authored-by: Cursor --- .../app/(dashboard)/booking/bookings-list.tsx | 17 ++-- .../booking/past/bookings-list.tsx | 16 ++-- .../settings/developer/webhooks/new/page.tsx | 10 +- .../attributes/[id]/edit/page.tsx | 11 +-- .../attributes/attributes-section.tsx | 18 ++-- .../organizations/attributes/new/page.tsx | 11 +-- .../calcom/components/app/app-command.tsx | 14 +-- .../settings/settings-mobile-header.tsx | 10 +- apps/ui/app/docs/[[...slug]]/page.tsx | 34 ++++--- apps/ui/app/not-found.tsx | 22 ++--- apps/ui/app/page.tsx | 15 ++- apps/ui/app/particles/particle-card.tsx | 32 +++---- apps/ui/components/main-nav.tsx | 18 ++-- apps/ui/public/r/button.json | 2 +- apps/ui/public/r/p-button-17.json | 2 +- apps/ui/public/r/p-button-18.json | 2 +- apps/ui/public/r/p-command-2.json | 2 +- apps/ui/public/r/p-drawer-11.json | 2 +- apps/ui/public/r/p-drawer-14.json | 2 +- apps/ui/public/r/p-pagination-2.json | 2 +- .../default/particles/p-button-17.tsx | 8 +- .../default/particles/p-button-18.tsx | 2 +- .../default/particles/p-command-2.tsx | 14 +-- .../default/particles/p-drawer-11.tsx | 42 +++++---- .../default/particles/p-drawer-14.tsx | 50 +++++----- .../default/particles/p-pagination-2.tsx | 48 +++++----- apps/ui/registry/default/ui/button.tsx | 75 ++++++--------- apps/www/app/not-found.tsx | 22 ++--- packages/ui/src/components/button.tsx | 75 ++++++--------- packages/ui/src/shared/github-link-client.tsx | 26 +++--- packages/ui/src/shared/site-cta.tsx | 25 +++-- packages/ui/test/components/button.test.tsx | 93 ++++++++----------- 32 files changed, 346 insertions(+), 376 deletions(-) diff --git a/apps/examples/calcom/app/(dashboard)/booking/bookings-list.tsx b/apps/examples/calcom/app/(dashboard)/booking/bookings-list.tsx index 32e0eb458..fdeaa9250 100644 --- a/apps/examples/calcom/app/(dashboard)/booking/bookings-list.tsx +++ b/apps/examples/calcom/app/(dashboard)/booking/bookings-list.tsx @@ -1,7 +1,7 @@ "use client"; import { Badge } from "@coss/ui/components/badge"; -import { Button } from "@coss/ui/components/button"; +import { Button, buttonVariants } from "@coss/ui/components/button"; import { Card, CardFrame, @@ -400,16 +400,19 @@ function BookingListItem({ /> {showJoinLink && ( - + )} diff --git a/apps/examples/calcom/app/(dashboard)/booking/past/bookings-list.tsx b/apps/examples/calcom/app/(dashboard)/booking/past/bookings-list.tsx index 394ebe2f6..4109dc01b 100644 --- a/apps/examples/calcom/app/(dashboard)/booking/past/bookings-list.tsx +++ b/apps/examples/calcom/app/(dashboard)/booking/past/bookings-list.tsx @@ -1,7 +1,7 @@ "use client"; import { Badge } from "@coss/ui/components/badge"; -import { Button } from "@coss/ui/components/button"; +import { Button, buttonVariants } from "@coss/ui/components/button"; import { Card, CardFrame, @@ -201,15 +201,17 @@ export function BookingsList() {

{locationLabel && ( - + )} diff --git a/apps/examples/calcom/app/(settings)/settings/developer/webhooks/new/page.tsx b/apps/examples/calcom/app/(settings)/settings/developer/webhooks/new/page.tsx index cff31dada..335113821 100644 --- a/apps/examples/calcom/app/(settings)/settings/developer/webhooks/new/page.tsx +++ b/apps/examples/calcom/app/(settings)/settings/developer/webhooks/new/page.tsx @@ -1,4 +1,4 @@ -import { Button } from "@coss/ui/components/button"; +import { Button, buttonVariants } from "@coss/ui/components/button"; import { Card, CardFrame, @@ -36,12 +36,12 @@ export default function NewWebhookPage() { - + diff --git a/apps/examples/calcom/app/(settings)/settings/organizations/attributes/[id]/edit/page.tsx b/apps/examples/calcom/app/(settings)/settings/organizations/attributes/[id]/edit/page.tsx index 14c016631..f80e1385e 100644 --- a/apps/examples/calcom/app/(settings)/settings/organizations/attributes/[id]/edit/page.tsx +++ b/apps/examples/calcom/app/(settings)/settings/organizations/attributes/[id]/edit/page.tsx @@ -1,4 +1,4 @@ -import { Button } from "@coss/ui/components/button"; +import { Button, buttonVariants } from "@coss/ui/components/button"; import { Card, CardFrame, @@ -25,14 +25,13 @@ export default async function EditAttributePage({ <>
- + Update a custom attribute for your team members. diff --git a/apps/examples/calcom/app/(settings)/settings/organizations/attributes/attributes-section.tsx b/apps/examples/calcom/app/(settings)/settings/organizations/attributes/attributes-section.tsx index ef7002307..2777da699 100644 --- a/apps/examples/calcom/app/(settings)/settings/organizations/attributes/attributes-section.tsx +++ b/apps/examples/calcom/app/(settings)/settings/organizations/attributes/attributes-section.tsx @@ -9,7 +9,7 @@ import { AlertDialogPopup, AlertDialogTitle, } from "@coss/ui/components/alert-dialog"; -import { Button } from "@coss/ui/components/button"; +import { Button, buttonVariants } from "@coss/ui/components/button"; import { Card, CardFrame, @@ -127,12 +127,13 @@ export function AttributesSection({ - + ); @@ -144,14 +145,13 @@ export function AttributesSection({ Custom - + diff --git a/apps/examples/calcom/app/(settings)/settings/organizations/attributes/new/page.tsx b/apps/examples/calcom/app/(settings)/settings/organizations/attributes/new/page.tsx index d44c5462d..884fc8433 100644 --- a/apps/examples/calcom/app/(settings)/settings/organizations/attributes/new/page.tsx +++ b/apps/examples/calcom/app/(settings)/settings/organizations/attributes/new/page.tsx @@ -1,4 +1,4 @@ -import { Button } from "@coss/ui/components/button"; +import { Button, buttonVariants } from "@coss/ui/components/button"; import { Card, CardFrame, @@ -19,14 +19,13 @@ export default function NewAttributePage() { <>
- + Create a custom attribute for your team members. diff --git a/apps/examples/calcom/components/app/app-command.tsx b/apps/examples/calcom/components/app/app-command.tsx index 742151a7e..f2632c296 100644 --- a/apps/examples/calcom/components/app/app-command.tsx +++ b/apps/examples/calcom/components/app/app-command.tsx @@ -1,7 +1,7 @@ "use client"; import { useAutocompleteFilter } from "@coss/ui/components/autocomplete"; -import { Button } from "@coss/ui/components/button"; +import { Button, buttonVariants } from "@coss/ui/components/button"; import { Command, CommandCollection, @@ -696,14 +696,16 @@ export function AppCommand(): React.ReactElement { {aiState.referenceLinks.length > 0 && (
{aiState.referenceLinks.map((link, index) => ( - + ))}
)} diff --git a/apps/examples/calcom/components/settings/settings-mobile-header.tsx b/apps/examples/calcom/components/settings/settings-mobile-header.tsx index 87e228eef..665119c46 100644 --- a/apps/examples/calcom/components/settings/settings-mobile-header.tsx +++ b/apps/examples/calcom/components/settings/settings-mobile-header.tsx @@ -1,4 +1,4 @@ -import { Button } from "@coss/ui/components/button"; +import { buttonVariants } from "@coss/ui/components/button"; import { ArrowLeftIcon } from "lucide-react"; import Link from "next/link"; import type * as React from "react"; @@ -8,14 +8,14 @@ import { SettingsDrawer } from "@/components/settings/settings-drawer"; export function SettingsMobileHeader(): React.ReactElement { return ( - + ); diff --git a/apps/ui/app/docs/[[...slug]]/page.tsx b/apps/ui/app/docs/[[...slug]]/page.tsx index e8323a753..499094035 100644 --- a/apps/ui/app/docs/[[...slug]]/page.tsx +++ b/apps/ui/app/docs/[[...slug]]/page.tsx @@ -2,7 +2,7 @@ import { LinkSquare02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import Link from "next/link"; import { notFound } from "next/navigation"; -import { Button } from "@/registry/default/ui/button"; +import { buttonVariants } from "@/registry/default/ui/button"; import { Card, CardFrame, CardPanel } from "@/registry/default/ui/card"; import { ScrollArea } from "@/registry/default/ui/scroll-area"; import { DocsCopyPage } from "@/components/docs-copy-page"; @@ -77,23 +77,21 @@ export default async function Page(props: {
{links?.doc && ( -
diff --git a/apps/ui/app/not-found.tsx b/apps/ui/app/not-found.tsx index 5dd71547a..8a1fd123a 100644 --- a/apps/ui/app/not-found.tsx +++ b/apps/ui/app/not-found.tsx @@ -6,7 +6,7 @@ import { import { RiArrowLeftLine } from "@remixicon/react"; import type { Metadata } from "next"; import Link from "next/link"; -import { Button } from "@/registry/default/ui/button"; +import { buttonVariants } from "@/registry/default/ui/button"; export const metadata: Metadata = { description: @@ -24,19 +24,13 @@ export default function NotFound() { moved.
-
diff --git a/apps/ui/app/page.tsx b/apps/ui/app/page.tsx index 4e706d5ba..e89186c7e 100644 --- a/apps/ui/app/page.tsx +++ b/apps/ui/app/page.tsx @@ -7,7 +7,7 @@ import { SiteFooter } from "@coss/ui/shared/site-footer"; import type { Metadata } from "next"; import Link from "next/link"; import { Badge } from "@/registry/default/ui/badge"; -import { Button } from "@/registry/default/ui/button"; +import { buttonVariants } from "@/registry/default/ui/button"; import { Card, CardFrame, @@ -39,16 +39,15 @@ export default function Page() { {description}
- - +
diff --git a/apps/ui/app/particles/particle-card.tsx b/apps/ui/app/particles/particle-card.tsx index 53e01a1b3..1bc6606fd 100644 --- a/apps/ui/app/particles/particle-card.tsx +++ b/apps/ui/app/particles/particle-card.tsx @@ -3,7 +3,7 @@ import { InformationCircleIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { cache } from "react"; import { Index } from "@/registry/__index__"; -import { Button } from "@/registry/default/ui/button"; +import { Button, buttonVariants } from "@/registry/default/ui/button"; import { Drawer, DrawerContent, @@ -84,9 +84,11 @@ export async function ParticleCard({ /> - } + className={buttonVariants({ + className: "text-sm", + size: "sm", + variant: "outline", + })} > View code @@ -115,19 +117,15 @@ export async function ParticleCard({

Code

- + ))} ); diff --git a/apps/ui/public/r/button.json b/apps/ui/public/r/button.json index c8b25ec4b..d84b9f193 100644 --- a/apps/ui/public/r/button.json +++ b/apps/ui/public/r/button.json @@ -10,7 +10,7 @@ "files": [ { "path": "registry/default/ui/button.tsx", - "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\nexport const buttonVariants = cva(\n \"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-loading:select-none data-loading:text-transparent sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 px-[calc(--spacing(3)-1px)] sm:h-8\",\n icon: \"size-9 sm:size-8\",\n \"icon-lg\": \"size-10 sm:size-9\",\n \"icon-sm\": \"size-8 sm:size-7\",\n \"icon-xl\":\n \"size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5\",\n \"icon-xs\":\n \"size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5\",\n lg: \"h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9\",\n sm: \"h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7\",\n xl: \"h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5\",\n xs: \"h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5\",\n },\n variant: {\n default:\n \"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none\",\n destructive:\n \"not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none\",\n \"destructive-outline\":\n \"border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none\",\n ghost:\n \"border-transparent text-foreground hover:bg-accent data-pressed:bg-accent *:data-[slot=button-loading-indicator]:text-foreground\",\n link: \"border-transparent text-foreground underline-offset-4 hover:underline data-pressed:underline *:data-[slot=button-loading-indicator]:text-foreground\",\n outline:\n \"border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90 *:data-[slot=button-loading-indicator]:text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80\",\n },\n },\n },\n);\n\nexport interface ButtonProps extends useRender.ComponentProps<\"button\"> {\n variant?: VariantProps[\"variant\"];\n size?: VariantProps[\"size\"];\n loading?: boolean;\n}\n\nexport function Button({\n className,\n variant,\n size,\n render,\n children,\n loading = false,\n disabled: disabledProp,\n ...props\n}: ButtonProps): React.ReactElement {\n const isDisabled: boolean = Boolean(loading || disabledProp);\n const typeValue: React.ButtonHTMLAttributes[\"type\"] =\n render ? undefined : \"button\";\n\n const defaultProps = {\n children: (\n <>\n {children}\n {loading && (\n \n )}\n \n ),\n className: cn(buttonVariants({ className, size, variant })),\n \"aria-disabled\": loading || undefined,\n \"data-loading\": loading ? \"\" : undefined,\n \"data-slot\": \"button\",\n disabled: isDisabled,\n type: typeValue,\n };\n\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(defaultProps, props),\n render,\n });\n}\n", + "content": "import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\nexport const buttonVariants = cva(\n \"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-disabled:pointer-events-none data-loading:select-none data-loading:text-transparent data-disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 px-[calc(--spacing(3)-1px)] sm:h-8\",\n icon: \"size-9 sm:size-8\",\n \"icon-lg\": \"size-10 sm:size-9\",\n \"icon-sm\": \"size-8 sm:size-7\",\n \"icon-xl\":\n \"size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5\",\n \"icon-xs\":\n \"size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5\",\n lg: \"h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9\",\n sm: \"h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7\",\n xl: \"h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5\",\n xs: \"h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5\",\n },\n variant: {\n default:\n \"not-data-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [[data-disabled],:active,[data-pressed]]:shadow-none\",\n destructive:\n \"not-data-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [[data-disabled],:active,[data-pressed]]:shadow-none\",\n \"destructive-outline\":\n \"border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-data-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-data-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-data-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],:active,[data-pressed]]:shadow-none\",\n ghost:\n \"border-transparent text-foreground hover:bg-accent data-pressed:bg-accent *:data-[slot=button-loading-indicator]:text-foreground\",\n link: \"border-transparent text-foreground underline-offset-4 hover:underline data-pressed:underline *:data-[slot=button-loading-indicator]:text-foreground\",\n outline:\n \"border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-data-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-data-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-data-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],:active,[data-pressed]]:shadow-none\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90 *:data-[slot=button-loading-indicator]:text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80\",\n },\n },\n },\n);\n\nexport type ButtonProps = ButtonPrimitive.Props &\n VariantProps & {\n loading?: boolean;\n };\n\nexport function Button({\n className,\n variant,\n size,\n children,\n loading = false,\n disabled,\n focusableWhenDisabled,\n ...props\n}: ButtonProps): React.ReactElement {\n return (\n \n {children}\n {loading && (\n \n )}\n \n );\n}\n", "type": "registry:ui" } ], diff --git a/apps/ui/public/r/p-button-17.json b/apps/ui/public/r/p-button-17.json index e4666327f..db393c4de 100644 --- a/apps/ui/public/r/p-button-17.json +++ b/apps/ui/public/r/p-button-17.json @@ -8,7 +8,7 @@ "files": [ { "path": "registry/default/particles/p-button-17.tsx", - "content": "import Link from \"next/link\";\nimport { Button } from \"@/registry/default/ui/button\";\n\nexport default function Particle() {\n return ;\n}\n", + "content": "import Link from \"next/link\";\nimport { buttonVariants } from \"@/registry/default/ui/button\";\n\nexport default function Particle() {\n return (\n \n Link\n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-button-18.json b/apps/ui/public/r/p-button-18.json index f22844037..244ed1a4b 100644 --- a/apps/ui/public/r/p-button-18.json +++ b/apps/ui/public/r/p-button-18.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-button-18.tsx", - "content": "import { Button } from \"@/registry/default/ui/button\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\nexport default function Particle() {\n return (\n \n );\n}\n", + "content": "import { Button } from \"@/registry/default/ui/button\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\nexport default function Particle() {\n return (\n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-command-2.json b/apps/ui/public/r/p-command-2.json index 109f5e718..d7ff49561 100644 --- a/apps/ui/public/r/p-command-2.json +++ b/apps/ui/public/r/p-command-2.json @@ -19,7 +19,7 @@ "files": [ { "path": "registry/default/particles/p-command-2.tsx", - "content": "\"use client\";\n\nimport {\n ArrowDownIcon,\n ArrowLeftIcon,\n ArrowUpIcon,\n CircleQuestionMarkIcon,\n CornerDownLeftIcon,\n SearchIcon,\n SparklesIcon,\n} from \"lucide-react\";\nimport Link from \"next/link\";\nimport {\n Fragment,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAutocompleteFilter } from \"@/registry/default/ui/autocomplete\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Command,\n CommandCollection,\n CommandCreateHandle,\n CommandDialog,\n CommandDialogPopup,\n CommandEmpty,\n CommandFooter,\n CommandGroup,\n CommandGroupLabel,\n CommandInput,\n CommandItem,\n CommandList,\n CommandPanel,\n CommandSeparator,\n CommandShortcut,\n} from \"@/registry/default/ui/command\";\nimport { EmptyMedia } from \"@/registry/default/ui/empty\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport { Kbd, KbdGroup } from \"@/registry/default/ui/kbd\";\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\ninterface Item {\n value: string;\n label: string;\n shortcut?: string;\n keywords?: string[];\n}\n\ninterface Group {\n value: string;\n items: Item[];\n}\n\nconst commandGroups: Group[] = [\n {\n items: [\n {\n keywords: [\"dash\"],\n label: \"Dashboard\",\n shortcut: \"d\",\n value: \"dashboard\",\n },\n {\n keywords: [\"proj\"],\n label: \"Projects\",\n shortcut: \"p\",\n value: \"projects\",\n },\n { keywords: [\"team\"], label: \"Team\", shortcut: \"t\", value: \"team\" },\n ],\n value: \"Pages\",\n },\n {\n items: [\n {\n keywords: [\"prof\"],\n label: \"Profile\",\n shortcut: \"p s\",\n value: \"profile\",\n },\n {\n keywords: [\"acc\"],\n label: \"Account\",\n shortcut: \"a s\",\n value: \"account\",\n },\n {\n keywords: [\"pref\"],\n label: \"Preferences\",\n shortcut: \"p r\",\n value: \"preferences\",\n },\n ],\n value: \"Settings\",\n },\n {\n items: [\n {\n keywords: [\"docs\"],\n label: \"Documentation\",\n shortcut: \"d o\",\n value: \"docs\",\n },\n {\n keywords: [\"sup\"],\n label: \"Support\",\n shortcut: \"s u\",\n value: \"support\",\n },\n {\n keywords: [\"feed\"],\n label: \"Feedback\",\n shortcut: \"f b\",\n value: \"feedback\",\n },\n ],\n value: \"Help\",\n },\n];\n\nconst MOCK_AI_RESPONSE = `To create a new project, navigate to the Projects page and click the \"New Project\" button in the top right corner. You'll be prompted to enter a project name and description.\n\nOnce created, you can invite team members by clicking the \"Share\" button and entering their email addresses. Team members will receive an invitation link via email or you can add them manually by clicking the \"Add Team Member\" button in the project settings.\n\nYou can customize project settings at any time by clicking the settings icon in the project header. For more information, see the Project Settings documentation.`;\n\nconst MOCK_REFERENCE_LINKS = [\n { title: \"Creating Projects\", url: \"/docs/projects/create\" },\n { title: \"Team Collaboration\", url: \"/docs/team/collaborate\" },\n { title: \"Project Settings\", url: \"/docs/projects/settings\" },\n];\n\nexport const commandHandle: ReturnType =\n CommandCreateHandle();\n\ninterface AIState {\n mode: boolean;\n query: string;\n submittedQuery: string;\n response: string;\n referenceLinks: Array<{ title: string; url: string }>;\n isGenerating: boolean;\n error: string | null;\n}\n\nconst initialAIState: AIState = {\n error: null,\n isGenerating: false,\n mode: false,\n query: \"\",\n referenceLinks: [],\n response: \"\",\n submittedQuery: \"\",\n};\n\nfunction markdownToSafeHTML(markdown: string): string {\n // Simple markdown to HTML converter for demo purposes\n return markdown\n .split(\"\\n\\n\")\n .map((para) => `

${para}

`)\n .join(\"\");\n}\n\nexport default function PCommand2() {\n const [open, setOpen] = useState(false);\n const [aiState, setAIState] = useState(initialAIState);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const aiInputRef = useRef(null);\n const searchInputRef = useRef(null);\n const abortControllerRef = useRef(null);\n const commandResetKeyRef = useRef(0);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n abortControllerRef.current?.abort();\n };\n }, []);\n\n const resetAIState = useCallback(() => {\n abortControllerRef.current?.abort();\n setAIState(initialAIState);\n }, []);\n\n const handleItemClick = useCallback(() => {\n setOpen(false);\n }, []);\n\n const handleBackToSearch = useCallback(() => {\n resetAIState();\n setSearchQuery(\"\");\n commandResetKeyRef.current += 1;\n searchInputRef.current?.focus();\n }, [resetAIState]);\n\n const handleGenerateAI = useCallback(\n async (queryOverride?: string) => {\n const query = queryOverride || aiState.query;\n if (!query.trim()) return;\n\n abortControllerRef.current?.abort();\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n setAIState((prev) => ({\n ...prev,\n error: null,\n isGenerating: true,\n query: \"\",\n referenceLinks: [],\n response: \"\",\n submittedQuery: query,\n }));\n\n try {\n await new Promise((resolve, reject) => {\n const timeout = setTimeout(resolve, 1500);\n controller.signal.addEventListener(\"abort\", () => {\n clearTimeout(timeout);\n reject(new Error(\"aborted\"));\n });\n });\n\n if (controller.signal.aborted) return;\n\n setAIState((prev) => ({\n ...prev,\n isGenerating: false,\n referenceLinks: MOCK_REFERENCE_LINKS,\n response: MOCK_AI_RESPONSE,\n }));\n } catch (error) {\n if (error instanceof Error && error.message === \"aborted\") {\n return;\n }\n\n if (controller.signal.aborted) return;\n\n setAIState((prev) => ({\n ...prev,\n error: \"Failed to generate response. Please try again.\",\n isGenerating: false,\n }));\n }\n },\n [aiState.query],\n );\n\n const handleAskAI = useCallback(() => {\n const currentQuery = searchQuery;\n setSearchQuery(\"\");\n\n if (currentQuery.trim()) {\n setAIState((prev) => ({ ...prev, mode: true }));\n handleGenerateAI(currentQuery);\n } else {\n setAIState((prev) => ({ ...prev, mode: true, query: \"\" }));\n aiInputRef.current?.focus();\n }\n }, [searchQuery, handleGenerateAI]);\n\n const { contains } = useAutocompleteFilter({ sensitivity: \"base\" });\n\n const filterItem = useCallback(\n (itemValue: unknown, query: string): boolean => {\n if (typeof itemValue !== \"object\" || itemValue === null) {\n return false;\n }\n\n const item = itemValue as Item;\n\n if (contains(item.label, query)) {\n return true;\n }\n\n if (contains(item.value, query)) {\n return true;\n }\n\n if (item.keywords?.some((keyword) => contains(keyword, query))) {\n return true;\n }\n\n return false;\n },\n [contains],\n );\n\n useEffect(() => {\n if (!open || !aiState.mode) return;\n\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n handleBackToSearch();\n }\n };\n\n document.addEventListener(\"keydown\", handleEscape, true);\n return () => document.removeEventListener(\"keydown\", handleEscape, true);\n }, [open, aiState.mode, handleBackToSearch]);\n\n useEffect(() => {\n if (aiState.mode && !aiState.isGenerating) {\n aiInputRef.current?.focus();\n }\n }, [aiState.mode, aiState.isGenerating]);\n\n const hasResults = useMemo(\n () =>\n !searchQuery.trim() ||\n commandGroups.some((group) =>\n group.items.some((item) => filterItem(item, searchQuery)),\n ),\n [searchQuery, filterItem],\n );\n\n const handleOpenChange = useCallback(\n (newOpen: boolean) => {\n setOpen(newOpen);\n if (!newOpen) {\n setSearchQuery(\"\");\n resetAIState();\n }\n },\n [resetAIState],\n );\n\n return (\n <>\n \n \n \n {!aiState.mode ? (\n \n
\n setSearchQuery(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Tab\") {\n e.preventDefault();\n handleAskAI();\n }\n if (\n e.key === \"Enter\" &&\n !hasResults &&\n searchQuery.trim()\n ) {\n e.preventDefault();\n handleAskAI();\n }\n }}\n placeholder=\"Type a command or search...\"\n ref={searchInputRef}\n value={searchQuery}\n />\n \n \n Ask AI\n Tab\n \n
\n \n \n {searchQuery.trim() && (\n
\n \n \n \n

No results found.

\n

\n Press Enter to ask AI about:\n
{\" \"}\n \n {searchQuery}\n \n

\n
\n )}\n
\n \n {(group: Group) => (\n \n \n {group.value}\n \n {(item: Item) => (\n \n {item.label}\n {item.shortcut && (\n \n {item.shortcut}\n \n )}\n \n )}\n \n \n \n \n )}\n \n
\n \n {hasResults ? (\n <>\n
\n
\n \n \n \n \n \n \n \n \n Navigate\n
\n
\n \n \n \n Open\n
\n
\n
\n Esc\n Close\n
\n \n ) : (\n
\n Esc\n Close\n
\n )}\n
\n \n ) : (\n \n
\n
\n
\n \n \n
\n \n setAIState((prev) => ({\n ...prev,\n query: e.target.value,\n }))\n }\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !aiState.isGenerating) {\n handleGenerateAI();\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n handleBackToSearch();\n }\n }}\n placeholder=\"Ask AI anything…\"\n ref={aiInputRef}\n size=\"lg\"\n value={aiState.query}\n />\n
\n
\n \n \n Back to search\n Esc\n \n \n \n \n
\n {!aiState.isGenerating &&\n !aiState.response &&\n !aiState.error && (\n
\n

\n Ask AI anything and press Enter to get\n started.\n

\n
\n )}\n\n {aiState.error && (\n \n {aiState.error}\n
\n )}\n\n {aiState.isGenerating && (\n
\n
\n \n \n \n \n
\n
\n \n \n \n
\n
\n \n \n \n \n
\n
\n )}\n\n {aiState.response && !aiState.isGenerating && (\n <>\n \n {aiState.referenceLinks.length > 0 && (\n
\n {aiState.referenceLinks.map((link, index) => (\n }\n size=\"sm\"\n variant=\"secondary\"\n >\n {link.title}\n \n ))}\n
\n )}\n \n )}\n \n
\n
\n\n \n {aiState.isGenerating ? (\n
\n
\n \n
\n Generating response…\n
\n ) : aiState.response ? (\n
\n
\n \n
\n You asked: "{aiState.submittedQuery}"\n
\n ) : (\n
\n \n \n \n Ask AI\n
\n )}\n
\n
\n )}\n
\n \n \n );\n}\n", + "content": "\"use client\";\n\nimport {\n ArrowDownIcon,\n ArrowLeftIcon,\n ArrowUpIcon,\n CircleQuestionMarkIcon,\n CornerDownLeftIcon,\n SearchIcon,\n SparklesIcon,\n} from \"lucide-react\";\nimport Link from \"next/link\";\nimport {\n Fragment,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAutocompleteFilter } from \"@/registry/default/ui/autocomplete\";\nimport { Button, buttonVariants } from \"@/registry/default/ui/button\";\nimport {\n Command,\n CommandCollection,\n CommandCreateHandle,\n CommandDialog,\n CommandDialogPopup,\n CommandEmpty,\n CommandFooter,\n CommandGroup,\n CommandGroupLabel,\n CommandInput,\n CommandItem,\n CommandList,\n CommandPanel,\n CommandSeparator,\n CommandShortcut,\n} from \"@/registry/default/ui/command\";\nimport { EmptyMedia } from \"@/registry/default/ui/empty\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport { Kbd, KbdGroup } from \"@/registry/default/ui/kbd\";\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\ninterface Item {\n value: string;\n label: string;\n shortcut?: string;\n keywords?: string[];\n}\n\ninterface Group {\n value: string;\n items: Item[];\n}\n\nconst commandGroups: Group[] = [\n {\n items: [\n {\n keywords: [\"dash\"],\n label: \"Dashboard\",\n shortcut: \"d\",\n value: \"dashboard\",\n },\n {\n keywords: [\"proj\"],\n label: \"Projects\",\n shortcut: \"p\",\n value: \"projects\",\n },\n { keywords: [\"team\"], label: \"Team\", shortcut: \"t\", value: \"team\" },\n ],\n value: \"Pages\",\n },\n {\n items: [\n {\n keywords: [\"prof\"],\n label: \"Profile\",\n shortcut: \"p s\",\n value: \"profile\",\n },\n {\n keywords: [\"acc\"],\n label: \"Account\",\n shortcut: \"a s\",\n value: \"account\",\n },\n {\n keywords: [\"pref\"],\n label: \"Preferences\",\n shortcut: \"p r\",\n value: \"preferences\",\n },\n ],\n value: \"Settings\",\n },\n {\n items: [\n {\n keywords: [\"docs\"],\n label: \"Documentation\",\n shortcut: \"d o\",\n value: \"docs\",\n },\n {\n keywords: [\"sup\"],\n label: \"Support\",\n shortcut: \"s u\",\n value: \"support\",\n },\n {\n keywords: [\"feed\"],\n label: \"Feedback\",\n shortcut: \"f b\",\n value: \"feedback\",\n },\n ],\n value: \"Help\",\n },\n];\n\nconst MOCK_AI_RESPONSE = `To create a new project, navigate to the Projects page and click the \"New Project\" button in the top right corner. You'll be prompted to enter a project name and description.\n\nOnce created, you can invite team members by clicking the \"Share\" button and entering their email addresses. Team members will receive an invitation link via email or you can add them manually by clicking the \"Add Team Member\" button in the project settings.\n\nYou can customize project settings at any time by clicking the settings icon in the project header. For more information, see the Project Settings documentation.`;\n\nconst MOCK_REFERENCE_LINKS = [\n { title: \"Creating Projects\", url: \"/docs/projects/create\" },\n { title: \"Team Collaboration\", url: \"/docs/team/collaborate\" },\n { title: \"Project Settings\", url: \"/docs/projects/settings\" },\n];\n\nexport const commandHandle: ReturnType =\n CommandCreateHandle();\n\ninterface AIState {\n mode: boolean;\n query: string;\n submittedQuery: string;\n response: string;\n referenceLinks: Array<{ title: string; url: string }>;\n isGenerating: boolean;\n error: string | null;\n}\n\nconst initialAIState: AIState = {\n error: null,\n isGenerating: false,\n mode: false,\n query: \"\",\n referenceLinks: [],\n response: \"\",\n submittedQuery: \"\",\n};\n\nfunction markdownToSafeHTML(markdown: string): string {\n // Simple markdown to HTML converter for demo purposes\n return markdown\n .split(\"\\n\\n\")\n .map((para) => `

${para}

`)\n .join(\"\");\n}\n\nexport default function PCommand2() {\n const [open, setOpen] = useState(false);\n const [aiState, setAIState] = useState(initialAIState);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const aiInputRef = useRef(null);\n const searchInputRef = useRef(null);\n const abortControllerRef = useRef(null);\n const commandResetKeyRef = useRef(0);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n abortControllerRef.current?.abort();\n };\n }, []);\n\n const resetAIState = useCallback(() => {\n abortControllerRef.current?.abort();\n setAIState(initialAIState);\n }, []);\n\n const handleItemClick = useCallback(() => {\n setOpen(false);\n }, []);\n\n const handleBackToSearch = useCallback(() => {\n resetAIState();\n setSearchQuery(\"\");\n commandResetKeyRef.current += 1;\n searchInputRef.current?.focus();\n }, [resetAIState]);\n\n const handleGenerateAI = useCallback(\n async (queryOverride?: string) => {\n const query = queryOverride || aiState.query;\n if (!query.trim()) return;\n\n abortControllerRef.current?.abort();\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n setAIState((prev) => ({\n ...prev,\n error: null,\n isGenerating: true,\n query: \"\",\n referenceLinks: [],\n response: \"\",\n submittedQuery: query,\n }));\n\n try {\n await new Promise((resolve, reject) => {\n const timeout = setTimeout(resolve, 1500);\n controller.signal.addEventListener(\"abort\", () => {\n clearTimeout(timeout);\n reject(new Error(\"aborted\"));\n });\n });\n\n if (controller.signal.aborted) return;\n\n setAIState((prev) => ({\n ...prev,\n isGenerating: false,\n referenceLinks: MOCK_REFERENCE_LINKS,\n response: MOCK_AI_RESPONSE,\n }));\n } catch (error) {\n if (error instanceof Error && error.message === \"aborted\") {\n return;\n }\n\n if (controller.signal.aborted) return;\n\n setAIState((prev) => ({\n ...prev,\n error: \"Failed to generate response. Please try again.\",\n isGenerating: false,\n }));\n }\n },\n [aiState.query],\n );\n\n const handleAskAI = useCallback(() => {\n const currentQuery = searchQuery;\n setSearchQuery(\"\");\n\n if (currentQuery.trim()) {\n setAIState((prev) => ({ ...prev, mode: true }));\n handleGenerateAI(currentQuery);\n } else {\n setAIState((prev) => ({ ...prev, mode: true, query: \"\" }));\n aiInputRef.current?.focus();\n }\n }, [searchQuery, handleGenerateAI]);\n\n const { contains } = useAutocompleteFilter({ sensitivity: \"base\" });\n\n const filterItem = useCallback(\n (itemValue: unknown, query: string): boolean => {\n if (typeof itemValue !== \"object\" || itemValue === null) {\n return false;\n }\n\n const item = itemValue as Item;\n\n if (contains(item.label, query)) {\n return true;\n }\n\n if (contains(item.value, query)) {\n return true;\n }\n\n if (item.keywords?.some((keyword) => contains(keyword, query))) {\n return true;\n }\n\n return false;\n },\n [contains],\n );\n\n useEffect(() => {\n if (!open || !aiState.mode) return;\n\n const handleEscape = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n handleBackToSearch();\n }\n };\n\n document.addEventListener(\"keydown\", handleEscape, true);\n return () => document.removeEventListener(\"keydown\", handleEscape, true);\n }, [open, aiState.mode, handleBackToSearch]);\n\n useEffect(() => {\n if (aiState.mode && !aiState.isGenerating) {\n aiInputRef.current?.focus();\n }\n }, [aiState.mode, aiState.isGenerating]);\n\n const hasResults = useMemo(\n () =>\n !searchQuery.trim() ||\n commandGroups.some((group) =>\n group.items.some((item) => filterItem(item, searchQuery)),\n ),\n [searchQuery, filterItem],\n );\n\n const handleOpenChange = useCallback(\n (newOpen: boolean) => {\n setOpen(newOpen);\n if (!newOpen) {\n setSearchQuery(\"\");\n resetAIState();\n }\n },\n [resetAIState],\n );\n\n return (\n <>\n \n \n \n {!aiState.mode ? (\n \n
\n setSearchQuery(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Tab\") {\n e.preventDefault();\n handleAskAI();\n }\n if (\n e.key === \"Enter\" &&\n !hasResults &&\n searchQuery.trim()\n ) {\n e.preventDefault();\n handleAskAI();\n }\n }}\n placeholder=\"Type a command or search...\"\n ref={searchInputRef}\n value={searchQuery}\n />\n \n \n Ask AI\n Tab\n \n
\n \n \n {searchQuery.trim() && (\n
\n \n \n \n

No results found.

\n

\n Press Enter to ask AI about:\n
{\" \"}\n \n {searchQuery}\n \n

\n
\n )}\n
\n \n {(group: Group) => (\n \n \n {group.value}\n \n {(item: Item) => (\n \n {item.label}\n {item.shortcut && (\n \n {item.shortcut}\n \n )}\n \n )}\n \n \n \n \n )}\n \n
\n \n {hasResults ? (\n <>\n
\n
\n \n \n \n \n \n \n \n \n Navigate\n
\n
\n \n \n \n Open\n
\n
\n
\n Esc\n Close\n
\n \n ) : (\n
\n Esc\n Close\n
\n )}\n
\n \n ) : (\n \n
\n
\n
\n \n \n
\n \n setAIState((prev) => ({\n ...prev,\n query: e.target.value,\n }))\n }\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !aiState.isGenerating) {\n handleGenerateAI();\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n handleBackToSearch();\n }\n }}\n placeholder=\"Ask AI anything…\"\n ref={aiInputRef}\n size=\"lg\"\n value={aiState.query}\n />\n
\n
\n \n \n Back to search\n Esc\n \n \n \n \n
\n {!aiState.isGenerating &&\n !aiState.response &&\n !aiState.error && (\n
\n

\n Ask AI anything and press Enter to get\n started.\n

\n
\n )}\n\n {aiState.error && (\n \n {aiState.error}\n
\n )}\n\n {aiState.isGenerating && (\n
\n
\n \n \n \n \n
\n
\n \n \n \n
\n
\n \n \n \n \n
\n
\n )}\n\n {aiState.response && !aiState.isGenerating && (\n <>\n \n {aiState.referenceLinks.length > 0 && (\n
\n {aiState.referenceLinks.map((link, index) => (\n \n {link.title}\n \n ))}\n
\n )}\n \n )}\n \n
\n
\n\n \n {aiState.isGenerating ? (\n
\n
\n \n
\n Generating response…\n
\n ) : aiState.response ? (\n
\n
\n \n
\n You asked: "{aiState.submittedQuery}"\n
\n ) : (\n
\n \n \n \n Ask AI\n
\n )}\n
\n
\n )}\n
\n \n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-drawer-11.json b/apps/ui/public/r/p-drawer-11.json index d0eca4e04..b5d014f2e 100644 --- a/apps/ui/public/r/p-drawer-11.json +++ b/apps/ui/public/r/p-drawer-11.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-drawer-11.tsx", - "content": "import Link from \"next/link\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Drawer,\n DrawerClose,\n DrawerHeader,\n DrawerPanel,\n DrawerPopup,\n DrawerTitle,\n DrawerTrigger,\n} from \"@/registry/default/ui/drawer\";\n\nexport default function Particle() {\n return (\n \n }>\n Open menu\n \n \n \n Menu\n \n \n \n \n \n \n );\n}\n", + "content": "import Link from \"next/link\";\nimport { Button, buttonVariants } from \"@/registry/default/ui/button\";\nimport {\n Drawer,\n DrawerClose,\n DrawerHeader,\n DrawerPanel,\n DrawerPopup,\n DrawerTitle,\n DrawerTrigger,\n} from \"@/registry/default/ui/drawer\";\n\nexport default function Particle() {\n return (\n \n }>\n Open menu\n \n \n \n Menu\n \n \n \n \n \n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-drawer-14.json b/apps/ui/public/r/p-drawer-14.json index 2f112aa89..027a40d6f 100644 --- a/apps/ui/public/r/p-drawer-14.json +++ b/apps/ui/public/r/p-drawer-14.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-drawer-14.tsx", - "content": "import Link from \"next/link\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Drawer,\n DrawerHeader,\n DrawerPanel,\n DrawerPopup,\n DrawerSwipeArea,\n DrawerTitle,\n} from \"@/registry/default/ui/drawer\";\n\nexport default function Particle() {\n return (\n
\n \n \n \n Swipe area\n \n \n\n
\n

\n Swipe from the left edge to open the menu.\n

\n
\n\n \n \n Menu\n \n \n \n \n \n
\n
\n );\n}\n", + "content": "import Link from \"next/link\";\nimport { buttonVariants } from \"@/registry/default/ui/button\";\nimport {\n Drawer,\n DrawerHeader,\n DrawerPanel,\n DrawerPopup,\n DrawerSwipeArea,\n DrawerTitle,\n} from \"@/registry/default/ui/drawer\";\n\nexport default function Particle() {\n return (\n
\n \n \n \n Swipe area\n \n \n\n
\n

\n Swipe from the left edge to open the menu.\n

\n
\n\n \n \n Menu\n \n \n \n \n \n
\n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-pagination-2.json b/apps/ui/public/r/p-pagination-2.json index 42aad84bc..0edf340e1 100644 --- a/apps/ui/public/r/p-pagination-2.json +++ b/apps/ui/public/r/p-pagination-2.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-pagination-2.tsx", - "content": "import Link from \"next/link\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Pagination,\n PaginationContent,\n PaginationItem,\n} from \"@/registry/default/ui/pagination\";\n\ntype PaginationProps = {\n currentPage: number;\n totalPages: number;\n};\n\nexport default function Particle({ currentPage, totalPages }: PaginationProps) {\n return (\n \n \n \n \n )\n }\n variant=\"outline\"\n >\n Previous\n \n \n \n \n )\n }\n variant=\"outline\"\n >\n Next\n \n \n \n \n );\n}\n", + "content": "import Link from \"next/link\";\nimport { Button, buttonVariants } from \"@/registry/default/ui/button\";\nimport {\n Pagination,\n PaginationContent,\n PaginationItem,\n} from \"@/registry/default/ui/pagination\";\n\ntype PaginationProps = {\n currentPage: number;\n totalPages: number;\n};\n\nexport default function Particle({ currentPage, totalPages }: PaginationProps) {\n return (\n \n \n \n {currentPage === 1 ? (\n \n ) : (\n \n Previous\n \n )}\n \n \n {currentPage === totalPages ? (\n \n ) : (\n \n Next\n \n )}\n \n \n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/registry/default/particles/p-button-17.tsx b/apps/ui/registry/default/particles/p-button-17.tsx index 31af4e966..03c1d2596 100644 --- a/apps/ui/registry/default/particles/p-button-17.tsx +++ b/apps/ui/registry/default/particles/p-button-17.tsx @@ -1,6 +1,10 @@ import Link from "next/link"; -import { Button } from "@/registry/default/ui/button"; +import { buttonVariants } from "@/registry/default/ui/button"; export default function Particle() { - return ; + return ( + + Link + + ); } diff --git a/apps/ui/registry/default/particles/p-button-18.tsx b/apps/ui/registry/default/particles/p-button-18.tsx index 092941f99..cff42be09 100644 --- a/apps/ui/registry/default/particles/p-button-18.tsx +++ b/apps/ui/registry/default/particles/p-button-18.tsx @@ -3,7 +3,7 @@ import { Spinner } from "@/registry/default/ui/spinner"; export default function Particle() { return ( - diff --git a/apps/ui/registry/default/particles/p-command-2.tsx b/apps/ui/registry/default/particles/p-command-2.tsx index 834ce46b4..ad184c122 100644 --- a/apps/ui/registry/default/particles/p-command-2.tsx +++ b/apps/ui/registry/default/particles/p-command-2.tsx @@ -19,7 +19,7 @@ import { useState, } from "react"; import { useAutocompleteFilter } from "@/registry/default/ui/autocomplete"; -import { Button } from "@/registry/default/ui/button"; +import { Button, buttonVariants } from "@/registry/default/ui/button"; import { Command, CommandCollection, @@ -568,14 +568,16 @@ export default function PCommand2() { {aiState.referenceLinks.length > 0 && (
{aiState.referenceLinks.map((link, index) => ( - + ))}
)} diff --git a/apps/ui/registry/default/particles/p-drawer-11.tsx b/apps/ui/registry/default/particles/p-drawer-11.tsx index 393ce6a79..85a8f9f1a 100644 --- a/apps/ui/registry/default/particles/p-drawer-11.tsx +++ b/apps/ui/registry/default/particles/p-drawer-11.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Button } from "@/registry/default/ui/button"; +import { Button, buttonVariants } from "@/registry/default/ui/button"; import { Drawer, DrawerClose, @@ -25,10 +25,12 @@ export default function Particle() { } - variant="ghost" + } > @@ -37,10 +39,12 @@ export default function Particle() { } - variant="ghost" + } > @@ -49,10 +53,12 @@ export default function Particle() { } - variant="ghost" + } > @@ -61,10 +67,12 @@ export default function Particle() { } - variant="ghost" + } > diff --git a/apps/ui/registry/default/particles/p-drawer-14.tsx b/apps/ui/registry/default/particles/p-drawer-14.tsx index dfec38658..2a79f16f8 100644 --- a/apps/ui/registry/default/particles/p-drawer-14.tsx +++ b/apps/ui/registry/default/particles/p-drawer-14.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Button } from "@/registry/default/ui/button"; +import { buttonVariants } from "@/registry/default/ui/button"; import { Drawer, DrawerHeader, @@ -31,34 +31,42 @@ export default function Particle() { diff --git a/apps/ui/registry/default/particles/p-pagination-2.tsx b/apps/ui/registry/default/particles/p-pagination-2.tsx index 23a5e404c..5fb8b2670 100644 --- a/apps/ui/registry/default/particles/p-pagination-2.tsx +++ b/apps/ui/registry/default/particles/p-pagination-2.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Button } from "@/registry/default/ui/button"; +import { Button, buttonVariants } from "@/registry/default/ui/button"; import { Pagination, PaginationContent, @@ -16,30 +16,32 @@ export default function Particle({ currentPage, totalPages }: PaginationProps) { - + {currentPage === 1 ? ( + + ) : ( + + Previous + + )} - + {currentPage === totalPages ? ( + + ) : ( + + Next + + )} diff --git a/apps/ui/registry/default/ui/button.tsx b/apps/ui/registry/default/ui/button.tsx index 0116153f7..37ab25f18 100644 --- a/apps/ui/registry/default/ui/button.tsx +++ b/apps/ui/registry/default/ui/button.tsx @@ -1,14 +1,11 @@ -"use client"; - -import { mergeProps } from "@base-ui/react/merge-props"; -import { useRender } from "@base-ui/react/use-render"; +import { Button as ButtonPrimitive } from "@base-ui/react/button"; import { cva, type VariantProps } from "class-variance-authority"; import type * as React from "react"; import { cn } from "@/registry/default/lib/utils"; import { Spinner } from "@/registry/default/ui/spinner"; export const buttonVariants = cva( - "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-loading:select-none data-loading:text-transparent sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0", + "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-disabled:pointer-events-none data-loading:select-none data-loading:text-transparent data-disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0", { defaultVariants: { size: "default", @@ -31,16 +28,16 @@ export const buttonVariants = cva( }, variant: { default: - "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none", + "not-data-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [[data-disabled],:active,[data-pressed]]:shadow-none", destructive: - "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none", + "not-data-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [[data-disabled],:active,[data-pressed]]:shadow-none", "destructive-outline": - "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none", + "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-data-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-data-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-data-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],:active,[data-pressed]]:shadow-none", ghost: "border-transparent text-foreground hover:bg-accent data-pressed:bg-accent *:data-[slot=button-loading-indicator]:text-foreground", link: "border-transparent text-foreground underline-offset-4 hover:underline data-pressed:underline *:data-[slot=button-loading-indicator]:text-foreground", outline: - "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none", + "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-data-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-data-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-data-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],:active,[data-pressed]]:shadow-none", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90 *:data-[slot=button-loading-indicator]:text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80", }, @@ -48,49 +45,37 @@ export const buttonVariants = cva( }, ); -export interface ButtonProps extends useRender.ComponentProps<"button"> { - variant?: VariantProps["variant"]; - size?: VariantProps["size"]; - loading?: boolean; -} +export type ButtonProps = ButtonPrimitive.Props & + VariantProps & { + loading?: boolean; + }; export function Button({ className, variant, size, - render, children, loading = false, - disabled: disabledProp, + disabled, + focusableWhenDisabled, ...props }: ButtonProps): React.ReactElement { - const isDisabled: boolean = Boolean(loading || disabledProp); - const typeValue: React.ButtonHTMLAttributes["type"] = - render ? undefined : "button"; - - const defaultProps = { - children: ( - <> - {children} - {loading && ( - - )} - - ), - className: cn(buttonVariants({ className, size, variant })), - "aria-disabled": loading || undefined, - "data-loading": loading ? "" : undefined, - "data-slot": "button", - disabled: isDisabled, - type: typeValue, - }; - - return useRender({ - defaultTagName: "button", - props: mergeProps<"button">(defaultProps, props), - render, - }); + return ( + + {children} + {loading && ( + + )} + + ); } diff --git a/apps/www/app/not-found.tsx b/apps/www/app/not-found.tsx index fea4bc84b..68ec1e642 100644 --- a/apps/www/app/not-found.tsx +++ b/apps/www/app/not-found.tsx @@ -1,4 +1,4 @@ -import { Button } from "@coss/ui/components/button"; +import { buttonVariants } from "@coss/ui/components/button"; import { PageHeader, PageHeaderDescription, @@ -24,19 +24,13 @@ export default function NotFound() { moved.
-
diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index b44298d53..337d24186 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -1,14 +1,11 @@ -"use client"; - -import { mergeProps } from "@base-ui/react/merge-props"; -import { useRender } from "@base-ui/react/use-render"; +import { Button as ButtonPrimitive } from "@base-ui/react/button"; import { Spinner } from "@coss/ui/components/spinner"; import { cn } from "@coss/ui/lib/utils"; import { cva, type VariantProps } from "class-variance-authority"; import type * as React from "react"; export const buttonVariants = cva( - "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-loading:select-none data-loading:text-transparent sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0", + "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-disabled:pointer-events-none data-loading:select-none data-loading:text-transparent data-disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0", { defaultVariants: { size: "default", @@ -31,16 +28,16 @@ export const buttonVariants = cva( }, variant: { default: - "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none", + "not-data-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs hover:bg-primary/90 data-pressed:bg-primary/90 *:data-[slot=button-loading-indicator]:text-primary-foreground [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [[data-disabled],:active,[data-pressed]]:shadow-none", destructive: - "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none", + "not-data-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs hover:bg-destructive/90 data-pressed:bg-destructive/90 *:data-[slot=button-loading-indicator]:text-white [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [[data-disabled],:active,[data-pressed]]:shadow-none", "destructive-outline": - "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none", + "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-data-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:border-destructive/32 hover:bg-destructive/4 data-pressed:border-destructive/32 data-pressed:bg-destructive/4 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:not-data-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-data-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],:active,[data-pressed]]:shadow-none", ghost: "border-transparent text-foreground hover:bg-accent data-pressed:bg-accent *:data-[slot=button-loading-indicator]:text-foreground", link: "border-transparent text-foreground underline-offset-4 hover:underline data-pressed:underline *:data-[slot=button-loading-indicator]:text-foreground", outline: - "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none", + "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-data-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] hover:bg-accent/50 data-pressed:bg-accent/50 *:data-[slot=button-loading-indicator]:text-foreground dark:bg-input/32 dark:data-pressed:bg-input/64 dark:hover:bg-input/64 dark:not-data-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-data-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],:active,[data-pressed]]:shadow-none", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90 *:data-[slot=button-loading-indicator]:text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80", }, @@ -48,49 +45,37 @@ export const buttonVariants = cva( }, ); -export interface ButtonProps extends useRender.ComponentProps<"button"> { - variant?: VariantProps["variant"]; - size?: VariantProps["size"]; - loading?: boolean; -} +export type ButtonProps = ButtonPrimitive.Props & + VariantProps & { + loading?: boolean; + }; export function Button({ className, variant, size, - render, children, loading = false, - disabled: disabledProp, + disabled, + focusableWhenDisabled, ...props }: ButtonProps): React.ReactElement { - const isDisabled: boolean = Boolean(loading || disabledProp); - const typeValue: React.ButtonHTMLAttributes["type"] = - render ? undefined : "button"; - - const defaultProps = { - children: ( - <> - {children} - {loading && ( - - )} - - ), - className: cn(buttonVariants({ className, size, variant })), - "aria-disabled": loading || undefined, - "data-loading": loading ? "" : undefined, - "data-slot": "button", - disabled: isDisabled, - type: typeValue, - }; - - return useRender({ - defaultTagName: "button", - props: mergeProps<"button">(defaultProps, props), - render, - }); + return ( + + {children} + {loading && ( + + )} + + ); } diff --git a/packages/ui/src/shared/github-link-client.tsx b/packages/ui/src/shared/github-link-client.tsx index 9492cf4ab..c5ed6b90d 100644 --- a/packages/ui/src/shared/github-link-client.tsx +++ b/packages/ui/src/shared/github-link-client.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button } from "@coss/ui/components/button"; +import { buttonVariants } from "@coss/ui/components/button"; import { siteConfig } from "@coss/ui/lib/config"; import { GithubIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -9,16 +9,18 @@ import type * as React from "react"; export function GitHubLinkClient({ stars }: { stars: React.ReactNode }) { return ( - - + ); diff --git a/packages/ui/test/components/button.test.tsx b/packages/ui/test/components/button.test.tsx index a653fdde3..11cbbc63a 100644 --- a/packages/ui/test/components/button.test.tsx +++ b/packages/ui/test/components/button.test.tsx @@ -1,81 +1,70 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"; -import * as React from "react"; +import type * as React from "react"; -type UseRenderConfig = { - defaultTagName: string; - render?: (...args: unknown[]) => unknown; - props: Record; +type ButtonPrimitiveProps = Record & { + children?: React.ReactNode; }; -const useRenderCalls: UseRenderConfig[] = []; -const mergePropsCalls: Array< - [Record, Record] -> = []; +const buttonPrimitiveCalls: ButtonPrimitiveProps[] = []; -function useRenderMock(config: UseRenderConfig) { - useRenderCalls.push(config); +function mockButtonPrimitive(props: ButtonPrimitiveProps) { + buttonPrimitiveCalls.push(props); return null; } -function mergePropsMock( - defaults: Record, - overrides: Record = {}, -) { - mergePropsCalls.push([defaults, overrides]); - return { ...defaults, ...overrides }; -} - -mock.module("@base-ui/react/use-render", () => ({ - useRender: useRenderMock, -})); - -mock.module("@base-ui/react/merge-props", () => ({ - mergeProps: mergePropsMock, +mock.module("@base-ui/react/button", () => ({ + Button: mockButtonPrimitive, })); -const { Button } = await import("../../src/components/button"); +const { Button: ButtonComponent } = await import("../../src/components/button"); -function lastUseRenderCall() { - const lastCall = useRenderCalls[useRenderCalls.length - 1]; - if (!lastCall) { - throw new Error("useRender was not called"); - } - return lastCall; +function Button(props: Parameters[0]) { + const element = ButtonComponent(props); + (element.type as (props: unknown) => unknown)(element.props); } -function lastMergePropsCall() { - const lastCall = mergePropsCalls[mergePropsCalls.length - 1]; +function lastButtonPrimitiveCall() { + const lastCall = buttonPrimitiveCalls[buttonPrimitiveCalls.length - 1]; if (!lastCall) { - throw new Error("mergeProps was not called"); + throw new Error("ButtonPrimitive was not called"); } return lastCall; } describe("Button", () => { beforeEach(() => { - useRenderCalls.length = 0; - mergePropsCalls.length = 0; + buttonPrimitiveCalls.length = 0; }); - test("sets base attributes and defaults type to button", () => { + test("sets base attributes and forwards children", () => { Button({ children: "Default label" }); - const call = lastUseRenderCall(); - expect(call.defaultTagName).toBe("button"); - expect(call.props["data-slot"]).toBe("button"); - expect(call.props.type).toBe("button"); + const call = lastButtonPrimitiveCall(); + expect(call["data-slot"]).toBe("button"); + expect(call.disabled).toBe(false); + expect(call.focusableWhenDisabled).toBe(false); + expect(call.children).toBeDefined(); }); - test("omits the default type when a custom render is provided", () => { - const render = () => React.createElement("div"); - Button({ render, type: "submit" }); + test("disables and stays focusable while loading", () => { + Button({ children: "Submit", loading: true }); + + const call = lastButtonPrimitiveCall(); + expect(call.disabled).toBe(true); + expect(call.focusableWhenDisabled).toBe(true); + expect(call["data-loading"]).toBe(""); + }); - const [defaults, overrides] = lastMergePropsCall(); - expect(defaults.type).toBeUndefined(); - expect(overrides.type).toBe("submit"); + test("respects an explicit focusableWhenDisabled override", () => { + Button({ + children: "Submit", + focusableWhenDisabled: false, + loading: true, + }); - const call = lastUseRenderCall(); - expect(call.render).toBe(render); + const call = lastButtonPrimitiveCall(); + expect(call.disabled).toBe(true); + expect(call.focusableWhenDisabled).toBe(false); }); test("merges variants, sizes, and custom className", () => { @@ -85,8 +74,8 @@ describe("Button", () => { variant: "destructive", }); - const call = lastUseRenderCall(); - const className = call.props.className as string; + const call = lastButtonPrimitiveCall(); + const className = call.className as string; expect(className).toContain("border-destructive"); expect(className).toContain("h-10"); expect(className).toContain("custom-class"); From 231f741b4440eaaf52bd3a345741eaf5cc76e706 Mon Sep 17 00:00:00 2001 From: pasqualevitiello Date: Thu, 30 Jul 2026 17:15:09 +0200 Subject: [PATCH 2/4] docs(ui): document Base UI Button and buttonVariants links Update Button docs, changelog, radix migration, and agent skills so links use buttonVariants instead of Button render. Co-authored-by: Cursor --- apps/ui/AGENTS.md | 25 +++++++-- apps/ui/content/docs/(root)/changelog.mdx | 48 +++++++++++++++++ .../content/docs/(root)/radix-migration.mdx | 16 ++++-- apps/ui/content/docs/(root)/skills.mdx | 2 +- apps/ui/content/docs/components/button.mdx | 53 ++++++++++++++----- apps/ui/public/r/p-button-17.json | 2 +- apps/ui/public/r/registry.json | 2 +- apps/ui/registry.json | 2 +- apps/ui/registry/__index__.tsx | 2 +- apps/ui/registry/registry-particles.ts | 2 +- apps/ui/skills/coss-particles/SKILL.md | 2 +- .../coss/references/component-registry.md | 2 +- .../coss/references/primitives/button.md | 39 +++++++++++--- .../coss/references/rules/composition.md | 1 + .../skills/coss/references/rules/migration.md | 21 ++++++++ 15 files changed, 186 insertions(+), 33 deletions(-) diff --git a/apps/ui/AGENTS.md b/apps/ui/AGENTS.md index 6deb3eba2..e3c99b766 100644 --- a/apps/ui/AGENTS.md +++ b/apps/ui/AGENTS.md @@ -811,7 +811,8 @@ When creating a particle equivalent to an origin component: - Understand why certain classes were used in origin 4. **Look at existing particles** in the same category for consistency 5. **Understand the differences:** - - `asChild` → `render` prop + - `asChild` → `render` prop (triggers / composed parts — not Button-as-link) + - Button links → `buttonVariants` on `` / `Link` (not `Button render`) - `*Content` → `*Popup` or `*Panel` - `onSelect` → `onClick` (Menu) - `type="multiple"` → `multiple={true}` @@ -819,7 +820,7 @@ When creating a particle equivalent to an origin component: ### Key Migration Patterns -**asChild → render:** +**asChild → render (triggers / composed parts):** ```tsx // Origin (Radix) @@ -831,6 +832,23 @@ When creating a particle equivalent to an origin component: }>Open ``` +**Button links → `buttonVariants` (not `Button render`):** + +```tsx +// Origin (Radix / older coss) + +// or: + +// coss (Base UI Button) +import { buttonVariants } from "@/registry/default/ui/button"; + + + Login + +``` + **Component naming:** ```tsx @@ -1019,7 +1037,8 @@ const handleSubmit = async () => { - Avoid Next.js-specific APIs (`next/link`, `next/image`) unless the particle specifically demonstrates Next.js integration - Use standard HTML elements and React patterns -- For navigation, use standard `` tags or demonstrate with `render` prop +- For navigation that should look like a button, style `` / `Link` with `buttonVariants` — do not use ` ++ ++ Login ++ +``` + +With variant/size: + +```diff +- ++ ++ Docs ++ +``` + +Trigger composition is unchanged — keep putting `Button` on the **other** component’s `render` prop: + +```tsx +}>Open +``` + +**Agent migration prompt:** + +```text +coss Button now wraps @base-ui/react/button. Stop using +// [!code word:buttonVariants] +import { buttonVariants } from "@/components/ui/button" + + + Login + ``` diff --git a/apps/ui/content/docs/(root)/skills.mdx b/apps/ui/content/docs/(root)/skills.mdx index c56ba2aca..802e84877 100644 --- a/apps/ui/content/docs/(root)/skills.mdx +++ b/apps/ui/content/docs/(root)/skills.mdx @@ -36,7 +36,7 @@ Tailwind v4 token usage, semantic color system, icon sizing rules, `data-slot` s ### Migration Patterns -Rules for migrating from shadcn/Radix to coss/Base UI — `asChild` to `render`, `onSelect` to `onClick`, Select items-first pattern, ToggleGroup `type` to `multiple`, and Slider scalar values. +Rules for migrating from shadcn/Radix to coss/Base UI — `asChild` to `render` on triggers, Button links via `buttonVariants` (not `Button render={}`), `onSelect` to `onClick`, Select items-first pattern, ToggleGroup `type` to `multiple`, and Slider scalar values. ### CLI and Registry diff --git a/apps/ui/content/docs/components/button.mdx b/apps/ui/content/docs/components/button.mdx index b3f86634e..90ce6a2ff 100644 --- a/apps/ui/content/docs/components/button.mdx +++ b/apps/ui/content/docs/components/button.mdx @@ -1,10 +1,15 @@ --- title: Button description: A button or a component that looks like a button. + +links: + doc: https://base-ui.com/react/components/button --- +This component wraps [Base UI Button](https://base-ui.com/react/components/button). + ## Installation @@ -71,41 +76,65 @@ import { Button } from "@/components/ui/button" ## Link -You can use the [`render`](https://base-ui.com/react/utils/use-render#migrating-from-radix-ui) prop to make another component look like a button. Here's an example of a link that looks like a button. +Do not use `Button` for links. Base UI Button enforces button semantics (`role="button"`, keyboard interaction, disabled state), so it should not wrap an `` or router `Link`. + +To make a link look like a button, apply `buttonVariants` to the link element: ```tsx import Link from "next/link" -import { Button } from "@/components/ui/button" +import { buttonVariants } from "@/components/ui/button" export function LinkAsButton() { - return + return ( + + Login + + ) } ``` +Pass variant and size options the same way as on `Button`: + +```tsx + + Docs + +``` + ## API Reference -This is a custom component using Base UI's `useRender` hook, not a direct Base UI wrapper. +This component is built on [Base UI Button](https://base-ui.com/react/components/button). It accepts the same props as Base UI `Button`, plus the coss styling and loading helpers below. ### Button A button component with variant, size, and loading states. -| Prop | Type | Default | Description | -| --------- | ----------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------- | -| `variant` | `"default" \| "destructive" \| "destructive-outline" \| "ghost" \| "link" \| "outline" \| "secondary"` | `"default"` | Controls the button styling | -| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xl" \| "icon-xs" \| "lg" \| "sm" \| "xl" \| "xs"` | `"default"` | Controls the button size | -| `loading` | `boolean` | `false` | Shows a spinner and forces disabled button behavior while loading | -| `render` | `React.ReactElement` | - | Render as a different element (e.g., Link) | +| Prop | Type | Default | Description | +| ------------------------ | ------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------- | +| `variant` | `"default" \| "destructive" \| "destructive-outline" \| "ghost" \| "link" \| "outline" \| "secondary"` | `"default"` | Controls the button styling | +| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xl" \| "icon-xs" \| "lg" \| "sm" \| "xl" \| "xs"` | `"default"` | Controls the button size | +| `loading` | `boolean` | `false` | Shows a spinner and disables the button while loading | +| `focusableWhenDisabled` | `boolean` | `false`\* | Keeps the button focusable while disabled (Base UI). Defaults to `true` when `loading` is `true` | +| `disabled` | `boolean` | `false` | Disables the button | +| `nativeButton` | `boolean` | `true` | Set to `false` when rendering a non-` ``` -`Button` defaults to `type="button"` when rendered as the native button part. In form flows, set `type` explicitly (`button` / `submit` / `reset`) to match intent, especially when using `render` composition. +`Button` wraps [Base UI Button](https://base-ui.com/react/components/button). For form submit buttons, set `type="submit"` explicitly (Base UI requires it). Prefer `type="button"` for non-submit actions inside forms. ## Patterns from coss particles @@ -63,20 +63,39 @@ Button with icon and text (no opacity on the icon): ``` -Sizes: `xs`, `sm`, `default`, `lg`, `icon-xs`, `icon-sm`, `icon`, `icon-lg`. +Sizes: `xs`, `sm`, `default`, `lg`, `xl`, `icon-xs`, `icon-sm`, `icon`, `icon-lg`, `icon-xl`. + +### Links that look like buttons + +Do **not** use ` ``` -Composite approach (manual `Spinner` + `disabled`): +Composite approach (manual `Spinner` + `disabled` / `focusableWhenDisabled` as needed): ```tsx - @@ -84,6 +103,8 @@ Composite approach (manual `Spinner` + `disabled`): Prefer the `loading` prop for typical async actions. Use the composite approach when you need custom spinner placement or label. +Disabled styles use `data-disabled` (including when focusable while disabled). + ### More examples - default: `p-button-1` @@ -94,16 +115,22 @@ Prefer the `loading` prop for typical async actions. Use the composite approach - ghost: `p-button-6` - link: `p-button-7` - extra-small size: `p-button-8` +- link styled with `buttonVariants`: `p-button-17` +- built-in loading prop: `p-button-41` +- composite loading (Spinner + disabled): `p-button-18` ## Common pitfalls +- Using ` +``` + +```tsx +// coss / Base UI Button (and shadcn’s recommended link pattern) +import { buttonVariants } from "@/components/ui/button" + + + Login + +``` + +Do not write `\n );\n}\n\nexport function SidebarRail({\n className,\n ...props\n}: React.ComponentProps<\"button\">): React.ReactElement {\n const { toggleSidebar } = useSidebar();\n\n return (\n