diff --git a/apps/ui/app/docs/[[...slug]]/page.tsx b/apps/ui/app/docs/[[...slug]]/page.tsx index 625225a87..6a68e103f 100644 --- a/apps/ui/app/docs/[[...slug]]/page.tsx +++ b/apps/ui/app/docs/[[...slug]]/page.tsx @@ -12,7 +12,7 @@ import { DocsTableOfContents } from "@/components/docs-toc"; import { SiteFooter } from "@/components/site-footer"; import { source } from "@/lib/source"; import { mdxComponents } from "@/mdx-components"; -import { Button } from "@/registry/default/ui/button"; +import { ButtonLink } from "@/registry/default/ui/button"; export const revalidate = false; export const dynamic = "force-static"; @@ -82,7 +82,7 @@ export default async function Page(props: {
{links?.doc && ( - + ))} ); diff --git a/apps/ui/content/docs/(root)/index.mdx b/apps/ui/content/docs/(root)/index.mdx index 940fc53b4..3d79d4152 100644 --- a/apps/ui/content/docs/(root)/index.mdx +++ b/apps/ui/content/docs/(root)/index.mdx @@ -9,7 +9,7 @@ We think Base UI is the best foundation for modern web applications. We've taken This is the component library we'll be progressively adopting for [cal.com](https://cal.com). We're building it in the open for anyone who wants to create beautiful, reliable user interfaces. - + Early Access diff --git a/apps/ui/content/docs/components/button.mdx b/apps/ui/content/docs/components/button.mdx index e99a8774c..52be655ce 100644 --- a/apps/ui/content/docs/components/button.mdx +++ b/apps/ui/content/docs/components/button.mdx @@ -1,6 +1,6 @@ --- title: Button -description: A button or a component that looks like a button. +description: A button component that can be rendered as another tag or focusable when disabled. --- @@ -62,25 +62,33 @@ npm install @base-ui-components/react ## Usage ```tsx -import { Button } from "@/components/ui/button" +import { Button, ButtonLink } from "@/components/ui/button" ``` ```tsx - +// Render a native button element + + +// Render a div of role="button" that looks like a button + + +// Render an anchor tag that looks like a button +About + +// Render a custom component (e.g., Next.js Link) that looks like a button +}>About ``` -## Link +`ButtonLink` was introduced because the Base UI `Button` component adds attributes (like `role="button"`) that are incompatible with anchor tags when using the `render` prop. -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. +For links styled as buttons, always use `ButtonLink` instead of `Button` with `render`: ```tsx -import Link from "next/link" +// Correct: Use ButtonLink for links +}>About -import { Button } from "@/components/ui/button" - -export function LinkAsButton() { - return -} +// Incorrect: Button adds incompatible attributes to anchor elements + ``` ## Examples @@ -157,6 +165,35 @@ export function LinkAsButton() { +## API Reference + +### Button + +`Button` supports the following props: + +| Prop | Type | Default | Description | +| :---------- | :------------------------------------------------------------ | :-------- | :--------------------------------------------- | +| `variant` | `"default" \| "destructive" \| "outline" \| "ghost" \| ...` | `"default"` | The visual style variant | +| `size` | `"xs" \| "sm" \| "default" \| "lg" \| "xl" \| "icon" \| ...` | `"default"` | The size of the button | +| `render` | `ReactElement \| ((props) => ReactElement)` | - | Custom element to render | +| `className` | `string` | - | Additional CSS classes | + +All standard button (` + + ``` ```tsx title="coss ui" -// [!code word:render] - -``` -Link -> +}>Next.js Link +``` + diff --git a/apps/ui/content/docs/components/input-group.mdx b/apps/ui/content/docs/components/input-group.mdx index 68b716373..189c988fe 100644 --- a/apps/ui/content/docs/components/input-group.mdx +++ b/apps/ui/content/docs/components/input-group.mdx @@ -151,7 +151,7 @@ A container for addons like icons, text, buttons, and other elements. Can be pos | `className` | `string` | | Additional CSS classes to apply to the component | | `...props` | `React.ComponentProps<'div'>` | | All standard div attributes are supported | - + For proper focus navigation, the `InputGroupAddon` component should be placed diff --git a/apps/ui/package.json b/apps/ui/package.json index 9ccf00fea..1ccfe0a9c 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "@base-ui-components/react": "^1.0.0-beta.7", + "@base-ui-components/react": "^1.0.0-rc.0", "@coss/ui": "workspace:*", "@hugeicons/core-free-icons": "^2.0.0", "@hugeicons/react": "^1.1.1", diff --git a/apps/ui/public/llms.txt b/apps/ui/public/llms.txt index b9b24cef0..559fd434a 100644 --- a/apps/ui/public/llms.txt +++ b/apps/ui/public/llms.txt @@ -17,7 +17,8 @@ - [Avatar](https://coss.com/ui/docs/components/avatar.md): A visual representation of a user or entity. - [Badge](https://coss.com/ui/docs/components/badge.md): A small status indicator or label component. - [Breadcrumb](https://coss.com/ui/docs/components/breadcrumb.md): Displays the path to the current resource using a hierarchy of links. -- [Button](https://coss.com/ui/docs/components/button.md): A button or a component that looks like a button. +- [Button](https://coss.com/ui/docs/components/button.md): A button component that can be rendered as another tag or focusable when disabled. +- [Button Link](https://coss.com/ui/docs/components/button-link.md): A link component that looks like a button. - [Card](https://coss.com/ui/docs/components/card.md): A content container for grouping related information. - [Checkbox](https://coss.com/ui/docs/components/checkbox.md): A binary toggle input for selecting one or multiple options. - [Checkbox Group](https://coss.com/ui/docs/components/checkbox-group.md): A collection of related checkboxes with group-level control. diff --git a/apps/ui/public/r/button.json b/apps/ui/public/r/button.json index 9a8264ec8..432d02283 100644 --- a/apps/ui/public/r/button.json +++ b/apps/ui/public/r/button.json @@ -8,7 +8,7 @@ "files": [ { "path": "registry/default/ui/button.tsx", - "content": "import { mergeProps } from \"@base-ui-components/react/merge-props\";\nimport { useRender } from \"@base-ui-components/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n \"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border bg-clip-padding font-medium text-sm 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 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default:\n \"min-h-8 px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)]\",\n icon: \"size-8\",\n \"icon-lg\": \"size-9\",\n \"icon-sm\": \"size-7\",\n \"icon-xl\": \"size-10 [&_svg:not([class*='size-'])]:size-4.5\",\n \"icon-xs\":\n \"size-6 rounded-md before:rounded-[calc(var(--radius-md)-1px)]\",\n lg: \"min-h-9 px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2)-1px)]\",\n sm: \"min-h-7 gap-1.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)]\",\n xl: \"min-h-10 px-[calc(--spacing(4)-1px)] py-[calc(--spacing(2)-1px)] text-base [&_svg:not([class*='size-'])]:size-4.5\",\n xs: \"min-h-6 gap-1 rounded-md px-[calc(--spacing(2)-1px)] py-[calc(--spacing(1)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3\",\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 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(: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 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none\",\n \"destructive-outline\":\n \"border-border bg-transparent text-destructive-foreground shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:border-destructive/32 [&:is(:hover,[data-pressed])]:bg-destructive/4\",\n ghost: \"border-transparent hover:bg-accent data-pressed:bg-accent\",\n link: \"border-transparent underline-offset-4 hover:underline\",\n outline:\n \"border-border bg-background shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:bg-accent/50 dark:[&:is(:hover,[data-pressed])]:bg-input/64\",\n secondary:\n \"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90\",\n },\n },\n },\n);\n\ninterface ButtonProps extends useRender.ComponentProps<\"button\"> {\n variant?: VariantProps[\"variant\"];\n size?: VariantProps[\"size\"];\n}\n\nfunction Button({ className, variant, size, render, ...props }: ButtonProps) {\n const typeValue: React.ButtonHTMLAttributes[\"type\"] =\n render ? undefined : \"button\";\n\n const defaultProps = {\n className: cn(buttonVariants({ className, size, variant })),\n \"data-slot\": \"button\",\n type: typeValue,\n };\n\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(defaultProps, props),\n render,\n });\n}\n\nexport { Button, buttonVariants };\n", + "content": "\"use client\";\n\nimport { Button as ButtonPrimitive } from \"@base-ui-components/react/button\";\nimport { mergeProps } from \"@base-ui-components/react/merge-props\";\nimport { useRender } from \"@base-ui-components/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n \"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border bg-clip-padding font-medium text-sm 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 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default:\n \"min-h-8 px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)]\",\n icon: \"size-8\",\n \"icon-lg\": \"size-9\",\n \"icon-sm\": \"size-7\",\n \"icon-xl\": \"size-10 [&_svg:not([class*='size-'])]:size-4.5\",\n \"icon-xs\":\n \"size-6 rounded-md before:rounded-[calc(var(--radius-md)-1px)]\",\n lg: \"min-h-9 px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2)-1px)]\",\n sm: \"min-h-7 gap-1.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)]\",\n xl: \"min-h-10 px-[calc(--spacing(4)-1px)] py-[calc(--spacing(2)-1px)] text-base [&_svg:not([class*='size-'])]:size-4.5\",\n xs: \"min-h-6 gap-1 rounded-md px-[calc(--spacing(2)-1px)] py-[calc(--spacing(1)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3\",\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 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(: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 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none\",\n \"destructive-outline\":\n \"border-border bg-transparent text-destructive-foreground shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:border-destructive/32 [&:is(:hover,[data-pressed])]:bg-destructive/4\",\n ghost: \"border-transparent hover:bg-accent data-pressed:bg-accent\",\n link: \"border-transparent underline-offset-4 hover:underline\",\n outline:\n \"border-border bg-background shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/4%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none [&:is(:hover,[data-pressed])]:bg-accent/50 dark:[&:is(:hover,[data-pressed])]:bg-input/64\",\n secondary:\n \"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90\",\n },\n },\n },\n);\n\nfunction Button({\n className,\n variant,\n size,\n ...props\n}: ButtonPrimitive.Props &\n VariantProps &\n React.ComponentPropsWithRef) {\n return (\n \n );\n}\n\nfunction ButtonLink({\n className,\n variant,\n size,\n render,\n ...props\n}: useRender.ComponentProps<\"a\"> & VariantProps) {\n const defaultProps = {\n className: cn(buttonVariants({ className, size, variant })),\n \"data-slot\": \"button\",\n };\n\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">(defaultProps, props),\n render,\n });\n}\n\nexport { Button, ButtonLink, buttonVariants };\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 3df1b90b1..e727ed4a8 100644 --- a/apps/ui/public/r/p-button-17.json +++ b/apps/ui/public/r/p-button-17.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/default/particles/p-button-17.tsx", - "content": "import Link from \"next/link\";\n\nimport { Button } from \"@/registry/default/ui/button\";\n\nexport default function Particle() {\n return ;\n}\n", + "content": "import Link from \"next/link\";\n\nimport { ButtonLink } from \"@/registry/default/ui/button\";\n\nexport default function Particle() {\n return (\n
\n Link\n }>Next.js Link\n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-toast-7.json b/apps/ui/public/r/p-toast-7.json index 27941a703..baf95dfd1 100644 --- a/apps/ui/public/r/p-toast-7.json +++ b/apps/ui/public/r/p-toast-7.json @@ -12,7 +12,7 @@ "files": [ { "path": "registry/default/particles/p-toast-7.tsx", - "content": "\"use client\";\n\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { useCopyToClipboard } from \"@/registry/default/hooks/use-copy-to-clipboard\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { anchoredToastManager } from \"@/registry/default/ui/toast\";\nimport {\n Tooltip,\n TooltipPopup,\n TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\n\nexport default function Particle() {\n const copyButtonRef = React.useRef(null);\n const toastTimeout = 2000;\n\n const { copyToClipboard, isCopied } = useCopyToClipboard({\n onCopy: () => {\n if (copyButtonRef.current) {\n anchoredToastManager.add({\n data: {\n tooltipStyle: true,\n },\n positionerProps: {\n anchor: copyButtonRef.current,\n },\n timeout: toastTimeout,\n title: \"Copied!\",\n });\n }\n },\n timeout: toastTimeout,\n });\n\n function handleCopy() {\n const url = \"https://coss.com\";\n copyToClipboard(url);\n }\n\n return (\n \n \n }\n >\n {isCopied ? (\n \n ) : (\n \n )}\n \n \n

Copy to clipboard

\n
\n
\n );\n}\n", + "content": "\"use client\";\n\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { useCopyToClipboard } from \"@/registry/default/hooks/use-copy-to-clipboard\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { anchoredToastManager } from \"@/registry/default/ui/toast\";\nimport {\n Tooltip,\n TooltipPopup,\n TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\n\nexport default function Particle() {\n const copyButtonRef = React.useRef(null);\n const toastTimeout = 2000;\n\n const { copyToClipboard, isCopied } = useCopyToClipboard({\n onCopy: () => {\n if (copyButtonRef.current) {\n anchoredToastManager.add({\n data: {\n tooltipStyle: true,\n },\n positionerProps: {\n anchor: copyButtonRef.current,\n },\n timeout: toastTimeout,\n title: \"Copied!\",\n });\n }\n },\n timeout: toastTimeout,\n });\n\n function handleCopy() {\n const url = \"https://coss.com\";\n copyToClipboard(url);\n }\n\n return (\n \n \n }\n >\n {isCopied ? : }\n \n \n

Copy to clipboard

\n
\n
\n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/p-toolbar-1.json b/apps/ui/public/r/p-toolbar-1.json index f856d4ad7..04ee51128 100644 --- a/apps/ui/public/r/p-toolbar-1.json +++ b/apps/ui/public/r/p-toolbar-1.json @@ -16,7 +16,7 @@ "files": [ { "path": "registry/default/particles/p-toolbar-1.tsx", - "content": "import {\n AlignCenterIcon,\n AlignLeftIcon,\n AlignRightIcon,\n DollarSignIcon,\n PercentIcon,\n} from \"lucide-react\";\n\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Select,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\nimport { Toggle, ToggleGroup } from \"@/registry/default/ui/toggle-group\";\nimport {\n Toolbar,\n ToolbarButton,\n ToolbarGroup,\n ToolbarSeparator,\n} from \"@/registry/default/ui/toolbar\";\nimport {\n Tooltip,\n TooltipPopup,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\n\nconst items = [\n { label: \"Helvetica\", value: \"helvetica\" },\n { label: \"Arial\", value: \"arial\" },\n { label: \"Times New Roman\", value: \"times-new-roman\" },\n];\n\nexport default function Particle() {\n return (\n \n \n \n \n }\n >\n \n \n }\n />\n Align left\n \n \n }\n >\n \n \n }\n />\n Align center\n \n \n }\n >\n \n \n }\n />\n Align right\n \n \n \n \n \n }\n >\n \n \n }\n />\n Format as currency\n \n \n }\n >\n \n \n }\n />\n Format as percent\n \n \n \n \n \n \n \n \n }>Save\n \n \n \n );\n}\n", + "content": "import {\n AlignCenterIcon,\n AlignLeftIcon,\n AlignRightIcon,\n DollarSignIcon,\n PercentIcon,\n} from \"lucide-react\";\n\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Select,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\nimport { Toggle, ToggleGroup } from \"@/registry/default/ui/toggle-group\";\nimport {\n Toolbar,\n ToolbarButton,\n ToolbarGroup,\n ToolbarSeparator,\n} from \"@/registry/default/ui/toolbar\";\nimport {\n Tooltip,\n TooltipPopup,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\n\nconst items = [\n { label: \"Helvetica\", value: \"helvetica\" },\n { label: \"Arial\", value: \"arial\" },\n { label: \"Times New Roman\", value: \"times-new-roman\" },\n];\n\nexport default function Particle() {\n return (\n \n \n \n \n }\n >\n \n \n }\n />\n Align left\n \n \n }\n >\n \n \n }\n />\n Align center\n \n \n }\n >\n \n \n }\n />\n Align right\n \n \n \n \n \n }\n >\n \n \n }\n />\n Format as currency\n \n \n }\n >\n \n \n }\n />\n Format as percent\n \n \n \n \n \n \n \n \n }>Save\n \n \n \n );\n}\n", "type": "registry:block" } ], diff --git a/apps/ui/public/r/pagination.json b/apps/ui/public/r/pagination.json index 34e048af9..8f811ecd4 100644 --- a/apps/ui/public/r/pagination.json +++ b/apps/ui/public/r/pagination.json @@ -8,7 +8,7 @@ "files": [ { "path": "registry/default/ui/pagination.tsx", - "content": "import { mergeProps } from \"@base-ui-components/react/merge-props\";\nimport { useRender } from \"@base-ui-components/react/use-render\";\nimport {\n ChevronLeftIcon,\n ChevronRightIcon,\n MoreHorizontalIcon,\n} from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { type Button, buttonVariants } from \"@/registry/default/ui/button\";\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n return (\n \n );\n}\n\nfunction PaginationContent({\n className,\n ...props\n}: React.ComponentProps<\"ul\">) {\n return (\n \n );\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n return
  • ;\n}\n\ntype PaginationLinkProps = {\n isActive?: boolean;\n size?: React.ComponentProps[\"size\"];\n} & useRender.ComponentProps<\"a\">;\n\nfunction PaginationLink({\n className,\n isActive,\n size = \"icon\",\n render,\n ...props\n}: PaginationLinkProps) {\n const defaultProps = {\n \"aria-current\": isActive ? (\"page\" as const) : undefined,\n className: render\n ? className\n : cn(\n buttonVariants({\n size,\n variant: isActive ? \"outline\" : \"ghost\",\n }),\n className,\n ),\n \"data-active\": isActive,\n \"data-slot\": \"pagination-link\",\n };\n\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">(defaultProps, props),\n render,\n });\n}\n\nfunction PaginationPrevious({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n Previous\n \n );\n}\n\nfunction PaginationNext({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n Next\n \n \n );\n}\n\nfunction PaginationEllipsis({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n \n \n More pages\n \n );\n}\n\nexport {\n Pagination,\n PaginationContent,\n PaginationLink,\n PaginationItem,\n PaginationPrevious,\n PaginationNext,\n PaginationEllipsis,\n};\n", + "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui-components/react/merge-props\";\nimport { useRender } from \"@base-ui-components/react/use-render\";\nimport {\n ChevronLeftIcon,\n ChevronRightIcon,\n MoreHorizontalIcon,\n} from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { type Button, buttonVariants } from \"@/registry/default/ui/button\";\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n return (\n \n );\n}\n\nfunction PaginationContent({\n className,\n ...props\n}: React.ComponentProps<\"ul\">) {\n return (\n \n );\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n return
  • ;\n}\n\ntype PaginationLinkProps = {\n isActive?: boolean;\n size?: React.ComponentProps[\"size\"];\n} & useRender.ComponentProps<\"a\">;\n\nfunction PaginationLink({\n className,\n isActive,\n size = \"icon\",\n render,\n ...props\n}: PaginationLinkProps) {\n const defaultProps = {\n \"aria-current\": isActive ? (\"page\" as const) : undefined,\n className: render\n ? className\n : cn(\n buttonVariants({\n size,\n variant: isActive ? \"outline\" : \"ghost\",\n }),\n className,\n ),\n \"data-active\": isActive,\n \"data-slot\": \"pagination-link\",\n };\n\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">(defaultProps, props),\n render,\n });\n}\n\nfunction PaginationPrevious({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n Previous\n \n );\n}\n\nfunction PaginationNext({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n Next\n \n \n );\n}\n\nfunction PaginationEllipsis({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n \n \n More pages\n \n );\n}\n\nexport {\n Pagination,\n PaginationContent,\n PaginationLink,\n PaginationItem,\n PaginationPrevious,\n PaginationNext,\n PaginationEllipsis,\n};\n", "type": "registry:ui" } ] diff --git a/apps/ui/registry/default/particles/p-button-17.tsx b/apps/ui/registry/default/particles/p-button-17.tsx index 6901bbd01..03eb6b2f6 100644 --- a/apps/ui/registry/default/particles/p-button-17.tsx +++ b/apps/ui/registry/default/particles/p-button-17.tsx @@ -1,7 +1,12 @@ import Link from "next/link"; -import { Button } from "@/registry/default/ui/button"; +import { ButtonLink } from "@/registry/default/ui/button"; export default function Particle() { - return ; + return ( +
    + Link + }>Next.js Link +
    + ); } diff --git a/apps/ui/registry/default/particles/p-toast-7.tsx b/apps/ui/registry/default/particles/p-toast-7.tsx index 05b7b2ec4..ea3a0d08d 100644 --- a/apps/ui/registry/default/particles/p-toast-7.tsx +++ b/apps/ui/registry/default/particles/p-toast-7.tsx @@ -13,7 +13,7 @@ import { } from "@/registry/default/ui/tooltip"; export default function Particle() { - const copyButtonRef = React.useRef(null); + const copyButtonRef = React.useRef(null); const toastTimeout = 2000; const { copyToClipboard, isCopied } = useCopyToClipboard({ @@ -46,6 +46,7 @@ export default function Particle() { - +
  • ); diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index 998222d08..929829691 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -355,7 +355,7 @@ [data-lib="radix-ui"] & { &::before { - background-color: --alpha(var(--color-error) / 20%); + background-color: --alpha(var(--color-destructive) / 20%); } } diff --git a/packages/ui/src/ui/button.test.tsx b/packages/ui/src/ui/button.test.tsx index 6e38d8d53..f9178c6ee 100644 --- a/packages/ui/src/ui/button.test.tsx +++ b/packages/ui/src/ui/button.test.tsx @@ -1,80 +1,75 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"; -type UseRenderConfig = { - defaultTagName: string; - render?: (...args: unknown[]) => unknown; - props: Record; +type ButtonRenderCall = Record & { + className?: string; + "data-slot"?: string; + type?: "button" | "submit" | "reset"; + render?: unknown; + disabled?: boolean; + "aria-label"?: string; }; -const useRenderCalls: UseRenderConfig[] = []; -const mergePropsCalls: Array< - [Record, Record] -> = []; +const buttonRenderCalls: ButtonRenderCall[] = []; +const cnCalls: string[][] = []; -function useRenderMock(config: UseRenderConfig) { - useRenderCalls.push(config); +function mockButtonPrimitive(props: ButtonRenderCall) { + buttonRenderCalls.push(props); return null; } -function mergePropsMock( - defaults: Record, - overrides: Record = {}, -) { - mergePropsCalls.push([defaults, overrides]); - return { ...defaults, ...overrides }; +const ButtonPrimitiveMock = Object.assign(mockButtonPrimitive, { + Props: {} as Record, +}); + +function cnMock(...inputs: string[]) { + cnCalls.push(inputs); + return inputs.filter(Boolean).join(" "); } -mock.module("@base-ui-components/react/use-render", () => ({ - useRender: useRenderMock, +mock.module("@base-ui-components/react/button", () => ({ + Button: ButtonPrimitiveMock, })); -mock.module("@base-ui-components/react/merge-props", () => ({ - mergeProps: mergePropsMock, +mock.module("@coss/ui/lib/utils", () => ({ + cn: cnMock, })); -const { Button } = await import("./button"); +const { Button: ButtonComponent } = await import("./button"); -function lastUseRenderCall() { - const lastCall = useRenderCalls[useRenderCalls.length - 1]; - if (!lastCall) { - throw new Error("useRender was not called"); - } - return lastCall; +// Mock the Button component to track render calls +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 lastButtonCall() { + const lastCall = buttonRenderCalls[buttonRenderCalls.length - 1]; if (!lastCall) { - throw new Error("mergeProps was not called"); + throw new Error("Button was not called"); } return lastCall; } describe("Button", () => { beforeEach(() => { - useRenderCalls.length = 0; - mergePropsCalls.length = 0; + buttonRenderCalls.length = 0; + cnCalls.length = 0; }); - test("sets base attributes and defaults type to button", () => { + test("sets base attributes with data-slot", () => { 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 = lastButtonCall(); + expect(call["data-slot"]).toBe("button"); }); - test("omits the default type when a custom render is provided", () => { - const render = () => null; + test("accepts custom render function", () => { + const render = ; Button({ render, type: "submit" }); - const [defaults, overrides] = lastMergePropsCall(); - expect(defaults.type).toBeUndefined(); - expect(overrides.type).toBe("submit"); - - const call = lastUseRenderCall(); + const call = lastButtonCall(); expect(call.render).toBe(render); + expect(call.type).toBe("submit"); }); test("merges variants, sizes, and custom className", () => { @@ -84,10 +79,28 @@ describe("Button", () => { variant: "destructive", }); - const call = lastUseRenderCall(); - const className = call.props.className as string; + const call = lastButtonCall(); + const className = call.className as string; expect(className).toContain("border-destructive"); expect(className).toContain("min-h-9"); expect(className).toContain("custom-class"); }); + + test("applies default variant and size styles", () => { + Button({}); + + const call = lastButtonCall(); + const className = call.className as string; + expect(className).toContain("border-primary"); + expect(className).toContain("bg-primary"); + expect(className).toContain("min-h-8"); + }); + + test("passes through additional props", () => { + Button({ "aria-label": "Custom button", disabled: true }); + + const call = lastButtonCall(); + expect(call["aria-label"]).toBe("Custom button"); + expect(call.disabled).toBe(true); + }); }); diff --git a/packages/ui/src/ui/button.tsx b/packages/ui/src/ui/button.tsx index a48051924..821470441 100644 --- a/packages/ui/src/ui/button.tsx +++ b/packages/ui/src/ui/button.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { Button as ButtonPrimitive } from "@base-ui-components/react/button"; import { mergeProps } from "@base-ui-components/react/merge-props"; import { useRender } from "@base-ui-components/react/use-render"; import { cva, type VariantProps } from "class-variance-authority"; @@ -45,26 +48,40 @@ const buttonVariants = cva( }, ); -interface ButtonProps extends useRender.ComponentProps<"button"> { - variant?: VariantProps["variant"]; - size?: VariantProps["size"]; +function Button({ + className, + variant, + size, + ...props +}: ButtonPrimitive.Props & + VariantProps & + React.ComponentPropsWithRef) { + return ( + + ); } -function Button({ className, variant, size, render, ...props }: ButtonProps) { - const typeValue: React.ButtonHTMLAttributes["type"] = - render ? undefined : "button"; - +function ButtonLink({ + className, + variant, + size, + render, + ...props +}: useRender.ComponentProps<"a"> & VariantProps) { const defaultProps = { className: cn(buttonVariants({ className, size, variant })), "data-slot": "button", - type: typeValue, }; return useRender({ - defaultTagName: "button", - props: mergeProps<"button">(defaultProps, props), + defaultTagName: "a", + props: mergeProps<"a">(defaultProps, props), render, }); } -export { Button, buttonVariants }; +export { Button, ButtonLink, buttonVariants }; diff --git a/packages/ui/src/ui/pagination.tsx b/packages/ui/src/ui/pagination.tsx index 89ca9ce4c..26b9ce297 100644 --- a/packages/ui/src/ui/pagination.tsx +++ b/packages/ui/src/ui/pagination.tsx @@ -1,3 +1,5 @@ +"use client"; + import { mergeProps } from "@base-ui-components/react/merge-props"; import { useRender } from "@base-ui-components/react/use-render"; import {