feat: add coss-ui core package (#25984)

* Install/add components in base coss-ui package

* fix components imports

* add yarn script to pull

* Update packagejson

* remove skeleton duplicates

* feat: allow importing components with the cli

* fix: update exports in package.json to reference styles.css instead of globals.css

* add lucide dep

* refactor: re-pull components

* refactor: remove unused registries section from components.json

* refactor: implement changes suggested by Volnei

* refactor(wip): implemet coss ui components on the profile page

* fix src/src problem

* update command to overwrite existing components

* pulled from registry

* restore globals.css

* feat: add pull script

* feat: enhance pull script with validation and error handling

* remove duplicate components

* refactor: reimport coss component and fix ts errors

* fix: components pulling

* chore: regenerate yarn.lock

* chore: remove tailwindcss/forms

* trying different moduleResolution

* TypeScript requires module to also be "NodeNext".

* remove unnecessary changes

* type fixes plus resolve @coss/ui

* use bundler tsconfig

* add missing deps to features and ui

* Add bundler to other packages

* fix module enext resolution with bundler

* remove reduant files

* remove package json from features

* revert unrelated change to the PR

---------

Co-authored-by: pasqualevitiello <pasqualevitiello@gmail.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
This commit is contained in:
sean-brydon
2025-12-18 19:57:21 +00:00
committed by GitHub
co-authored by pasqualevitiello Eunjae Lee
parent 3423777ee3
commit 42f2c170aa
69 changed files with 5948 additions and 341 deletions
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/coss-ui/src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"hooks": "@coss/ui/hooks",
"utils": "@coss/ui/lib/utils",
"ui": "@coss/ui/components"
}
}
+1
View File
@@ -219,6 +219,7 @@ const nextConfig = (phase) => {
"@calcom/lib",
"@calcom/prisma",
"@calcom/trpc",
"@coss/ui",
],
modularizeImports: {
"@calcom/features/insights/components": {
+1 -1
View File
@@ -1,7 +1,7 @@
@import "tailwindcss";
@import "../../../packages/coss-ui/styles.css";
@import "../../../packages/config/theme/shared-sources.css";
@import "../../../packages/config/theme/tokens.css";
@import "../../../packages/coss-ui/src/styles/globals.css";
@import "./raqb.css";
::-moz-selection {
+2 -1
View File
@@ -9,7 +9,8 @@
"@lib/*": ["lib/*"],
"@server/*": ["server/*"],
"@prisma/client/*": ["@calcom/prisma/client/*"],
"@calcom/repository/*": ["@calcom/lib/server/repository/*"]
"@calcom/repository/*": ["@calcom/lib/server/repository/*"],
"@coss/ui/*": ["../../packages/coss-ui/src/*"]
},
"plugins": [
{
-1
View File
@@ -11,7 +11,6 @@
],
"dependencies": {
"@calcom/eslint-plugin-eslint": "workspace:*",
"@tailwindcss/forms": "^0.5.10",
"@todesktop/tailwind-variants": "^1.0.0",
"eslint-config-next": "^13.2.1",
"eslint-config-prettier": "^8.6.0",
+2 -1
View File
@@ -5,4 +5,5 @@
@source "../../../packages/app-store/**/*{components,pages}/**/*.{js,ts,jsx,tsx}";
@source "../../../packages/features/**/*.{js,ts,jsx,tsx}";
@source "../../../packages/ui/**/*.{js,ts,jsx,tsx}";
@source "../../../packages/platform/atoms/**/*.{js,ts,jsx,tsx}";
@source "../../../packages/platform/atoms/**/*.{js,ts,jsx,tsx}";
@source "../../../packages/coss-ui/src/**/*.{js,ts,jsx,tsx}";
-1
View File
@@ -1,7 +1,6 @@
/** This file is used in webapp as well as inside shadow DOM of embed elements. **/
@import "tw-animate-css";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@plugin "tailwindcss-radix";
@plugin 'tailwind-scrollbar' {
+6 -3
View File
@@ -4,7 +4,7 @@
"rsc": true,
"tsx": true,
"tailwind": {
"css": "./styles.css",
"css": "src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
@@ -12,8 +12,11 @@
"aliases": {
"components": "@coss/ui/components",
"utils": "@coss/ui/lib/utils",
"hooks": "@coss/ui/hooks",
"ui": "@coss/ui/components",
"lib": "@coss/ui/lib",
"ui": "@coss/ui/components"
"hooks": "@coss/ui/hooks"
},
"registries": {
"@coss": "https://coss.com/ui/r/{name}.json"
}
}
-67
View File
@@ -1,67 +0,0 @@
import * as React from "react"
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"
import { cn } from "@coss/ui/lib/utils"
const buttonVariants = cva(
"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg border bg-clip-padding text-sm font-medium whitespace-nowrap transition-shadow outline-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] 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 pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default:
"border-primary bg-primary text-primary-foreground shadow-xs shadow-primary/24 not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] hover:bg-primary/90 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none",
outline:
"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:not-data-pressed: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",
secondary:
"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90",
destructive:
"border-destructive bg-destructive text-white shadow-xs shadow-destructive/24 not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] hover:bg-destructive/90 [&:is(:active,[data-pressed])]:inset-shadow-[0_1px_--theme(--color-black/8%)] [&:is(:disabled,:active,[data-pressed])]:shadow-none",
"destructive-outline":
"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",
ghost: "border-transparent hover:bg-accent data-pressed:bg-accent",
link: "border-transparent underline-offset-4 hover:underline",
},
size: {
default:
"min-h-8 px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)]",
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",
sm: "min-h-7 gap-1.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)]",
lg: "min-h-9 px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2)-1px)]",
xl: "min-h-10 px-[calc(--spacing(4)-1px)] py-[calc(--spacing(2)-1px)] text-base [&_svg:not([class*='size-'])]:size-4.5",
icon: "size-8",
"icon-sm": "size-7",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
interface ButtonProps extends useRender.ComponentProps<"button"> {
variant?: VariantProps<typeof buttonVariants>["variant"]
size?: VariantProps<typeof buttonVariants>["size"]
}
function Button({ className, variant, size, render, ...props }: ButtonProps) {
const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] =
render ? undefined : "button"
const defaultProps = {
"data-slot": "button",
className: cn(buttonVariants({ variant, size, className })),
type: typeValue,
}
return useRender({
defaultTagName: "button",
render,
props: mergeProps<"button">(defaultProps, props),
})
}
export { Button, buttonVariants }
-41
View File
@@ -1,41 +0,0 @@
"use client"
import { Input as InputPrimitive } from "@base-ui-components/react/input"
import { cn } from "@coss/ui/lib/utils"
function Input({
className,
size = "default",
...props
}: Omit<InputPrimitive.Props, "size"> & {
size?: "sm" | "default" | "lg" | number
}) {
return (
<span
data-slot="input-control"
className={cn(
"relative inline-flex w-full rounded-lg border border-input bg-background bg-clip-padding text-base/5 shadow-xs ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:border-ring has-focus-visible:ring-[3px] has-disabled:opacity-64 has-aria-invalid:border-destructive/36 has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 sm:text-sm dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/8%)] dark:has-aria-invalid:ring-destructive/24 [&:has(:disabled,:focus-visible,[aria-invalid])]:shadow-none",
className
)}
>
<InputPrimitive
data-slot="input"
className={cn(
"w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none placeholder:text-muted-foreground/64",
size === "sm" &&
"px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)]",
size === "lg" && "py-[calc(--spacing(2)-1px)]",
props.type === "search" &&
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
props.type === "file" &&
"text-muted-foreground file:me-3 file:bg-transparent file:text-sm file:font-medium file:text-foreground"
)}
size={typeof size === "number" ? size : undefined}
{...props}
/>
</span>
)
}
export { Input }
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+21 -7
View File
@@ -3,14 +3,28 @@
"private": true,
"version": "0.0.0",
"exports": {
"./styles.css": "./styles.css",
"./components/*": "./components/*.tsx",
"./lib/*": "./lib/*.ts"
"./styles.css": "./src/styles/globals.css",
"./lib/*": "./src/lib/*.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts"
},
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"registry:pull": "ts-node ../../scripts/pull-coss-ui-components.ts"
},
"dependencies": {
"@base-ui-components/react": "^1.0.0-beta.4",
"class-variance-authority": "^0.4.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
"@base-ui/react": "^1.0.0",
"class-variance-authority": "0.7.1",
"clsx": "^2.1.1",
"lucide-react": "0.555.0",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"ts-node": "^10.9.2",
"tsc-absolute": "1.0.0",
"typescript": "5.9.0-beta"
}
}
@@ -0,0 +1,66 @@
"use client";
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "@coss/ui/lib/utils";
function Accordion(props: AccordionPrimitive.Root.Props) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return (
<AccordionPrimitive.Item
className={cn("border-b last:border-b-0", className)}
data-slot="accordion-item"
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.Trigger.Props) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
className={cn(
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 [&[data-panel-open]>svg]:rotate-180",
className,
)}
data-slot="accordion-trigger"
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 opacity-80 transition-transform duration-200 ease-in-out" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionPanel({
className,
children,
...props
}: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
className="h-(--accordion-panel-height) overflow-hidden text-muted-foreground text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0"
data-slot="accordion-panel"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Panel>
);
}
export {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionPanel,
AccordionPanel as AccordionContent,
};
@@ -0,0 +1,153 @@
"use client";
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import { cn } from "@coss/ui/lib/utils";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogBackdrop({
className,
...props
}: AlertDialogPrimitive.Backdrop.Props) {
return (
<AlertDialogPrimitive.Backdrop
className={cn(
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 ease-out data-ending-style:opacity-0 data-starting-style:opacity-0",
className,
)}
data-slot="alert-dialog-backdrop"
{...props}
/>
);
}
function AlertDialogViewport({
className,
...props
}: AlertDialogPrimitive.Viewport.Props) {
return (
<AlertDialogPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 grid grid-rows-[1fr_auto] justify-items-center pt-6 sm:grid-rows-[1fr_auto_3fr] sm:p-4",
className,
)}
data-slot="alert-dialog-viewport"
{...props}
/>
);
}
function AlertDialogPopup({
className,
...props
}: AlertDialogPrimitive.Popup.Props) {
return (
<AlertDialogPortal>
<AlertDialogBackdrop />
<AlertDialogViewport>
<AlertDialogPrimitive.Popup
className={cn(
"sm:-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 grid max-h-full w-full min-w-0 border-t bg-popover bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-nested-dialog-open:origin-top data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden sm:max-w-lg sm:data-nested:data-ending-style:translate-y-8 sm:data-nested:data-starting-style:translate-y-8 sm:scale-[calc(1-0.1*var(--nested-dialogs))] sm:rounded-2xl sm:border sm:data-ending-style:scale-98 sm:data-starting-style:scale-98 sm:before:rounded-[calc(var(--radius-2xl)-1px)] dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="alert-dialog-popup"
{...props}
/>
</AlertDialogViewport>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex flex-col gap-2 p-6 not-has-[+[data-slot=alert-dialog-footer]]:pb-4 text-center max-sm:pb-4 sm:text-left",
className,
)}
data-slot="alert-dialog-header"
{...props}
/>
);
}
function AlertDialogFooter({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & {
variant?: "default" | "bare";
}) {
return (
<div
className={cn(
"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
variant === "default" && "border-t bg-muted/50 py-4",
variant === "bare" && "pt-4 pb-6",
className,
)}
data-slot="alert-dialog-footer"
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: AlertDialogPrimitive.Title.Props) {
return (
<AlertDialogPrimitive.Title
className={cn("font-heading text-xl leading-none", className)}
data-slot="alert-dialog-title"
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: AlertDialogPrimitive.Description.Props) {
return (
<AlertDialogPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="alert-dialog-description"
{...props}
/>
);
}
function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
return (
<AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogBackdrop,
AlertDialogBackdrop as AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogPopup,
AlertDialogPopup as AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogClose,
AlertDialogViewport,
};
+80
View File
@@ -0,0 +1,80 @@
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
const alertVariants = cva(
"relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4",
{
defaultVariants: {
variant: "default",
},
variants: {
variant: {
default:
"bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
error:
"border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
info: "border-info/32 bg-info/4 [&>svg]:text-info",
success: "border-success/32 bg-success/4 [&>svg]:text-success",
warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
},
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
className={cn(alertVariants({ variant }), className)}
data-slot="alert"
role="alert"
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("font-medium [svg~&]:col-start-2", className)}
data-slot="alert-title"
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2",
className,
)}
data-slot="alert-description"
{...props}
/>
);
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex gap-1 max-sm:col-start-2 max-sm:mt-2 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-slot=alert-description]~&]:col-start-2 sm:[[data-slot=alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-slot=alert-description]~&]:col-start-3 sm:[svg~[data-slot=alert-title]~&]:col-start-3",
className,
)}
data-slot="alert-action"
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription, AlertAction };
@@ -0,0 +1,284 @@
"use client";
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
import { ChevronsUpDownIcon, XIcon } from "lucide-react";
import { cn } from "@coss/ui/lib/utils";
import { Input } from "@coss/ui/components/input";
import { ScrollArea } from "@coss/ui/components/scroll-area";
const Autocomplete = AutocompletePrimitive.Root;
function AutocompleteInput({
className,
showTrigger = false,
showClear = false,
size,
...props
}: Omit<AutocompletePrimitive.Input.Props, "size"> & {
showTrigger?: boolean;
showClear?: boolean;
size?: "sm" | "default" | "lg" | number;
}) {
const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
return (
<div className="relative w-full">
<AutocompletePrimitive.Input
className={cn(
sizeValue === "sm"
? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5"
: "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7",
className,
)}
data-slot="autocomplete-input"
render={<Input size={sizeValue} />}
{...props}
/>
{showTrigger && (
<AutocompleteTrigger
className={cn(
"-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
>
<ChevronsUpDownIcon />
</AutocompleteTrigger>
)}
{showClear && (
<AutocompleteClear
className={cn(
"-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
>
<XIcon />
</AutocompleteClear>
)}
</div>
);
}
function AutocompletePopup({
className,
children,
sideOffset = 4,
...props
}: AutocompletePrimitive.Popup.Props & {
sideOffset?: number;
}) {
return (
<AutocompletePrimitive.Portal>
<AutocompletePrimitive.Positioner
className="z-50 select-none"
data-slot="autocomplete-positioner"
sideOffset={sideOffset}
>
<span
className={cn(
"relative flex max-h-full origin-(--transform-origin) rounded-lg border bg-popover bg-clip-padding transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-lg has-data-starting-style:scale-98 has-data-starting-style:opacity-0 dark:not-in-data-[slot=group]:bg-clip-border",
className,
)}
>
<AutocompletePrimitive.Popup
className="flex max-h-[min(var(--available-height),23rem)] w-(--anchor-width) max-w-(--available-width) flex-col"
data-slot="autocomplete-popup"
{...props}
>
{children}
</AutocompletePrimitive.Popup>
</span>
</AutocompletePrimitive.Positioner>
</AutocompletePrimitive.Portal>
);
}
function AutocompleteItem({
className,
children,
...props
}: AutocompletePrimitive.Item.Props) {
return (
<AutocompletePrimitive.Item
className={cn(
"flex min-h-8 cursor-default select-none items-center rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm",
className,
)}
data-slot="autocomplete-item"
{...props}
>
{children}
</AutocompletePrimitive.Item>
);
}
function AutocompleteSeparator({
className,
...props
}: AutocompletePrimitive.Separator.Props) {
return (
<AutocompletePrimitive.Separator
className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
data-slot="autocomplete-separator"
{...props}
/>
);
}
function AutocompleteGroup({
className,
...props
}: AutocompletePrimitive.Group.Props) {
return (
<AutocompletePrimitive.Group
className={className}
data-slot="autocomplete-group"
{...props}
/>
);
}
function AutocompleteGroupLabel({
className,
...props
}: AutocompletePrimitive.GroupLabel.Props) {
return (
<AutocompletePrimitive.GroupLabel
className={cn(
"px-2 py-1.5 font-medium text-muted-foreground text-xs",
className,
)}
data-slot="autocomplete-group-label"
{...props}
/>
);
}
function AutocompleteEmpty({
className,
...props
}: AutocompletePrimitive.Empty.Props) {
return (
<AutocompletePrimitive.Empty
className={cn(
"not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
className,
)}
data-slot="autocomplete-empty"
{...props}
/>
);
}
function AutocompleteRow({
className,
...props
}: AutocompletePrimitive.Row.Props) {
return (
<AutocompletePrimitive.Row
className={className}
data-slot="autocomplete-row"
{...props}
/>
);
}
function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
return (
<AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
);
}
function AutocompleteList({
className,
...props
}: AutocompletePrimitive.List.Props) {
return (
<ScrollArea scrollbarGutter scrollFade>
<AutocompletePrimitive.List
className={cn(
"not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3",
className,
)}
data-slot="autocomplete-list"
{...props}
/>
</ScrollArea>
);
}
function AutocompleteClear({
className,
...props
}: AutocompletePrimitive.Clear.Props) {
return (
<AutocompletePrimitive.Clear
className={cn(
"-translate-y-1/2 absolute end-0.5 top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="autocomplete-clear"
{...props}
>
<XIcon />
</AutocompletePrimitive.Clear>
);
}
function AutocompleteStatus({
className,
...props
}: AutocompletePrimitive.Status.Props) {
return (
<AutocompletePrimitive.Status
className={cn(
"px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
className,
)}
data-slot="autocomplete-status"
{...props}
/>
);
}
function AutocompleteCollection({
...props
}: AutocompletePrimitive.Collection.Props) {
return (
<AutocompletePrimitive.Collection
data-slot="autocomplete-collection"
{...props}
/>
);
}
function AutocompleteTrigger({
className,
...props
}: AutocompletePrimitive.Trigger.Props) {
return (
<AutocompletePrimitive.Trigger
className={className}
data-slot="autocomplete-trigger"
{...props}
/>
);
}
export {
Autocomplete,
AutocompleteInput,
AutocompleteTrigger,
AutocompletePopup,
AutocompleteItem,
AutocompleteSeparator,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteEmpty,
AutocompleteValue,
AutocompleteList,
AutocompleteClear,
AutocompleteStatus,
AutocompleteRow,
AutocompleteCollection,
};
@@ -0,0 +1,46 @@
"use client";
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
import { cn } from "@coss/ui/lib/utils";
function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) {
return (
<AvatarPrimitive.Root
className={cn(
"inline-flex size-8 shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-background align-middle font-medium text-xs",
className,
)}
data-slot="avatar"
{...props}
/>
);
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
<AvatarPrimitive.Image
className={cn("size-full object-cover", className)}
data-slot="avatar-image"
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: AvatarPrimitive.Fallback.Props) {
return (
<AvatarPrimitive.Fallback
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted",
className,
)}
data-slot="avatar-fallback"
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };
+58
View File
@@ -0,0 +1,58 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@coss/ui/lib/utils";
const badgeVariants = cva(
"relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm border border-transparent font-medium outline-none transition-shadow 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*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button,a&]:cursor-pointer [button,a&]:pointer-coarse:after:absolute [button,a&]:pointer-coarse:after:size-full [button,a&]:pointer-coarse:after:min-h-11 [button,a&]:pointer-coarse:after:min-w-11",
{
defaultVariants: {
size: "default",
variant: "default",
},
variants: {
size: {
default:
"h-5.5 min-w-5.5 px-[calc(--spacing(1)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs",
lg: "h-6.5 min-w-6.5 px-[calc(--spacing(1.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm",
sm: "h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]",
},
variant: {
default:
"bg-primary text-primary-foreground [button,a&]:hover:bg-primary/90",
destructive:
"bg-destructive text-white [button,a&]:hover:bg-destructive/90",
error:
"bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
info: "bg-info/8 text-info-foreground dark:bg-info/16",
outline:
"border-border bg-transparent dark:bg-input/32 [button,a&]:hover:bg-accent/50 dark:[button,a&]:hover:bg-input/48",
secondary:
"bg-secondary text-secondary-foreground [button,a&]:hover:bg-secondary/90",
success: "bg-success/8 text-success-foreground dark:bg-success/16",
warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
},
},
},
);
interface BadgeProps extends useRender.ComponentProps<"span"> {
variant?: VariantProps<typeof badgeVariants>["variant"];
size?: VariantProps<typeof badgeVariants>["size"];
}
function Badge({ className, variant, size, render, ...props }: BadgeProps) {
const defaultProps = {
className: cn(badgeVariants({ className, size, variant })),
"data-slot": "badge",
};
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(defaultProps, props),
render,
});
}
export { Badge, badgeVariants };
@@ -0,0 +1,110 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
className={cn(
"wrap-break-word flex flex-wrap items-center gap-1.5 text-muted-foreground text-sm sm:gap-2.5",
className,
)}
data-slot="breadcrumb-list"
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
className={cn("inline-flex items-center gap-1.5", className)}
data-slot="breadcrumb-item"
{...props}
/>
);
}
function BreadcrumbLink({
className,
render,
...props
}: useRender.ComponentProps<"a">) {
const defaultProps = {
className: cn("transition-colors hover:text-foreground", className),
"data-slot": "breadcrumb-link",
};
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(defaultProps, props),
render,
});
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
// biome-ignore lint(a11y/useFocusableInteractive): known
<span
aria-current="page"
aria-disabled="true"
className={cn("font-normal text-foreground", className)}
data-slot="breadcrumb-page"
role="link"
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
aria-hidden="true"
className={cn("opacity-80 [&>svg]:size-4", className)}
data-slot="breadcrumb-separator"
role="presentation"
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden="true"
className={className}
data-slot="breadcrumb-ellipsis"
role="presentation"
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
@@ -0,0 +1,70 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
const buttonVariants = cva(
"[&_svg]:-mx-0.5 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 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]:shrink-0",
{
defaultVariants: {
size: "default",
variant: "default",
},
variants: {
size: {
default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
icon: "size-9 sm:size-8",
"icon-lg": "size-10 sm:size-9",
"icon-sm": "size-8 sm:size-7",
"icon-xl":
"size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
"icon-xs":
"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",
lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7",
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",
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",
},
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 [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [: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 [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none",
"destructive-outline":
"border-border bg-transparent bg-clip-padding 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%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-destructive/4",
ghost: "border-transparent hover:bg-accent data-pressed:bg-accent",
link: "border-transparent underline-offset-4 hover:underline",
outline:
"border-border bg-background bg-clip-padding 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%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64",
secondary:
"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/90 data-pressed:bg-secondary/90",
},
},
},
);
interface ButtonProps extends useRender.ComponentProps<"button"> {
variant?: VariantProps<typeof buttonVariants>["variant"];
size?: VariantProps<typeof buttonVariants>["size"];
}
function Button({ className, variant, size, render, ...props }: ButtonProps) {
const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] =
render ? undefined : "button";
const defaultProps = {
className: cn(buttonVariants({ className, size, variant })),
"data-slot": "button",
type: typeValue,
};
return useRender({
defaultTagName: "button",
props: mergeProps<"button">(defaultProps, props),
render,
});
}
export { Button, buttonVariants };
+93
View File
@@ -0,0 +1,93 @@
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"relative flex flex-col gap-6 rounded-2xl border bg-card bg-clip-padding py-6 text-card-foreground shadow-xs before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="card"
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
data-slot="card-header"
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("font-semibold text-lg leading-none", className)}
data-slot="card-title"
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("text-muted-foreground text-sm", className)}
data-slot="card-description"
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
data-slot="card-action"
{...props}
/>
);
}
function CardPanel({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("px-6", className)}
data-slot="card-content"
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
data-slot="card-footer"
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardPanel,
CardPanel as CardContent,
};
@@ -0,0 +1,16 @@
"use client";
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group";
import { cn } from "@coss/ui/lib/utils";
function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) {
return (
<CheckboxGroupPrimitive
className={cn("flex flex-col items-start gap-3", className)}
{...props}
/>
);
}
export { CheckboxGroup };
@@ -0,0 +1,60 @@
"use client";
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { cn } from "@coss/ui/lib/utils";
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
className={cn(
"relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-[4px] border border-input bg-background bg-clip-padding shadow-xs outline-none ring-ring transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[3px] not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/48 data-disabled:opacity-64 sm:size-4 dark:not-data-checked:bg-input/32 dark:bg-clip-border dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/8%)] [[data-disabled],[data-checked],[aria-invalid]]:shadow-none",
className,
)}
data-slot="checkbox"
{...props}
>
<CheckboxPrimitive.Indicator
className="-inset-px absolute flex items-center justify-center rounded-[4px] text-primary-foreground data-unchecked:hidden data-checked:bg-primary data-indeterminate:text-foreground"
data-slot="checkbox-indicator"
render={(props, state) => (
<span {...props}>
{state.indeterminate ? (
<svg
className="size-3.5 sm:size-3"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="3"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.252 12h13.496" />
</svg>
) : (
<svg
className="size-3.5 sm:size-3"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="3"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
)}
</span>
)}
/>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };
@@ -0,0 +1,45 @@
"use client";
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
import { cn } from "@coss/ui/lib/utils";
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
className,
...props
}: CollapsiblePrimitive.Trigger.Props) {
return (
<CollapsiblePrimitive.Trigger
className={cn("cursor-pointer", className)}
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsiblePanel({
className,
...props
}: CollapsiblePrimitive.Panel.Props) {
return (
<CollapsiblePrimitive.Panel
className={cn(
"h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 data-ending-style:h-0 data-starting-style:h-0",
className,
)}
data-slot="collapsible-panel"
{...props}
/>
);
}
export {
Collapsible,
CollapsibleTrigger,
CollapsiblePanel,
CollapsiblePanel as CollapsibleContent,
};
@@ -0,0 +1,362 @@
"use client";
import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
import { ChevronsUpDownIcon, XIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@coss/ui/lib/utils";
import { Input } from "@coss/ui/components/input";
import { ScrollArea } from "@coss/ui/components/scroll-area";
const ComboboxContext = React.createContext<{
chipsRef: React.RefObject<Element | null> | null;
multiple: boolean;
}>({
chipsRef: null,
multiple: false,
});
type ComboboxRootProps<
ItemValue,
Multiple extends boolean | undefined,
> = Parameters<typeof ComboboxPrimitive.Root<ItemValue, Multiple>>[0];
function Combobox<ItemValue, Multiple extends boolean | undefined = false>(
props: ComboboxPrimitive.Root.Props<ItemValue, Multiple>,
) {
const chipsRef = React.useRef<Element | null>(null);
return (
<ComboboxContext.Provider value={{ chipsRef, multiple: !!props.multiple }}>
<ComboboxPrimitive.Root
{...(props as ComboboxRootProps<ItemValue, Multiple>)}
/>
</ComboboxContext.Provider>
);
}
function ComboboxInput({
className,
showTrigger = true,
showClear = false,
size,
...props
}: Omit<ComboboxPrimitive.Input.Props, "size"> & {
showTrigger?: boolean;
showClear?: boolean;
size?: "sm" | "default" | "lg" | number;
}) {
const { multiple } = React.useContext(ComboboxContext);
const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
// multiple mode
if (multiple) {
return (
<ComboboxPrimitive.Input
className={cn(
"min-w-12 flex-1 text-base outline-none sm:text-sm [[data-slot=combobox-chip]+&]:ps-0.5",
sizeValue === "sm" ? "ps-1.5" : "ps-2",
className,
)}
data-size={typeof sizeValue === "string" ? sizeValue : undefined}
data-slot="combobox-input"
size={typeof sizeValue === "number" ? sizeValue : undefined}
{...props}
/>
);
}
// single mode
return (
<div className="relative w-full has-disabled:opacity-64">
<ComboboxPrimitive.Input
className={cn(
sizeValue === "sm"
? "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-6.5"
: "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-7",
className,
)}
data-slot="combobox-input"
render={<Input className="has-disabled:opacity-100" size={sizeValue} />}
{...props}
/>
{showTrigger && (
<ComboboxTrigger
className={cn(
"-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
>
<ChevronsUpDownIcon />
</ComboboxTrigger>
)}
{showClear && (
<ComboboxClear
className={cn(
"-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
sizeValue === "sm" ? "end-0" : "end-0.5",
)}
>
<XIcon />
</ComboboxClear>
)}
</div>
);
}
function ComboboxTrigger({
className,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
className={className}
data-slot="combobox-trigger"
{...props}
/>
);
}
function ComboboxPopup({
className,
children,
sideOffset = 4,
...props
}: ComboboxPrimitive.Popup.Props & {
sideOffset?: number;
}) {
const { chipsRef } = React.useContext(ComboboxContext);
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
anchor={chipsRef}
className="z-50 select-none"
data-slot="combobox-positioner"
sideOffset={sideOffset}
>
<span
className={cn(
"relative flex max-h-full origin-(--transform-origin) rounded-lg border bg-popover bg-clip-padding transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-lg has-data-starting-style:scale-98 has-data-starting-style:opacity-0 dark:not-in-data-[slot=group]:bg-clip-border",
className,
)}
>
<ComboboxPrimitive.Popup
className="flex max-h-[min(var(--available-height),23rem)] w-(--anchor-width) max-w-(--available-width) flex-col"
data-slot="combobox-popup"
{...props}
>
{children}
</ComboboxPrimitive.Popup>
</span>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
className={cn(
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="combobox-item"
{...props}
>
<ComboboxPrimitive.ItemIndicator className="col-start-1">
<svg
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/1500/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
</ComboboxPrimitive.ItemIndicator>
<div className="col-start-2">{children}</div>
</ComboboxPrimitive.Item>
);
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
data-slot="combobox-separator"
{...props}
/>
);
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
className={className}
data-slot="combobox-group"
{...props}
/>
);
}
function ComboboxGroupLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
className={cn(
"px-2 py-1.5 font-medium text-muted-foreground text-xs",
className,
)}
data-slot="combobox-group-label"
{...props}
/>
);
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
className={cn(
"not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
className,
)}
data-slot="combobox-empty"
{...props}
/>
);
}
function ComboboxRow({ className, ...props }: ComboboxPrimitive.Row.Props) {
return (
<ComboboxPrimitive.Row
className={className}
data-slot="combobox-row"
{...props}
/>
);
}
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ScrollArea scrollbarGutter scrollFade>
<ComboboxPrimitive.List
className={cn(
"not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3",
className,
)}
data-slot="combobox-list"
{...props}
/>
</ScrollArea>
);
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
className={className}
data-slot="combobox-clear"
{...props}
/>
);
}
function ComboboxStatus({
className,
...props
}: ComboboxPrimitive.Status.Props) {
return (
<ComboboxPrimitive.Status
className={cn(
"px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
className,
)}
data-slot="combobox-status"
{...props}
/>
);
}
function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
);
}
function ComboboxChips({ className, ...props }: ComboboxPrimitive.Chips.Props) {
const { chipsRef } = React.useContext(ComboboxContext);
return (
<ComboboxPrimitive.Chips
className={cn(
"relative inline-flex min-h-9 w-full flex-wrap gap-1 rounded-lg border border-input bg-background bg-clip-padding p-[calc(--spacing(1)-1px)] text-base shadow-xs outline-none ring-ring/24 transition-shadow *:min-h-7 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:border-ring focus-within:ring-[3px] has-disabled:pointer-events-none has-data-[size=lg]:min-h-10 has-data-[size=sm]:min-h-8 has-aria-invalid:border-destructive/36 has-disabled:opacity-64 has-[:disabled,:focus-within,[aria-invalid]]:shadow-none focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/16 has-data-[size=lg]:*:min-h-8 has-data-[size=sm]:*:min-h-6 sm:min-h-8 sm:text-sm sm:has-data-[size=lg]:min-h-9 sm:has-data-[size=sm]:min-h-7 sm:*:min-h-6 sm:has-data-[size=lg]:*:min-h-7 sm:has-data-[size=sm]:*:min-h-5 dark:not-has-disabled:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="combobox-chips"
ref={chipsRef as React.Ref<HTMLDivElement> | null}
{...props}
/>
);
}
function ComboboxChip({ children, ...props }: ComboboxPrimitive.Chip.Props) {
return (
<ComboboxPrimitive.Chip
className="flex items-center rounded-[calc(var(--radius-md)-1px)] bg-accent ps-2 font-medium text-accent-foreground text-sm outline-none sm:text-xs/(--text-xs--line-height) [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
data-slot="combobox-chip"
{...props}
>
{children}
<ComboboxChipRemove />
</ComboboxPrimitive.Chip>
);
}
function ComboboxChipRemove(props: ComboboxPrimitive.ChipRemove.Props) {
return (
<ComboboxPrimitive.ChipRemove
aria-label="Remove"
className="h-full shrink-0 cursor-pointer px-1.5 opacity-80 hover:opacity-100 [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
data-slot="combobox-chip-remove"
{...props}
>
<XIcon />
</ComboboxPrimitive.ChipRemove>
);
}
export {
Combobox,
ComboboxInput,
ComboboxTrigger,
ComboboxPopup,
ComboboxItem,
ComboboxSeparator,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxEmpty,
ComboboxValue,
ComboboxList,
ComboboxClear,
ComboboxStatus,
ComboboxRow,
ComboboxCollection,
ComboboxChips,
ComboboxChip,
};
+177
View File
@@ -0,0 +1,177 @@
"use client";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
import { cn } from "@coss/ui/lib/utils";
import { Button } from "@coss/ui/components/button";
import { ScrollArea } from "@coss/ui/components/scroll-area";
const Dialog = DialogPrimitive.Root;
const DialogPortal = DialogPrimitive.Portal;
function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogClose(props: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogBackdrop({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
className={cn(
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
className,
)}
data-slot="dialog-backdrop"
{...props}
/>
);
}
function DialogViewport({
className,
...props
}: DialogPrimitive.Viewport.Props) {
return (
<DialogPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 grid grid-rows-[1fr_auto] justify-items-center pt-12 sm:grid-rows-[1fr_auto_3fr] sm:p-4",
className,
)}
data-slot="dialog-viewport"
{...props}
/>
);
}
function DialogPopup({
className,
children,
showCloseButton = true,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
<DialogBackdrop />
<DialogViewport>
<DialogPrimitive.Popup
className={cn(
"sm:-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 flex-col border-t bg-popover bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-nested-dialog-open:origin-top data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden sm:max-w-lg sm:data-nested:data-ending-style:translate-y-8 sm:data-nested:data-starting-style:translate-y-8 sm:scale-[calc(1-0.1*var(--nested-dialogs))] sm:rounded-2xl sm:border sm:data-ending-style:scale-98 sm:data-starting-style:scale-98 sm:before:rounded-[calc(var(--radius-2xl)-1px)] dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="dialog-popup"
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
aria-label="Close"
className="absolute end-2 top-2"
render={<Button size="icon" variant="ghost" />}
>
<XIcon />
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogViewport>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex flex-col gap-2 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pb-3 max-sm:pb-4",
className,
)}
data-slot="dialog-header"
{...props}
/>
);
}
function DialogFooter({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & {
variant?: "default" | "bare";
}) {
return (
<div
className={cn(
"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
variant === "default" && "border-t bg-muted/50 py-4",
variant === "bare" &&
"in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pt-3 pt-4 pb-6",
className,
)}
data-slot="dialog-footer"
{...props}
/>
);
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
className={cn("font-heading text-xl leading-none", className)}
data-slot="dialog-title"
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="dialog-description"
{...props}
/>
);
}
function DialogPanel({ className, ...props }: React.ComponentProps<"div">) {
return (
<ScrollArea>
<div
className={cn(
"px-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-header])]:pt-1 in-[[data-slot=dialog-popup]:not(:has([data-slot=dialog-header]))]:pt-6 in-[[data-slot=dialog-popup]:not(:has([data-slot=dialog-footer]))]:pb-6! in-[[data-slot=dialog-popup]:not(:has([data-slot=dialog-footer].border-t))]:pb-1 pb-6",
className,
)}
data-slot="dialog-panel"
{...props}
/>
</ScrollArea>
);
}
export {
Dialog,
DialogTrigger,
DialogPortal,
DialogClose,
DialogBackdrop,
DialogBackdrop as DialogOverlay,
DialogPopup,
DialogPopup as DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogPanel,
DialogViewport,
};
+127
View File
@@ -0,0 +1,127 @@
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@coss/ui/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-xl border-dashed p-6 text-center md:p-12",
className,
)}
data-slot="empty"
{...props}
/>
);
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex max-w-sm flex-col items-center text-center",
className,
)}
data-slot="empty-header"
{...props}
/>
);
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
defaultVariants: {
variant: "default",
},
variants: {
variant: {
default: "bg-transparent",
icon: "relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-card text-foreground shadow-black/5 shadow-sm before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_svg:not([class*='size-'])]:size-4.5",
},
},
},
);
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
className={cn("relative mb-6", className)}
data-slot="empty-media"
data-variant={variant}
{...props}
>
{variant === "icon" && (
<>
<div
aria-hidden="true"
className={cn(
emptyMediaVariants({ className, variant }),
"-translate-x-0.5 -rotate-10 pointer-events-none absolute bottom-px origin-bottom-left scale-84 shadow-none",
)}
/>
<div
aria-hidden="true"
className={cn(
emptyMediaVariants({ className, variant }),
"pointer-events-none absolute bottom-px origin-bottom-right translate-x-0.5 rotate-10 scale-84 shadow-none",
)}
/>
</>
)}
<div
className={cn(emptyMediaVariants({ className, variant }))}
{...props}
/>
</div>
);
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("font-heading text-xl leading-none", className)}
data-slot="empty-title"
{...props}
/>
);
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
className={cn(
"text-muted-foreground text-sm/relaxed [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 [[data-slot=empty-title]+&]:mt-1",
className,
)}
data-slot="empty-description"
{...props}
/>
);
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm",
className,
)}
data-slot="empty-content"
{...props}
/>
);
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
};
+63
View File
@@ -0,0 +1,63 @@
"use client";
import { Field as FieldPrimitive } from "@base-ui/react/field";
import { cn } from "@coss/ui/lib/utils";
function Field({ className, ...props }: FieldPrimitive.Root.Props) {
return (
<FieldPrimitive.Root
className={cn("flex flex-col items-start gap-2", className)}
data-slot="field"
{...props}
/>
);
}
function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) {
return (
<FieldPrimitive.Label
className={cn(
"inline-flex items-center gap-2 font-medium text-base/4.5 sm:text-sm/4",
className,
)}
data-slot="field-label"
{...props}
/>
);
}
function FieldDescription({
className,
...props
}: FieldPrimitive.Description.Props) {
return (
<FieldPrimitive.Description
className={cn("text-muted-foreground text-xs", className)}
data-slot="field-description"
{...props}
/>
);
}
function FieldError({ className, ...props }: FieldPrimitive.Error.Props) {
return (
<FieldPrimitive.Error
className={cn("text-destructive-foreground text-xs", className)}
data-slot="field-error"
{...props}
/>
);
}
const FieldControl = FieldPrimitive.Control;
const FieldValidity = FieldPrimitive.Validity;
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldControl,
FieldValidity,
};
@@ -0,0 +1,29 @@
"use client";
import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset";
import { cn } from "@coss/ui/lib/utils";
function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) {
return (
<FieldsetPrimitive.Root
className={cn("flex w-full max-w-64 flex-col gap-6", className)}
data-slot="fieldset"
{...props}
/>
);
}
function FieldsetLegend({
className,
...props
}: FieldsetPrimitive.Legend.Props) {
return (
<FieldsetPrimitive.Legend
className={cn("font-semibold", className)}
data-slot="fieldset-legend"
{...props}
/>
);
}
export { Fieldset, FieldsetLegend };
+17
View File
@@ -0,0 +1,17 @@
"use client";
import { Form as FormPrimitive } from "@base-ui/react/form";
import { cn } from "@coss/ui/lib/utils";
function Form({ className, ...props }: FormPrimitive.Props) {
return (
<FormPrimitive
className={cn("flex w-full flex-col gap-4", className)}
data-slot="form"
{...props}
/>
);
}
export { Form };
+88
View File
@@ -0,0 +1,88 @@
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
function Frame({
className,
stackedPanels = false,
...props
}: React.ComponentProps<"div"> & { stackedPanels?: boolean }) {
return (
<div
className={cn(
"relative flex flex-col rounded-2xl bg-muted/50 p-1",
stackedPanels
? "*:has-[+[data-slot=frame-panel]]:rounded-b-none *:has-[+[data-slot=frame-panel]]:before:hidden dark:*:has-[+[data-slot=frame-panel]]:before:block *:[[data-slot=frame-panel]+[data-slot=frame-panel]]:rounded-t-none *:[[data-slot=frame-panel]+[data-slot=frame-panel]]:border-t-0 dark:*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:before:hidden"
: "*:[[data-slot=frame-panel]+[data-slot=frame-panel]]:mt-1",
className,
)}
data-slot="frame"
{...props}
/>
);
}
function FramePanel({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"relative rounded-xl border bg-background bg-clip-padding p-5 shadow-xs before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="frame-panel"
{...props}
/>
);
}
function FrameHeader({ className, ...props }: React.ComponentProps<"header">) {
return (
<header
className={cn("flex flex-col px-5 py-4", className)}
data-slot="frame-panel-header"
{...props}
/>
);
}
function FrameTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("font-semibold text-sm", className)}
data-slot="frame-panel-title"
{...props}
/>
);
}
function FrameDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn("text-muted-foreground text-sm", className)}
data-slot="frame-panel-description"
{...props}
/>
);
}
function FrameFooter({ className, ...props }: React.ComponentProps<"footer">) {
return (
<footer
className={cn("flex flex-col gap-1 px-5 py-4", className)}
data-slot="frame-panel-footer"
{...props}
/>
);
}
export {
Frame,
FramePanel,
FrameHeader,
FrameTitle,
FrameDescription,
FrameFooter,
};
+95
View File
@@ -0,0 +1,95 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
import { Separator } from "@coss/ui/components/separator";
const groupVariants = cva(
"flex w-fit *:focus-visible:z-10 has-[>[data-slot=group]]:gap-2 *:has-focus-visible:z-10",
{
defaultVariants: {
orientation: "horizontal",
},
variants: {
orientation: {
horizontal:
"*:not-first:before:-start-[0.5px] *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:before:-end-[0.5px] *:not-first:rounded-s-none *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:rounded-e-none *:not-first:border-s-0 *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:border-e-0 *:not-first:before:rounded-s-none *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:before:rounded-e-none *:after:absolute *:after:start-full *:after:h-full *:after:w-px *:pointer-coarse:after:min-w-auto",
vertical:
"*:not-first:before:-top-[0.5px] *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:before:-bottom-[0.5px] flex-col *:not-first:rounded-t-none *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:rounded-b-none *:not-first:border-t-0 *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:border-b-0 *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:before:hidden *:not-first:before:rounded-t-none *:not-nth-last-[1_of_:not(span[data-base-ui-focus-guard],span[data-base-ui-inert])]:before:rounded-b-none *:after:absolute *:after:top-full *:after:h-px *:pointer-coarse:after:min-h-auto *:after:w-full dark:*:last:before:hidden dark:*:first:before:block",
},
},
},
);
function Group({
className,
orientation,
children,
...props
}: {
className?: string;
orientation?: VariantProps<typeof groupVariants>["orientation"];
children: React.ReactNode;
} & React.ComponentProps<"div">) {
return (
<div
className={cn(groupVariants({ orientation }), className)}
data-orientation={orientation}
data-slot="group"
role="group"
{...props}
>
{children}
</div>
);
}
function GroupText({
className,
render,
...props
}: useRender.ComponentProps<"div">) {
const defaultProps = {
className: cn(
"relative inline-flex items-center whitespace-nowrap rounded-lg border border-border bg-muted bg-clip-padding px-[calc(--spacing(3)-1px)] text-muted-foreground text-base sm:text-sm shadow-xs outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/64 dark:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 [&_svg]:-mx-0.5",
className,
),
"data-slot": "group-text",
};
return useRender({
defaultTagName: "div",
props: mergeProps(defaultProps, props),
render,
});
}
function GroupSeparator({
className,
orientation = "vertical",
...props
}: {
className?: string;
} & React.ComponentProps<typeof Separator>) {
return (
<Separator
className={cn(
"[[data-slot=input-control]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&]:-translate-x-px pointer-events-none relative z-20 has-[+[data-slot=input-control]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:translate-x-px has-[+[data-slot=input-control]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:bg-ring [[data-slot=input-control]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+&]:bg-ring",
className,
)}
orientation={orientation}
{...props}
/>
);
}
export {
Group,
Group as ButtonGroup,
GroupText,
GroupText as ButtonGroupText,
GroupSeparator,
GroupSeparator as ButtonGroupSeparator,
groupVariants,
};
@@ -0,0 +1,99 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
import { Input, type InputProps } from "@coss/ui/components/input";
import { Textarea, type TextareaProps } from "@coss/ui/components/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background bg-clip-padding text-base shadow-xs ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:bg-clip-border dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/8%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
className,
)}
data-slot="input-group"
role="group"
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"[&_svg]:-mx-0.5 not-has-[button]:**:[svg]:not([class*='opacity-'])]:opacity-80 flex h-auto cursor-text select-none items-center justify-center gap-2 [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4",
{
defaultVariants: {
align: "inline-start",
},
variants: {
align: {
"block-end":
"order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
"block-start":
"order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
"inline-end":
"has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 order-last pe-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
"inline-start":
"has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 order-first ps-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
},
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
className={cn(inputGroupAddonVariants({ align }), className)}
data-align={align}
data-slot="input-group-addon"
onMouseDown={(e) => {
const target = e.target as HTMLElement;
const isInteractive = target.closest("button, a");
if (isInteractive) return;
e.preventDefault();
const parent = e.currentTarget.parentElement;
const input = parent?.querySelector<
HTMLInputElement | HTMLTextAreaElement
>("input, textarea");
if (input && !parent?.querySelector("input:focus, textarea:focus")) {
input.focus();
}
}}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"[&_svg]:-mx-0.5 line-clamp-1 flex items-center gap-2 text-muted-foreground in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className,
)}
{...props}
/>
);
}
function InputGroupInput({ className, ...props }: InputProps) {
return <Input className={className} unstyled {...props} />;
}
function InputGroupTextarea({ className, ...props }: TextareaProps) {
return <Textarea className={className} unstyled {...props} />;
}
export {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
+53
View File
@@ -0,0 +1,53 @@
"use client";
import { Input as InputPrimitive } from "@base-ui/react/input";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
type InputProps = Omit<
InputPrimitive.Props & React.RefAttributes<HTMLInputElement>,
"size"
> & {
size?: "sm" | "default" | "lg" | number;
unstyled?: boolean;
};
function Input({
className,
size = "default",
unstyled = false,
...props
}: InputProps) {
return (
<span
className={
cn(
!unstyled &&
"relative inline-flex w-full rounded-lg border border-input bg-background bg-clip-padding text-base shadow-xs ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
) || undefined
}
data-size={size}
data-slot="input-control"
>
<InputPrimitive
className={cn(
"h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5",
size === "sm" &&
"h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
props.type === "search" &&
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
props.type === "file" &&
"text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
)}
data-slot="input"
size={typeof size === "number" ? size : undefined}
{...props}
/>
</span>
);
}
export { Input, type InputProps };
+28
View File
@@ -0,0 +1,28 @@
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
className={cn(
"pointer-events-none inline-flex h-5 min-w-5 select-none items-center justify-center gap-1 rounded bg-muted px-1 font-medium font-sans text-muted-foreground text-xs [&_svg:not([class*='size-'])]:size-3",
className,
)}
data-slot="kbd"
{...props}
/>
);
}
function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
className={cn("inline-flex items-center gap-1", className)}
data-slot="kbd-group"
{...props}
/>
);
}
export { Kbd, KbdGroup };
+26
View File
@@ -0,0 +1,26 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cn } from "@coss/ui/lib/utils";
function Label({
className,
render,
...props
}: useRender.ComponentProps<"label">) {
const defaultProps = {
className: cn(
"inline-flex items-center gap-2 text-base/4.5 sm:text-sm/4 font-medium",
className,
),
"data-slot": "label",
};
return useRender({
defaultTagName: "label",
props: mergeProps<"label">(defaultProps, props),
render,
});
}
export { Label };
+286
View File
@@ -0,0 +1,286 @@
"use client";
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { ChevronRightIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
const Menu = MenuPrimitive.Root;
const MenuPortal = MenuPrimitive.Portal;
function MenuTrigger(props: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="menu-trigger" {...props} />;
}
function MenuPopup({
children,
className,
sideOffset = 4,
align = "center",
alignOffset,
side = "bottom",
...props
}: MenuPrimitive.Popup.Props & {
align?: MenuPrimitive.Positioner.Props["align"];
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
side?: MenuPrimitive.Positioner.Props["side"];
}) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
align={align}
alignOffset={alignOffset}
className="z-50"
data-slot="menu-positioner"
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
className={cn(
"relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg border bg-popover bg-clip-padding shadow-lg outline-none transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] focus:outline-none has-data-starting-style:scale-98 has-data-starting-style:opacity-0 dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="menu-popup"
{...props}
>
<div className="max-h-(--available-height) w-full overflow-y-auto p-1">
{children}
</div>
</MenuPrimitive.Popup>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
);
}
function MenuGroup(props: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="menu-group" {...props} />;
}
function MenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenuPrimitive.Item
className={cn(
"[&_svg]:-mx-0.5 flex min-h-8 cursor-default select-none items-center gap-2 rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-inset:ps-8 data-[variant=destructive]:text-destructive-foreground data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 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]:shrink-0",
className,
)}
data-inset={inset}
data-slot="menu-item"
data-variant={variant}
{...props}
/>
);
}
function MenuCheckboxItem({
className,
children,
checked,
...props
}: MenuPrimitive.CheckboxItem.Props) {
return (
<MenuPrimitive.CheckboxItem
checked={checked}
className={cn(
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="menu-checkbox-item"
{...props}
>
<MenuPrimitive.CheckboxItemIndicator className="col-start-1">
<svg
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
</MenuPrimitive.CheckboxItemIndicator>
<span className="col-start-2">{children}</span>
</MenuPrimitive.CheckboxItem>
);
}
function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
}
function MenuRadioItem({
className,
children,
...props
}: MenuPrimitive.RadioItem.Props) {
return (
<MenuPrimitive.RadioItem
className={cn(
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="menu-radio-item"
{...props}
>
<MenuPrimitive.RadioItemIndicator className="col-start-1">
<svg
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
</MenuPrimitive.RadioItemIndicator>
<span className="col-start-2">{children}</span>
</MenuPrimitive.RadioItem>
);
}
function MenuGroupLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.GroupLabel
className={cn(
"px-2 py-1.5 font-medium text-muted-foreground text-xs data-inset:ps-9 sm:data-inset:ps-8",
className,
)}
data-inset={inset}
data-slot="menu-label"
{...props}
/>
);
}
function MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
className={cn("mx-2 my-1 h-px bg-border", className)}
data-slot="menu-separator"
{...props}
/>
);
}
function MenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"ms-auto font-medium text-muted-foreground/72 text-xs tracking-widest",
className,
)}
data-slot="menu-shortcut"
{...props}
/>
);
}
function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />;
}
function MenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.SubmenuTrigger
className={cn(
"flex min-h-8 items-center gap-2 rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className,
)}
data-inset={inset}
data-slot="menu-sub-trigger"
{...props}
>
{children}
<ChevronRightIcon className="-me-0.5 ms-auto opacity-80" />
</MenuPrimitive.SubmenuTrigger>
);
}
function MenuSubPopup({
className,
sideOffset = 0,
alignOffset,
align = "start",
...props
}: MenuPrimitive.Popup.Props & {
align?: MenuPrimitive.Positioner.Props["align"];
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
}) {
const defaultAlignOffset = align !== "center" ? -5 : undefined;
return (
<MenuPopup
align={align}
alignOffset={alignOffset ?? defaultAlignOffset}
className={className}
data-slot="menu-sub-content"
side="inline-end"
sideOffset={sideOffset}
{...props}
/>
);
}
export {
Menu,
Menu as DropdownMenu,
MenuPortal,
MenuPortal as DropdownMenuPortal,
MenuTrigger,
MenuTrigger as DropdownMenuTrigger,
MenuPopup,
MenuPopup as DropdownMenuContent,
MenuGroup,
MenuGroup as DropdownMenuGroup,
MenuItem,
MenuItem as DropdownMenuItem,
MenuCheckboxItem,
MenuCheckboxItem as DropdownMenuCheckboxItem,
MenuRadioGroup,
MenuRadioGroup as DropdownMenuRadioGroup,
MenuRadioItem,
MenuRadioItem as DropdownMenuRadioItem,
MenuGroupLabel,
MenuGroupLabel as DropdownMenuLabel,
MenuSeparator,
MenuSeparator as DropdownMenuSeparator,
MenuShortcut,
MenuShortcut as DropdownMenuShortcut,
MenuSub,
MenuSub as DropdownMenuSub,
MenuSubTrigger,
MenuSubTrigger as DropdownMenuSubTrigger,
MenuSubPopup,
MenuSubPopup as DropdownMenuSubContent,
};
+67
View File
@@ -0,0 +1,67 @@
"use client";
import { Meter as MeterPrimitive } from "@base-ui/react/meter";
import { cn } from "@coss/ui/lib/utils";
function Meter({ className, children, ...props }: MeterPrimitive.Root.Props) {
return (
<MeterPrimitive.Root
className={cn("flex w-full flex-col gap-2", className)}
{...props}
>
{children ? (
children
) : (
<MeterTrack>
<MeterIndicator />
</MeterTrack>
)}
</MeterPrimitive.Root>
);
}
function MeterLabel({ className, ...props }: MeterPrimitive.Label.Props) {
return (
<MeterPrimitive.Label
className={cn("font-medium text-sm", className)}
data-slot="meter-label"
{...props}
/>
);
}
function MeterTrack({ className, ...props }: MeterPrimitive.Track.Props) {
return (
<MeterPrimitive.Track
className={cn("block h-2 w-full overflow-hidden bg-input", className)}
data-slot="meter-track"
{...props}
/>
);
}
function MeterIndicator({
className,
...props
}: MeterPrimitive.Indicator.Props) {
return (
<MeterPrimitive.Indicator
className={cn("bg-primary transition-all duration-500", className)}
data-slot="meter-indicator"
{...props}
/>
);
}
function MeterValue({ className, ...props }: MeterPrimitive.Value.Props) {
return (
<MeterPrimitive.Value
className={cn("text-sm tabular-nums", className)}
data-slot="meter-value"
{...props}
/>
);
}
export { Meter, MeterLabel, MeterTrack, MeterIndicator, MeterValue };
@@ -0,0 +1,160 @@
"use client";
import { NumberField as NumberFieldPrimitive } from "@base-ui/react/number-field";
import { MinusIcon, PlusIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@coss/ui/lib/utils";
import { Label } from "@coss/ui/components/label";
const NumberFieldContext = React.createContext<{
fieldId: string;
} | null>(null);
function NumberField({
id,
className,
size = "default",
...props
}: NumberFieldPrimitive.Root.Props & {
size?: "sm" | "default" | "lg";
}) {
const generatedId = React.useId();
const fieldId = id ?? generatedId;
return (
<NumberFieldContext.Provider value={{ fieldId }}>
<NumberFieldPrimitive.Root
className={cn("flex w-full flex-col items-start gap-2", className)}
data-size={size}
data-slot="number-field"
id={fieldId}
{...props}
/>
</NumberFieldContext.Provider>
);
}
function NumberFieldGroup({
className,
...props
}: NumberFieldPrimitive.Group.Props) {
return (
<NumberFieldPrimitive.Group
className={cn(
"relative flex w-full justify-between rounded-lg border border-input bg-background bg-clip-padding text-base shadow-xs ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:border-ring focus-within:ring-[3px] has-aria-invalid:border-destructive/36 focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/48 data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:has-aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-within,[aria-invalid]]:shadow-none",
className,
)}
data-slot="number-field-group"
{...props}
/>
);
}
function NumberFieldDecrement({
className,
...props
}: NumberFieldPrimitive.Decrement.Props) {
return (
<NumberFieldPrimitive.Decrement
className={cn(
"relative flex shrink-0 cursor-pointer items-center justify-center rounded-s-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent",
className,
)}
data-slot="number-field-decrement"
{...props}
>
<MinusIcon />
</NumberFieldPrimitive.Decrement>
);
}
function NumberFieldIncrement({
className,
...props
}: NumberFieldPrimitive.Increment.Props) {
return (
<NumberFieldPrimitive.Increment
className={cn(
"relative flex shrink-0 cursor-pointer items-center justify-center rounded-e-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent",
className,
)}
data-slot="number-field-increment"
{...props}
>
<PlusIcon />
</NumberFieldPrimitive.Increment>
);
}
function NumberFieldInput({
className,
...props
}: NumberFieldPrimitive.Input.Props) {
return (
<NumberFieldPrimitive.Input
className={cn(
"h-8.5 in-data-[size=lg]:h-9.5 in-data-[size=sm]:h-7.5 w-full min-w-0 grow bg-transparent in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] text-center tabular-nums in-data-[size=lg]:leading-9.5 in-data-[size=sm]:leading-7.5 leading-8.5 outline-none sm:h-7.5 sm:in-data-[size=lg]:h-8.5 sm:in-data-[size=sm]:h-6.5 sm:in-data-[size=lg]:leading-8.5 sm:in-data-[size=sm]:leading-8.5 sm:leading-7.5",
className,
)}
data-slot="number-field-input"
{...props}
/>
);
}
function NumberFieldScrubArea({
className,
label,
...props
}: NumberFieldPrimitive.ScrubArea.Props & {
label: string;
}) {
const context = React.useContext(NumberFieldContext);
if (!context) {
throw new Error(
"NumberFieldScrubArea must be used within a NumberField component for accessibility.",
);
}
return (
<NumberFieldPrimitive.ScrubArea
className={cn("flex cursor-ew-resize", className)}
data-slot="number-field-scrub-area"
{...props}
>
<Label className="cursor-ew-resize" htmlFor={context.fieldId}>
{label}
</Label>
<NumberFieldPrimitive.ScrubAreaCursor className="drop-shadow-[0_1px_1px_#0008] filter">
<CursorGrowIcon />
</NumberFieldPrimitive.ScrubAreaCursor>
</NumberFieldPrimitive.ScrubArea>
);
}
function CursorGrowIcon(props: React.ComponentProps<"svg">) {
return (
<svg
fill="black"
height="14"
stroke="white"
viewBox="0 0 24 14"
width="26"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M19.5 5.5L6.49737 5.51844V2L1 6.9999L6.5 12L6.49737 8.5L19.5 8.5V12L25 6.9999L19.5 2V5.5Z" />
</svg>
);
}
export {
NumberField,
NumberFieldScrubArea,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldGroup,
NumberFieldInput,
};
@@ -0,0 +1,134 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
import { type Button, buttonVariants } from "@coss/ui/components/button";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
data-slot="pagination"
{...props}
/>
);
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
className={cn("flex flex-row items-center gap-1", className)}
data-slot="pagination-content"
{...props}
/>
);
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />;
}
type PaginationLinkProps = {
isActive?: boolean;
size?: React.ComponentProps<typeof Button>["size"];
} & useRender.ComponentProps<"a">;
function PaginationLink({
className,
isActive,
size = "icon",
render,
...props
}: PaginationLinkProps) {
const defaultProps = {
"aria-current": isActive ? ("page" as const) : undefined,
className: render
? className
: cn(
buttonVariants({
size,
variant: isActive ? "outline" : "ghost",
}),
className,
),
"data-active": isActive,
"data-slot": "pagination-link",
};
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(defaultProps, props),
render,
});
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
className={cn("max-sm:aspect-square max-sm:p-0", className)}
size="default"
{...props}
>
<ChevronLeftIcon className="sm:-ms-1" />
<span className="max-sm:hidden">Previous</span>
</PaginationLink>
);
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
className={cn("max-sm:aspect-square max-sm:p-0", className)}
size="default"
{...props}
>
<span className="max-sm:hidden">Next</span>
<ChevronRightIcon className="sm:-me-1" />
</PaginationLink>
);
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
className={cn("flex min-w-7 justify-center", className)}
data-slot="pagination-ellipsis"
{...props}
>
<MoreHorizontalIcon className="size-5 sm:size-4" />
<span className="sr-only">More pages</span>
</span>
);
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};
+104
View File
@@ -0,0 +1,104 @@
"use client";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { cn } from "@coss/ui/lib/utils";
const PopoverCreateHandle = PopoverPrimitive.createHandle;
const Popover = PopoverPrimitive.Root;
function PopoverTrigger(props: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverPopup({
children,
className,
side = "bottom",
align = "center",
sideOffset = 4,
alignOffset = 0,
tooltipStyle = false,
...props
}: PopoverPrimitive.Popup.Props & {
side?: PopoverPrimitive.Positioner.Props["side"];
align?: PopoverPrimitive.Positioner.Props["align"];
sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"];
alignOffset?: PopoverPrimitive.Positioner.Props["alignOffset"];
tooltipStyle?: boolean;
}) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"
data-slot="popover-positioner"
side={side}
sideOffset={sideOffset}
>
<PopoverPrimitive.Popup
className={cn(
"relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg border bg-popover bg-clip-padding text-popover-foreground shadow-lg transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
tooltipStyle &&
"w-fit text-balance rounded-md text-xs shadow-black/5 shadow-md before:rounded-[calc(var(--radius-md)-1px)]",
className,
)}
data-slot="popover-popup"
{...props}
>
<PopoverPrimitive.Viewport
className={cn(
"relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) py-4 outline-none [--viewport-inline-padding:--spacing(4)] data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity",
tooltipStyle
? "py-1 [--viewport-inline-padding:--spacing(2)]"
: "not-data-transitioning:overflow-y-auto",
)}
data-slot="popover-viewport"
>
{children}
</PopoverPrimitive.Viewport>
</PopoverPrimitive.Popup>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
);
}
function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
}
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return (
<PopoverPrimitive.Title
className={cn("font-semibold text-lg leading-none", className)}
data-slot="popover-title"
{...props}
/>
);
}
function PopoverDescription({
className,
...props
}: PopoverPrimitive.Description.Props) {
return (
<PopoverPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="popover-description"
{...props}
/>
);
}
export {
PopoverCreateHandle,
Popover,
PopoverTrigger,
PopoverPopup,
PopoverPopup as PopoverContent,
PopoverTitle,
PopoverDescription,
PopoverClose,
};
@@ -0,0 +1,55 @@
"use client";
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
import { cn } from "@coss/ui/lib/utils";
const PreviewCard = PreviewCardPrimitive.Root;
function PreviewCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
return (
<PreviewCardPrimitive.Trigger data-slot="preview-card-trigger" {...props} />
);
}
function PreviewCardPopup({
className,
children,
align = "center",
sideOffset = 4,
...props
}: PreviewCardPrimitive.Popup.Props & {
align?: PreviewCardPrimitive.Positioner.Props["align"];
sideOffset?: PreviewCardPrimitive.Positioner.Props["sideOffset"];
}) {
return (
<PreviewCardPrimitive.Portal>
<PreviewCardPrimitive.Positioner
align={align}
className="z-50"
data-slot="preview-card-positioner"
sideOffset={sideOffset}
>
<PreviewCardPrimitive.Popup
className={cn(
"relative flex w-64 origin-(--transform-origin) text-balance rounded-lg border bg-popover bg-clip-padding p-4 text-popover-foreground text-sm shadow-lg transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="preview-card-content"
{...props}
>
{children}
</PreviewCardPrimitive.Popup>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
);
}
export {
PreviewCard,
PreviewCard as HoverCard,
PreviewCardTrigger,
PreviewCardTrigger as HoverCardTrigger,
PreviewCardPopup,
PreviewCardPopup as HoverCardContent,
};
@@ -0,0 +1,81 @@
"use client";
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
import { cn } from "@coss/ui/lib/utils";
function Progress({
className,
children,
...props
}: ProgressPrimitive.Root.Props) {
return (
<ProgressPrimitive.Root
className={cn("flex w-full flex-col gap-2", className)}
data-slot="progress"
{...props}
>
{children ? (
children
) : (
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
)}
</ProgressPrimitive.Root>
);
}
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
return (
<ProgressPrimitive.Label
className={cn("font-medium text-sm", className)}
data-slot="progress-label"
{...props}
/>
);
}
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
return (
<ProgressPrimitive.Track
className={cn(
"block h-1.5 w-full overflow-hidden rounded-full bg-input",
className,
)}
data-slot="progress-track"
{...props}
/>
);
}
function ProgressIndicator({
className,
...props
}: ProgressPrimitive.Indicator.Props) {
return (
<ProgressPrimitive.Indicator
className={cn("bg-primary transition-all duration-500", className)}
data-slot="progress-indicator"
{...props}
/>
);
}
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
return (
<ProgressPrimitive.Value
className={cn("text-sm tabular-nums", className)}
data-slot="progress-value"
{...props}
/>
);
}
export {
Progress,
ProgressLabel,
ProgressTrack,
ProgressIndicator,
ProgressValue,
};
@@ -0,0 +1,36 @@
"use client";
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
import { cn } from "@coss/ui/lib/utils";
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
return (
<RadioGroupPrimitive
className={cn("flex flex-col gap-3", className)}
data-slot="radio-group"
{...props}
/>
);
}
function Radio({ className, ...props }: RadioPrimitive.Root.Props) {
return (
<RadioPrimitive.Root
className={cn(
"relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-background bg-clip-padding shadow-xs outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-full not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/48 data-disabled:opacity-64 sm:size-4 dark:not-data-checked:bg-input/32 dark:bg-clip-border dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/8%)] [[data-disabled],[data-checked],[aria-invalid]]:shadow-none",
className,
)}
data-slot="radio"
{...props}
>
<RadioPrimitive.Indicator
className="-inset-px absolute flex size-4.5 items-center justify-center rounded-full before:size-2 before:rounded-full before:bg-primary-foreground data-unchecked:hidden data-checked:bg-primary sm:size-4 sm:before:size-1.5"
data-slot="radio-indicator"
/>
</RadioPrimitive.Root>
);
}
export { RadioGroup, Radio, Radio as RadioGroupItem };
@@ -0,0 +1,64 @@
"use client";
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
import { cn } from "@coss/ui/lib/utils";
function ScrollArea({
className,
children,
scrollFade = false,
scrollbarGutter = false,
...props
}: ScrollAreaPrimitive.Root.Props & {
scrollFade?: boolean;
scrollbarGutter?: boolean;
}) {
return (
<ScrollAreaPrimitive.Root
className={cn("size-full min-h-0", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
className={cn(
"h-full overscroll-contain rounded-[inherit] outline-none transition-shadows focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
scrollFade &&
"mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] [--fade-size:1.5rem]",
scrollbarGutter &&
"data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
)}
data-slot="scroll-area-viewport"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar orientation="vertical" />
<ScrollBar orientation="horizontal" />
<ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: ScrollAreaPrimitive.Scrollbar.Props) {
return (
<ScrollAreaPrimitive.Scrollbar
className={cn(
"m-1 flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
className,
)}
data-slot="scroll-area-scrollbar"
orientation={orientation}
{...props}
>
<ScrollAreaPrimitive.Thumb
className="relative flex-1 rounded-full bg-foreground/20"
data-slot="scroll-area-thumb"
/>
</ScrollAreaPrimitive.Scrollbar>
);
}
export { ScrollArea, ScrollBar };
+180
View File
@@ -0,0 +1,180 @@
"use client";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import {
ChevronDownIcon,
ChevronsUpDownIcon,
ChevronUpIcon,
} from "lucide-react";
import { cn } from "@coss/ui/lib/utils";
const Select = SelectPrimitive.Root;
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default" | "lg";
}) {
return (
<SelectPrimitive.Trigger
className={cn(
"relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-between gap-2 rounded-lg border border-input bg-background bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base shadow-xs outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:bg-input/32 dark:not-in-data-[slot=group]:bg-clip-border dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_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]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
size === "sm" &&
"min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
size === "lg" && "min-h-10 sm:min-h-9",
className,
)}
data-slot="select-trigger"
{...props}
>
{children}
<SelectPrimitive.Icon data-slot="select-icon">
<ChevronsUpDownIcon className="-me-1 size-4.5 opacity-80 sm:size-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
className={cn(
"flex-1 truncate data-placeholder:text-muted-foreground",
className,
)}
data-slot="select-value"
{...props}
/>
);
}
function SelectPopup({
className,
children,
sideOffset = 4,
alignItemWithTrigger = true,
...props
}: SelectPrimitive.Popup.Props & {
sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
alignItemWithTrigger?: SelectPrimitive.Positioner.Props["alignItemWithTrigger"];
}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
alignItemWithTrigger={alignItemWithTrigger}
className="z-50 select-none"
data-slot="select-positioner"
sideOffset={sideOffset}
>
<SelectPrimitive.Popup
className="origin-(--transform-origin) transition-[scale,opacity] has-data-[side=none]:scale-100 has-data-starting-style:scale-98 has-data-starting-style:opacity-0 has-data-[side=none]:transition-none"
data-slot="select-popup"
{...props}
>
<SelectPrimitive.ScrollUpArrow
className="top-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:top-px before:h-[200%] before:rounded-t-[calc(var(--radius-lg)-1px)] before:bg-linear-to-b before:from-50% before:from-popover"
data-slot="select-scroll-up-arrow"
>
<ChevronUpIcon className="relative size-4.5 sm:size-4" />
</SelectPrimitive.ScrollUpArrow>
<span className="relative block h-full rounded-lg border bg-popover bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-lg dark:not-in-data-[slot=group]:bg-clip-border">
<SelectPrimitive.List
className={cn(
"max-h-(--available-height) min-w-(--anchor-width) overflow-y-auto p-1",
className,
)}
data-slot="select-list"
>
{children}
</SelectPrimitive.List>
</span>
<SelectPrimitive.ScrollDownArrow
className="bottom-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:bottom-px before:h-[200%] before:rounded-b-[calc(var(--radius-lg)-1px)] before:bg-linear-to-t before:from-50% before:from-popover"
data-slot="select-scroll-down-arrow"
>
<ChevronDownIcon className="relative size-4.5 sm:size-4" />
</SelectPrimitive.ScrollDownArrow>
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
);
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
className={cn(
"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
data-slot="select-item"
{...props}
>
<SelectPrimitive.ItemIndicator className="col-start-1">
<svg
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/1500/svg"
>
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
</svg>
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="col-start-2 min-w-0">
{children}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
className={cn("mx-2 my-1 h-px bg-border", className)}
data-slot="select-separator"
{...props}
/>
);
}
function SelectGroup(props: SelectPrimitive.Group.Props) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
className="px-2 py-1.5 font-medium text-muted-foreground text-xs"
data-slot="select-group-label"
{...props}
/>
);
}
export {
Select,
SelectTrigger,
SelectValue,
SelectPopup,
SelectPopup as SelectContent,
SelectItem,
SelectSeparator,
SelectGroup,
SelectGroupLabel,
};
@@ -0,0 +1,23 @@
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
import { cn } from "@coss/ui/lib/utils";
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
className={cn(
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:not-[[class^='h-']]:not-[[class*='_h-']]:self-stretch",
className,
)}
data-slot="separator"
orientation={orientation}
{...props}
/>
);
}
export { Separator };
+200
View File
@@ -0,0 +1,200 @@
"use client";
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
import { cn } from "@coss/ui/lib/utils";
import { Button } from "@coss/ui/components/button";
import { ScrollArea } from "@coss/ui/components/scroll-area";
const Sheet = SheetPrimitive.Root;
const SheetPortal = SheetPrimitive.Portal;
function SheetTrigger(props: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose(props: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
className={cn(
"fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
className,
)}
data-slot="sheet-backdrop"
{...props}
/>
);
}
function SheetViewport({
className,
side,
inset = false,
...props
}: SheetPrimitive.Viewport.Props & {
side?: "right" | "left" | "top" | "bottom";
inset?: boolean;
}) {
return (
<SheetPrimitive.Viewport
className={cn(
"fixed inset-0 z-50 grid",
side === "bottom" && "grid grid-rows-[1fr_auto] pt-12",
side === "top" && "grid grid-rows-[auto_1fr] pb-12",
side === "left" && "flex justify-start",
side === "right" && "flex justify-end",
inset && "sm:p-4",
)}
data-slot="sheet-viewport"
{...props}
/>
);
}
function SheetPopup({
className,
children,
showCloseButton = true,
side = "right",
inset = false,
...props
}: SheetPrimitive.Popup.Props & {
showCloseButton?: boolean;
side?: "right" | "left" | "top" | "bottom";
inset?: boolean;
}) {
return (
<SheetPortal>
<SheetBackdrop />
<SheetViewport inset={inset} side={side}>
<SheetPrimitive.Popup
className={cn(
"relative flex max-h-full min-h-0 w-full min-w-0 flex-col bg-popover bg-clip-padding text-popover-foreground shadow-lg transition-[opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:before:hidden dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
side === "bottom" &&
"row-start-2 border-t data-ending-style:translate-y-8 data-starting-style:translate-y-8",
side === "top" &&
"data-ending-style:-translate-y-8 data-starting-style:-translate-y-8 border-b",
side === "left" &&
"data-ending-style:-translate-x-8 data-starting-style:-translate-x-8 w-[calc(100%-(--spacing(12)))] max-w-md border-e",
side === "right" &&
"col-start-2 w-[calc(100%-(--spacing(12)))] max-w-md border-s data-ending-style:translate-x-8 data-starting-style:translate-x-8",
inset &&
"before:hidden sm:rounded-2xl sm:border sm:before:rounded-[calc(var(--radius-2xl)-1px)] sm:**:data-[slot=sheet-footer]:rounded-b-[calc(var(--radius-2xl)-1px)]",
className,
)}
data-slot="sheet-popup"
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
aria-label="Close"
className="absolute end-2 top-2"
render={<Button size="icon" variant="ghost" />}
>
<XIcon />
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetViewport>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex flex-col gap-2 p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pb-3 max-sm:pb-4",
className,
)}
data-slot="sheet-header"
{...props}
/>
);
}
function SheetFooter({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & {
variant?: "default" | "bare";
}) {
return (
<div
className={cn(
"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end",
variant === "default" && "border-t bg-muted/50 py-4",
variant === "bare" &&
"in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pt-3 pt-4 pb-6",
className,
)}
data-slot="sheet-footer"
{...props}
/>
);
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
className={cn("font-heading text-xl leading-none", className)}
data-slot="sheet-title"
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: SheetPrimitive.Description.Props) {
return (
<SheetPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="sheet-description"
{...props}
/>
);
}
function SheetPanel({
className,
scrollFade = true,
...props
}: React.ComponentProps<"div"> & { scrollFade?: boolean }) {
return (
<ScrollArea scrollFade={scrollFade}>
<div
className={cn(
"px-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-header])]:pt-1 in-[[data-slot=sheet-popup]:not(:has([data-slot=sheet-header]))]:pt-6 in-[[data-slot=sheet-popup]:not(:has([data-slot=sheet-footer]))]:pb-6! in-[[data-slot=sheet-popup]:not(:has([data-slot=sheet-footer].border-t))]:pb-1 pb-6",
className,
)}
data-slot="sheet-panel"
{...props}
/>
</ScrollArea>
);
}
export {
Sheet,
SheetTrigger,
SheetPortal,
SheetClose,
SheetBackdrop,
SheetBackdrop as SheetOverlay,
SheetPopup,
SheetPopup as SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
SheetPanel,
};
+737
View File
@@ -0,0 +1,737 @@
"use client";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import * as React from "react";
import { useIsMobile } from "@coss/ui/hooks/use-mobile";
import { cn } from "@coss/ui/lib/utils";
import { Button } from "@coss/ui/components/button";
import { Input } from "@coss/ui/components/input";
import { Separator } from "@coss/ui/components/separator";
import {
Sheet,
SheetDescription,
SheetHeader,
SheetPopup,
SheetTitle,
} from "@coss/ui/components/sheet";
import { Skeleton } from "@coss/ui/components/skeleton";
import {
Tooltip,
TooltipPopup,
TooltipTrigger,
} from "@coss/ui/components/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
async (value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
await cookieStore.set({
expires: Date.now() + SIDEBAR_COOKIE_MAX_AGE * 1000,
name: SIDEBAR_COOKIE_NAME,
path: "/",
value: String(openState),
});
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
isMobile,
open,
openMobile,
setOpen,
setOpenMobile,
state,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<div
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
className,
)}
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
{...props}
>
{children}
</div>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
className,
)}
data-slot="sidebar"
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet onOpenChange={setOpenMobile} open={openMobile} {...props}>
<SheetPopup
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
data-mobile="true"
data-sidebar="sidebar"
data-slot="sidebar"
side={side}
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetPopup>
</Sheet>
);
}
return (
<div
className="group peer hidden text-sidebar-foreground md:block"
data-collapsible={state === "collapsed" ? collapsible : ""}
data-side={side}
data-slot="sidebar"
data-state={state}
data-variant={variant}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
data-slot="sidebar-gap"
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
data-slot="sidebar-container"
{...props}
>
<div
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
data-sidebar="sidebar"
data-slot="sidebar-inner"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar();
return (
<Button
className={cn("size-7", className)}
data-sidebar="trigger"
data-slot="sidebar-trigger"
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
size="icon"
variant="ghost"
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
aria-label="Toggle Sidebar"
className={cn(
"-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
data-sidebar="rail"
data-slot="sidebar-rail"
onClick={toggleSidebar}
tabIndex={-1}
title="Toggle Sidebar"
type="button"
{...props}
/>
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
className={cn(
"relative flex w-full flex-1 flex-col bg-background",
"md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ms-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm",
className,
)}
data-slot="sidebar-inset"
{...props}
/>
);
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
className={cn("h-8 w-full bg-background shadow-none", className)}
data-sidebar="input"
data-slot="sidebar-input"
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex flex-col gap-2 p-2", className)}
data-sidebar="header"
data-slot="sidebar-header"
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex flex-col gap-2 p-2", className)}
data-sidebar="footer"
data-slot="sidebar-footer"
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
className={cn("mx-2 w-auto bg-sidebar-border", className)}
data-sidebar="separator"
data-slot="sidebar-separator"
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
data-sidebar="content"
data-slot="sidebar-content"
{...props}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
data-sidebar="group"
data-slot="sidebar-group"
{...props}
/>
);
}
function SidebarGroupLabel({
className,
render,
...props
}: useRender.ComponentProps<"div">) {
const defaultProps = {
className: cn(
"flex h-8 shrink-0 items-center rounded-lg px-2 font-medium text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
),
"data-sidebar": "group-label",
"data-slot": "sidebar-group-label",
};
return useRender({
defaultTagName: "div",
props: mergeProps(defaultProps, props),
render,
});
}
function SidebarGroupAction({
className,
render,
...props
}: useRender.ComponentProps<"button">) {
const defaultProps = {
className: cn(
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-lg p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:-inset-2 after:absolute md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
),
"data-sidebar": "group-action",
"data-slot": "sidebar-group-action",
};
return useRender({
defaultTagName: "button",
props: mergeProps(defaultProps, props),
render,
});
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn("w-full text-sm", className)}
data-sidebar="group-content"
data-slot="sidebar-group-content"
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
data-sidebar="menu"
data-slot="sidebar-menu"
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
className={cn("group/menu-item relative", className)}
data-sidebar="menu-item"
data-slot="sidebar-menu-item"
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-lg p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0",
{
defaultVariants: {
size: "default",
variant: "default",
},
variants: {
size: {
default: "h-8 text-sm",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
sm: "h-7 text-xs",
},
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
},
},
);
function SidebarMenuButton({
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
render,
...props
}: useRender.ComponentProps<"button"> & {
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipPopup>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const { isMobile, state } = useSidebar();
const defaultProps = {
className: cn(sidebarMenuButtonVariants({ size, variant }), className),
"data-active": isActive,
"data-sidebar": "menu-button",
"data-size": size,
"data-slot": "sidebar-menu-button",
};
const buttonProps = mergeProps<"button">(defaultProps, props);
const buttonElement = useRender({
defaultTagName: "button",
props: buttonProps,
render,
});
if (!tooltip) {
return buttonElement;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger
render={buttonElement as React.ReactElement<Record<string, unknown>>}
/>
<TooltipPopup
align="center"
hidden={state !== "collapsed" || isMobile}
side="right"
{...tooltip}
/>
</Tooltip>
);
}
function SidebarMenuAction({
className,
showOnHover = false,
render,
...props
}: useRender.ComponentProps<"button"> & {
showOnHover?: boolean;
}) {
const defaultProps = {
className: cn(
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-lg p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:-inset-2 after:absolute md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
),
"data-sidebar": "menu-action",
"data-slot": "sidebar-menu-action",
};
return useRender({
defaultTagName: "button",
props: mergeProps<"button">(defaultProps, props),
render,
});
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-lg px-1 font-medium text-sidebar-foreground text-xs tabular-nums",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
data-sidebar="menu-badge"
data-slot="sidebar-menu-badge"
{...props}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
className={cn("flex h-8 items-center gap-2 rounded-lg px-2", className)}
data-sidebar="menu-skeleton"
data-slot="sidebar-menu-skeleton"
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-lg"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
data-sidebar="menu-sub"
data-slot="sidebar-menu-sub"
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
className={cn("group/menu-sub-item relative", className)}
data-sidebar="menu-sub-item"
data-slot="sidebar-menu-sub-item"
{...props}
/>
);
}
function SidebarMenuSubButton({
size = "md",
isActive = false,
className,
render,
...props
}: useRender.ComponentProps<"a"> & {
size?: "sm" | "md";
isActive?: boolean;
}) {
const defaultProps = {
className: cn(
"-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-lg px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
),
"data-active": isActive,
"data-sidebar": "menu-sub-button",
"data-size": size,
"data-slot": "sidebar-menu-sub-button",
};
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(defaultProps, props),
render,
});
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};
@@ -0,0 +1,16 @@
import { cn } from "@coss/ui/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"animate-skeleton rounded-sm [--skeleton-highlight:--alpha(var(--color-white)/64%)] [background:linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)_var(--color-muted)_0_0/200%_100%_fixed] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",
className,
)}
data-slot="skeleton"
{...props}
/>
);
}
export { Skeleton };
@@ -0,0 +1,76 @@
"use client";
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
import * as React from "react";
import { cn } from "@coss/ui/lib/utils";
function Slider({
className,
children,
defaultValue,
value,
min = 0,
max = 100,
...props
}: SliderPrimitive.Root.Props) {
const _values = React.useMemo(() => {
if (value !== undefined) {
return Array.isArray(value) ? value : [value];
}
if (defaultValue !== undefined) {
return Array.isArray(defaultValue) ? defaultValue : [defaultValue];
}
return [min];
}, [value, defaultValue, min]);
return (
<SliderPrimitive.Root
className="data-[orientation=horizontal]:w-full"
defaultValue={defaultValue}
max={max}
min={min}
thumbAlignment="edge"
value={value}
{...props}
>
{children}
<SliderPrimitive.Control
className={cn(
"flex touch-none select-none data-disabled:pointer-events-none data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:min-w-44 data-[orientation=vertical]:flex-col data-disabled:opacity-64",
className,
)}
data-slot="slider-control"
>
<SliderPrimitive.Track
className="relative grow select-none before:absolute before:rounded-full before:bg-input data-[orientation=horizontal]:h-1 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1 data-[orientation=horizontal]:before:inset-x-0.5 data-[orientation=vertical]:before:inset-x-0 data-[orientation=horizontal]:before:inset-y-0 data-[orientation=vertical]:before:inset-y-0.5"
data-slot="slider-track"
>
<SliderPrimitive.Indicator
className="select-none rounded-full bg-primary data-[orientation=horizontal]:ms-0.5 data-[orientation=vertical]:mb-0.5"
data-slot="slider-indicator"
/>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
className="block size-5 shrink-0 select-none rounded-full border border-input bg-white bg-clip-padding shadow-xs outline-none transition-shadow before:absolute before:inset-0 before:rounded-full before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-[3px] focus-visible:ring-ring/24 has-focus-visible:ring-[3px] has-focus-visible:ring-ring/24 data-dragging:ring-[3px] data-dragging:ring-ring/24 sm:size-4 dark:border-background dark:bg-clip-border dark:data-dragging:ring-ring/48 dark:focus-visible:ring-ring/48 [:focus-visible,[data-dragging]]:shadow-none"
data-slot="slider-thumb"
key={String(index)}
/>
))}
</SliderPrimitive.Track>
</SliderPrimitive.Control>
</SliderPrimitive.Root>
);
}
function SliderValue({ className, ...props }: SliderPrimitive.Value.Props) {
return (
<SliderPrimitive.Value
className={cn("flex justify-end text-sm", className)}
data-slot="slider-value"
{...props}
/>
);
}
export { Slider, SliderValue };
@@ -0,0 +1,16 @@
import { Loader2Icon } from "lucide-react";
import { cn } from "@coss/ui/lib/utils";
function Spinner({ className, ...props }: React.ComponentProps<typeof Loader2Icon>) {
return (
<Loader2Icon
aria-label="Loading"
className={cn("animate-spin", className)}
role="status"
{...props}
/>
);
}
export { Spinner };
@@ -0,0 +1,27 @@
"use client";
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
import { cn } from "@coss/ui/lib/utils";
function Switch({ className, ...props }: SwitchPrimitive.Root.Props) {
return (
<SwitchPrimitive.Root
className={cn(
"group/switch inset-shadow-[0_1px_--theme(--color-black/4%)] inline-flex h-5.5 w-9.5 shrink-0 items-center rounded-full p-px outline-none transition-all focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:h-4.5 sm:w-7.5",
className,
)}
data-slot="switch"
{...props}
>
<SwitchPrimitive.Thumb
className={cn(
"pointer-events-none block size-5 rounded-full bg-background shadow-sm transition-[translate,width] group-active/switch:not-data-disabled:w-5.5 data-checked:translate-x-4 data-unchecked:translate-x-0 data-checked:group-active/switch:translate-x-3.5 sm:size-4 sm:data-checked:translate-x-3 sm:group-active/switch:not-data-disabled:w-4.5 sm:data-checked:group-active/switch:translate-x-2.5",
)}
data-slot="switch-thumb"
/>
</SwitchPrimitive.Root>
);
}
export { Switch };
+126
View File
@@ -0,0 +1,126 @@
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
className="relative w-full overflow-x-auto"
data-slot="table-container"
>
<table
className={cn(
"w-full caption-bottom in-data-[slot=frame]:border-separate in-data-[slot=frame]:border-spacing-0 text-sm",
className,
)}
data-slot="table"
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
className={cn(
"[&_tr]:border-b in-data-[slot=frame]:**:[th]:h-9 in-data-[slot=frame]:*:[tr]:border-none in-data-[slot=frame]:*:[tr]:hover:bg-transparent",
className,
)}
data-slot="table-header"
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
className={cn(
"relative in-data-[slot=frame]:rounded-xl in-data-[slot=frame]:shadow-xs before:pointer-events-none before:absolute before:inset-px not-in-data-[slot=frame]:before:hidden before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_tr:last-child]:border-0 in-data-[slot=frame]:*:[tr]:border-0 in-data-[slot=frame]:*:[tr]:*:[td]:border-b in-data-[slot=frame]:*:[tr]:*:[td]:bg-background in-data-[slot=frame]:*:[tr]:*:[td]:bg-clip-padding in-data-[slot=frame]:*:[tr]:first:*:[td]:first:rounded-ss-xl in-data-[slot=frame]:*:[tr]:*:[td]:first:border-s in-data-[slot=frame]:*:[tr]:first:*:[td]:border-t in-data-[slot=frame]:*:[tr]:last:*:[td]:last:rounded-ee-xl in-data-[slot=frame]:*:[tr]:*:[td]:last:border-e in-data-[slot=frame]:*:[tr]:first:*:[td]:last:rounded-se-xl in-data-[slot=frame]:*:[tr]:last:*:[td]:first:rounded-es-xl in-data-[slot=frame]:*:[tr]:hover:*:[td]:bg-transparent in-data-[slot=frame]:*:[tr]:data-[state=selected]:*:[td]:bg-muted/50",
className,
)}
data-slot="table-body"
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
className={cn(
"border-t in-data-[slot=frame]:border-none bg-muted/50 in-data-[slot=frame]:bg-transparent font-medium [&>tr]:last:border-b-0 in-data-[slot=frame]:*:[tr]:hover:bg-transparent",
className,
)}
data-slot="table-footer"
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
className={cn(
"border-b transition-colors hover:bg-muted/50 in-data-[slot=frame]:hover:bg-transparent data-[state=selected]:bg-muted/50 in-data-[slot=frame]:data-[state=selected]:bg-transparent",
className,
)}
data-slot="table-row"
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
className={cn(
"h-10 whitespace-nowrap px-2 text-left align-middle font-medium text-muted-foreground has-[[role=checkbox]]:w-px has-[[role=checkbox]]:pe-0 *:[[role=checkbox]]:translate-y-0.5",
className,
)}
data-slot="table-head"
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
className={cn(
"whitespace-nowrap p-2 align-middle has-[[role=checkbox]]:pe-0 *:[[role=checkbox]]:translate-y-0.5",
className,
)}
data-slot="table-cell"
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
className={cn(
"in-data-[slot=frame]:my-4 mt-4 text-muted-foreground text-sm",
className,
)}
data-slot="table-caption"
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
+90
View File
@@ -0,0 +1,90 @@
"use client";
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { cn } from "@coss/ui/lib/utils";
type TabsVariant = "default" | "underline";
function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
className={cn(
"flex flex-col gap-2 data-[orientation=vertical]:flex-row",
className,
)}
data-slot="tabs"
{...props}
/>
);
}
function TabsList({
variant = "default",
className,
children,
...props
}: TabsPrimitive.List.Props & {
variant?: TabsVariant;
}) {
return (
<TabsPrimitive.List
className={cn(
"relative z-0 flex w-fit items-center justify-center gap-x-0.5 text-muted-foreground",
"data-[orientation=vertical]:flex-col",
variant === "default"
? "rounded-lg bg-muted p-0.5 text-muted-foreground/72"
: "data-[orientation=vertical]:px-1 data-[orientation=horizontal]:py-1 *:data-[slot=tabs-trigger]:hover:bg-accent",
className,
)}
data-slot="tabs-list"
{...props}
>
{children}
<TabsPrimitive.Indicator
className={cn(
"-translate-y-(--active-tab-bottom) absolute bottom-0 left-0 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) transition-[width,translate] duration-200 ease-in-out",
variant === "underline"
? "data-[orientation=vertical]:-translate-x-px z-10 bg-primary data-[orientation=horizontal]:h-0.5 data-[orientation=vertical]:w-0.5 data-[orientation=horizontal]:translate-y-px"
: "-z-1 rounded-md bg-background shadow-sm dark:bg-accent",
)}
data-slot="tab-indicator"
/>
</TabsPrimitive.List>
);
}
function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) {
return (
<TabsPrimitive.Tab
className={cn(
"[&_svg]:-mx-0.5 flex shrink-0 grow cursor-pointer items-center justify-center whitespace-nowrap rounded-md border border-transparent font-medium text-base outline-none transition-[color,background-color,box-shadow] focus-visible:ring-2 focus-visible:ring-ring data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"hover:text-muted-foreground data-active:text-foreground",
"h-9 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-8",
"data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start",
className,
)}
data-slot="tabs-trigger"
{...props}
/>
);
}
function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props) {
return (
<TabsPrimitive.Panel
className={cn("flex-1 outline-none", className)}
data-slot="tabs-content"
{...props}
/>
);
}
export {
Tabs,
TabsList,
TabsTab,
TabsTab as TabsTrigger,
TabsPanel,
TabsPanel as TabsContent,
};
@@ -0,0 +1,51 @@
"use client";
import { Field as FieldPrimitive } from "@base-ui/react/field";
import { mergeProps } from "@base-ui/react/merge-props";
import type * as React from "react";
import { cn } from "@coss/ui/lib/utils";
type TextareaProps = React.ComponentProps<"textarea"> & {
size?: "sm" | "default" | "lg" | number;
unstyled?: boolean;
};
function Textarea({
className,
size = "default",
unstyled = false,
...props
}: TextareaProps) {
return (
<span
className={
cn(
!unstyled &&
"relative inline-flex w-full rounded-lg border border-input bg-background bg-clip-padding text-base shadow-xs ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] sm:text-sm dark:bg-input/32 dark:bg-clip-border dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
) || undefined
}
data-size={size}
data-slot="textarea-control"
>
<FieldPrimitive.Control
render={(defaultProps) => (
<textarea
className={cn(
"field-sizing-content min-h-17.5 w-full rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none max-sm:min-h-20.5",
size === "sm" &&
"min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",
size === "lg" &&
"min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
)}
data-slot="textarea"
{...mergeProps(defaultProps, props)}
/>
)}
/>
</span>
);
}
export { Textarea, type TextareaProps };
+269
View File
@@ -0,0 +1,269 @@
"use client";
import { Toast } from "@base-ui/react/toast";
import {
CircleAlertIcon,
CircleCheckIcon,
InfoIcon,
LoaderCircleIcon,
TriangleAlertIcon,
} from "lucide-react";
import { cn } from "@coss/ui/lib/utils";
import { buttonVariants } from "@coss/ui/components/button";
const toastManager = Toast.createToastManager();
const anchoredToastManager = Toast.createToastManager();
const TOAST_ICONS = {
error: CircleAlertIcon,
info: InfoIcon,
loading: LoaderCircleIcon,
success: CircleCheckIcon,
warning: TriangleAlertIcon,
} as const;
type ToastPosition =
| "top-left"
| "top-center"
| "top-right"
| "bottom-left"
| "bottom-center"
| "bottom-right";
interface ToastProviderProps extends Toast.Provider.Props {
position?: ToastPosition;
}
function ToastProvider({
children,
position = "bottom-right",
...props
}: ToastProviderProps) {
return (
<Toast.Provider toastManager={toastManager} {...props}>
{children}
<Toasts position={position} />
</Toast.Provider>
);
}
function Toasts({ position = "bottom-right" }: { position: ToastPosition }) {
const { toasts } = Toast.useToastManager();
const isTop = position.startsWith("top");
return (
<Toast.Portal data-slot="toast-portal">
<Toast.Viewport
className={cn(
"fixed z-50 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]",
// Vertical positioning
"data-[position*=top]:top-(--toast-inset)",
"data-[position*=bottom]:bottom-(--toast-inset)",
// Horizontal positioning
"data-[position*=left]:left-(--toast-inset)",
"data-[position*=right]:right-(--toast-inset)",
"data-[position*=center]:-translate-x-1/2 data-[position*=center]:left-1/2",
)}
data-position={position}
data-slot="toast-viewport"
>
{toasts.map((toast) => {
const Icon = toast.type
? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
: null;
return (
<Toast.Root
className={cn(
"absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-popover bg-clip-padding text-popover-foreground shadow-lg [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
// Base positioning using data-position
"data-[position*=right]:right-0 data-[position*=right]:left-auto",
"data-[position*=left]:right-auto data-[position*=left]:left-0",
"data-[position*=center]:right-0 data-[position*=center]:left-0",
"data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
"data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
// Gap fill for hover
"after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
"data-[position*=top]:after:top-full",
"data-[position*=bottom]:after:bottom-full",
// Define some variables
"[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
// Define offset-y variable
"data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
"data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
// Default state transform
"data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
"data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
// Limited state
"data-limited:opacity-0",
// Expanded state
"data-expanded:h-(--toast-height)",
"data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
// Starting and ending animations
"data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
"data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
"data-ending-style:opacity-0",
// Ending animations (direction-aware)
"data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]",
"data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
"data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
// Ending animations (expanded)
"data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
"data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
)}
data-position={position}
key={toast.id}
swipeDirection={
position.includes("center")
? [isTop ? "up" : "down"]
: position.includes("left")
? ["left", isTop ? "up" : "down"]
: ["right", isTop ? "up" : "down"]
}
toast={toast}
>
<Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100">
<div className="flex gap-2">
{Icon && (
<div
className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
data-slot="toast-icon"
>
<Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
</div>
)}
<div className="flex flex-col gap-0.5">
<Toast.Title
className="font-medium"
data-slot="toast-title"
/>
<Toast.Description
className="text-muted-foreground"
data-slot="toast-description"
/>
</div>
</div>
{toast.actionProps && (
<Toast.Action
className={buttonVariants({ size: "xs" })}
data-slot="toast-action"
>
{toast.actionProps.children}
</Toast.Action>
)}
</Toast.Content>
</Toast.Root>
);
})}
</Toast.Viewport>
</Toast.Portal>
);
}
function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
return (
<Toast.Provider toastManager={anchoredToastManager} {...props}>
{children}
<AnchoredToasts />
</Toast.Provider>
);
}
function AnchoredToasts() {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal data-slot="toast-portal-anchored">
<Toast.Viewport
className="outline-none"
data-slot="toast-viewport-anchored"
>
{toasts.map((toast) => {
const Icon = toast.type
? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
: null;
const tooltipStyle =
(toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
const positionerProps = toast.positionerProps;
if (!positionerProps?.anchor) {
return null;
}
return (
<Toast.Positioner
className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
data-slot="toast-positioner"
key={toast.id}
sideOffset={positionerProps.sideOffset ?? 4}
toast={toast}
>
<Toast.Root
className={cn(
"relative text-balance border bg-popover bg-clip-padding text-popover-foreground text-xs transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
tooltipStyle
? "rounded-md shadow-black/5 shadow-md before:rounded-[calc(var(--radius-md)-1px)]"
: "rounded-lg shadow-lg before:rounded-[calc(var(--radius-lg)-1px)]",
)}
data-slot="toast-popup"
toast={toast}
>
{tooltipStyle ? (
<Toast.Content className="pointer-events-auto px-2 py-1">
<Toast.Title data-slot="toast-title" />
</Toast.Content>
) : (
<Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
<div className="flex gap-2">
{Icon && (
<div
className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
data-slot="toast-icon"
>
<Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
</div>
)}
<div className="flex flex-col gap-0.5">
<Toast.Title
className="font-medium"
data-slot="toast-title"
/>
<Toast.Description
className="text-muted-foreground"
data-slot="toast-description"
/>
</div>
</div>
{toast.actionProps && (
<Toast.Action
className={buttonVariants({ size: "xs" })}
data-slot="toast-action"
>
{toast.actionProps.children}
</Toast.Action>
)}
</Toast.Content>
)}
</Toast.Root>
</Toast.Positioner>
);
})}
</Toast.Viewport>
</Toast.Portal>
);
}
export {
ToastProvider,
type ToastPosition,
toastManager,
AnchoredToastProvider,
anchoredToastManager,
};
@@ -0,0 +1,95 @@
"use client";
import type { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
import type { VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@coss/ui/lib/utils";
import { Separator } from "@coss/ui/components/separator";
import {
Toggle as ToggleComponent,
type toggleVariants,
} from "@coss/ui/components/toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
function ToggleGroup({
className,
variant = "default",
size = "default",
orientation = "horizontal",
children,
...props
}: ToggleGroupPrimitive.Props & VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive
className={cn(
"flex w-fit *:focus-visible:z-10",
orientation === "horizontal"
? "*:pointer-coarse:after:min-w-auto"
: "*:pointer-coarse:after:min-h-auto",
variant === "default"
? "gap-0.5"
: orientation === "horizontal"
? "*:not-first:before:-start-[0.5px] *:not-last:before:-end-[0.5px] *:not-first:rounded-s-none *:not-last:rounded-e-none *:not-first:border-s-0 *:not-last:border-e-0 *:not-first:before:rounded-s-none *:not-last:before:rounded-e-none"
: "*:not-first:before:-top-[0.5px] *:not-last:before:-bottom-[0.5px] flex-col *:not-first:rounded-t-none *:not-last:rounded-b-none *:not-first:border-t-0 *:not-last:border-b-0 *:not-last:before:hidden *:not-first:before:rounded-t-none *:not-last:before:rounded-b-none dark:*:last:before:hidden dark:*:first:before:block",
className,
)}
data-size={size}
data-slot="toggle-group"
data-variant={variant}
orientation={orientation}
{...props}
>
<ToggleGroupContext.Provider value={{ size, variant }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive>
);
}
function Toggle({
className,
children,
variant,
size,
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
const resolvedVariant = context.variant || variant;
const resolvedSize = context.size || size;
return (
<ToggleComponent
className={className}
data-size={resolvedSize}
data-variant={resolvedVariant}
size={resolvedSize}
variant={resolvedVariant}
{...props}
>
{children}
</ToggleComponent>
);
}
function ToggleGroupSeparator({
className,
orientation = "vertical",
...props
}: {
className?: string;
} & React.ComponentProps<typeof Separator>) {
return (
<Separator className={className} orientation={orientation} {...props} />
);
}
export { ToggleGroup, Toggle, Toggle as ToggleGroupItem, ToggleGroupSeparator };
@@ -0,0 +1,45 @@
"use client";
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@coss/ui/lib/utils";
const toggleVariants = cva(
"[&_svg]:-mx-0.5 relative inline-flex shrink-0 cursor-pointer select-none 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 hover:bg-accent/50 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-pressed:bg-accent data-pressed:text-accent-foreground data-pressed:transition-none sm:text-sm dark:data-pressed:bg-input/80 dark:hover:bg-accent [&_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]:shrink-0",
{
defaultVariants: {
size: "default",
variant: "default",
},
variants: {
size: {
default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8",
lg: "h-10 min-w-10 px-[calc(--spacing(2.5)-1px)] sm:h-9 sm:min-w-9",
sm: "h-8 min-w-8 px-[calc(--spacing(1.5)-1px)] sm:h-7 sm:min-w-7",
},
variant: {
default: "border-transparent",
outline:
"border-border bg-clip-padding shadow-xs not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:hover:bg-input/64 dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/8%)] dark:not-disabled:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/4%)] [:disabled,:active,[data-pressed]]:shadow-none",
},
},
},
);
function Toggle({
className,
variant,
size,
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive
className={cn(toggleVariants({ className, size, variant }))}
data-slot="toggle"
{...props}
/>
);
}
export { Toggle, toggleVariants };
@@ -0,0 +1,83 @@
"use client";
import { Toolbar as ToolbarPrimitive } from "@base-ui/react/toolbar";
import { cn } from "@coss/ui/lib/utils";
function Toolbar({ className, ...props }: ToolbarPrimitive.Root.Props) {
return (
<ToolbarPrimitive.Root
className={cn(
"relative flex gap-2 rounded-xl border bg-card bg-clip-padding p-1 text-card-foreground",
className,
)}
data-slot="toolbar"
{...props}
/>
);
}
function ToolbarButton({ className, ...props }: ToolbarPrimitive.Button.Props) {
return (
<ToolbarPrimitive.Button
className={cn(className)}
data-slot="toolbar-button"
{...props}
/>
);
}
function ToolbarLink({ className, ...props }: ToolbarPrimitive.Link.Props) {
return (
<ToolbarPrimitive.Link
className={cn(className)}
data-slot="toolbar-link"
{...props}
/>
);
}
function ToolbarInput({ className, ...props }: ToolbarPrimitive.Input.Props) {
return (
<ToolbarPrimitive.Input
className={cn(className)}
data-slot="toolbar-input"
{...props}
/>
);
}
function ToolbarGroup({ className, ...props }: ToolbarPrimitive.Group.Props) {
return (
<ToolbarPrimitive.Group
className={cn("flex items-center gap-1", className)}
data-slot="toolbar-group"
{...props}
/>
);
}
function ToolbarSeparator({
className,
...props
}: ToolbarPrimitive.Separator.Props) {
return (
<ToolbarPrimitive.Separator
className={cn(
"shrink-0 bg-border data-[orientation=horizontal]:my-0.5 data-[orientation=vertical]:my-1.5 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:not-[[class^='h-']]:not-[[class*='_h-']]:self-stretch",
className,
)}
data-slot="toolbar-separator"
{...props}
/>
);
}
export {
Toolbar,
ToolbarGroup,
ToolbarSeparator,
ToolbarButton,
ToolbarLink,
ToolbarInput,
};
@@ -0,0 +1,65 @@
"use client";
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { cn } from "@coss/ui/lib/utils";
const TooltipCreateHandle = TooltipPrimitive.createHandle;
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipPopup({
className,
align = "center",
sideOffset = 4,
side = "top",
children,
...props
}: TooltipPrimitive.Popup.Props & {
align?: TooltipPrimitive.Positioner.Props["align"];
side?: TooltipPrimitive.Positioner.Props["side"];
sideOffset?: TooltipPrimitive.Positioner.Props["sideOffset"];
}) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"
data-slot="tooltip-positioner"
side={side}
sideOffset={sideOffset}
>
<TooltipPrimitive.Popup
className={cn(
"relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) text-balance rounded-md border bg-popover bg-clip-padding text-popover-foreground text-xs shadow-black/5 shadow-md transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 data-instant:duration-0 dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
className,
)}
data-slot="tooltip-popup"
{...props}
>
<TooltipPrimitive.Viewport
className="relative size-full overflow-clip px-(--viewport-inline-padding) py-1 [--viewport-inline-padding:--spacing(2)] data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:truncate **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity"
data-slot="tooltip-viewport"
>
{children}
</TooltipPrimitive.Viewport>
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
);
}
export {
TooltipCreateHandle,
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipPopup,
TooltipPopup as TooltipContent,
};
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+179
View File
@@ -0,0 +1,179 @@
@custom-variant dark (&:is(.dark *));
@custom-variant fixed (&:is(.layout-fixed *));
@theme inline {
--breakpoint-3xl: 1600px;
--breakpoint-4xl: 2000px;
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-heading: var(--font-heading);
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-code: var(--code);
--color-code-foreground: var(--code-foreground);
--color-code-highlight: var(--code-highlight);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-skeleton: skeleton 2s -1s infinite linear;
@keyframes skeleton {
to {
background-position: -200% 0;
}
}
}
:root {
--radius: 0.625rem;
--background: var(--color-white);
--foreground: var(--color-zinc-800);
--card: var(--color-white);
--card-foreground: var(--color-zinc-800);
--popover: var(--color-white);
--popover-foreground: var(--color-zinc-800);
--primary: var(--color-zinc-800);
--primary-foreground: var(--color-zinc-50);
--secondary: --alpha(var(--color-black) / 4%);
--secondary-foreground: var(--color-zinc-800);
--muted: --alpha(var(--color-black) / 4%);
--muted-foreground: color-mix(
in srgb,
var(--color-zinc-500) 90%,
var(--color-black)
);
--accent: --alpha(var(--color-black) / 4%);
--accent-foreground: var(--color-zinc-800);
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-red-700);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-700);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-700);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-700);
--border: --alpha(var(--color-black) / 12%);
--input: --alpha(var(--color-black) / 12%);
--ring: var(--color-zinc-400);
--chart-1: var(--color-orange-600);
--chart-2: var(--color-teal-600);
--chart-3: var(--color-cyan-900);
--chart-4: var(--color-amber-400);
--chart-5: var(--color-amber-500);
--sidebar: var(--color-zinc-50);
--sidebar-foreground: color-mix(
in srgb,
var(--color-zinc-800) 64%,
var(--sidebar)
);
--sidebar-primary: var(--color-zinc-800);
--sidebar-primary-foreground: var(--color-zinc-50);
--sidebar-accent: --alpha(var(--color-black) / 4%);
--sidebar-accent-foreground: var(--color-zinc-800);
--sidebar-border: --alpha(var(--color-black) / 12%);
--sidebar-ring: var(--color-zinc-400);
--code: var(--color-white);
--code-foreground: var(--foreground);
--code-highlight: --alpha(var(--color-black) / 4%);
}
.dark {
--background: color-mix(
in srgb,
var(--color-zinc-950) 98%,
var(--color-white)
);
--foreground: var(--color-zinc-200);
--card: color-mix(in srgb, var(--background) 98%, var(--color-white));
--card-foreground: var(--color-zinc-200);
--popover: color-mix(in srgb, var(--background) 96%, var(--color-white));
--popover-foreground: var(--color-zinc-200);
--primary: var(--color-zinc-200);
--primary-foreground: var(--color-zinc-800);
--secondary: --alpha(var(--color-white) / 6%);
--secondary-foreground: var(--color-zinc-200);
--muted: --alpha(var(--color-white) / 6%);
--muted-foreground: color-mix(
in srgb,
var(--color-zinc-500) 90%,
var(--color-white)
);
--accent: --alpha(var(--color-white) / 6%);
--accent-foreground: var(--color-zinc-200);
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-red-400);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-400);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-400);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-400);
--border: --alpha(var(--color-white) / 12%);
--input: --alpha(var(--color-white) / 12%);
--ring: var(--color-zinc-500);
--chart-1: var(--color-blue-700);
--chart-2: var(--color-emerald-500);
--chart-3: var(--color-amber-500);
--chart-4: var(--color-purple-500);
--chart-5: var(--color-rose-500);
--sidebar: var(--color-zinc-950);
--sidebar-foreground: color-mix(
in srgb,
var(--color-zinc-200) 64%,
var(--sidebar)
);
--sidebar-primary: var(--color-zinc-200);
--sidebar-primary-foreground: var(--color-zinc-800);
--sidebar-accent: --alpha(var(--color-white) / 6%);
--sidebar-accent-foreground: var(--color-zinc-200);
--sidebar-border: --alpha(var(--color-white) / 12%);
--sidebar-ring: var(--color-zinc-400);
--code: color-mix(in srgb, var(--background) 96%, var(--color-white));
--code-foreground: var(--foreground);
--code-highlight: --alpha(var(--color-white) / 6%);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
}
-156
View File
@@ -1,156 +0,0 @@
@source "./components/**/*.tsx";
@source "./lib/**/*.ts";
@custom-variant dark (&:is(.dark *));
@custom-variant fixed (&:is(.layout-fixed *));
@theme inline {
--breakpoint-3xl: 1600px;
--breakpoint-4xl: 2000px;
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-heading: var(--font-heading);
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-code: var(--code);
--color-code-foreground: var(--code-foreground);
--color-code-highlight: var(--code-highlight);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.21 0.006 285.885);
--card: oklch(1 0 0);
--card-foreground: oklch(0.21 0.006 285.885);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.21 0.006 285.885);
--primary: oklch(0.274 0.006 286.033);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0 0 0 / 4%);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0 0 0 / 4%);
--muted-foreground: oklch(0.442 0.017 285.786);
--accent: oklch(0 0 0 / 4%);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.637 0.237 25.331);
--destructive-foreground: oklch(0.505 0.213 27.518);
--info: oklch(0.623 0.214 259.815);
--info-foreground: oklch(0.488 0.243 264.376);
--success: oklch(0.696 0.17 162.48);
--success-foreground: oklch(0.508 0.118 165.612);
--warning: oklch(0.769 0.188 70.08);
--warning-foreground: oklch(0.555 0.163 48.998);
--border: oklch(0 0 0 / 10%);
--input: oklch(0 0 0 / 10%);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.21 0.006 285.885);
--sidebar-primary: oklch(0.274 0.006 286.033);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0 0 0 / 4%);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0 0 0 / 10%);
--sidebar-ring: oklch(0.705 0.015 286.067);
--code: oklch(1 0 0);
--code-foreground: var(--foreground);
--code-highlight: oklch(0 0 0 / 4%);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.967 0.001 286.375);
--card: color-mix(in srgb, oklch(0.21 0.006 285.885) 80%, oklch(0.141 0.005 285.823));
--card-foreground: oklch(0.967 0.001 286.375);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.967 0.001 286.375);
--primary: oklch(0.967 0.001 286.375);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(1 0 0 / 8%);
--secondary-foreground: oklch(0.967 0.001 286.375);
--muted: oklch(1 0 0 / 8%);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(1 0 0 / 8%);
--accent-foreground: oklch(0.967 0.001 286.375);
--destructive: oklch(0.637 0.237 25.331);
--destructive-foreground: oklch(0.704 0.191 22.216);
--info: oklch(0.623 0.214 259.815);
--info-foreground: oklch(0.707 0.165 254.624);
--success: oklch(0.696 0.17 162.48);
--success-foreground: oklch(0.765 0.177 163.223);
--warning: oklch(0.769 0.188 70.08);
--warning-foreground: oklch(0.828 0.189 84.429);
--border: oklch(1 0 0 / 12%);
--input: oklch(1 0 0 / 12%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: color-mix(in srgb, oklch(0.21 0.006 285.885) 32%, oklch(0.141 0.005 285.823));
--sidebar-foreground: oklch(0.967 0.001 286.375);
--sidebar-primary: oklch(0.967 0.001 286.375);
--sidebar-primary-foreground: oklch(0.21 0.006 285.885);
--sidebar-accent: oklch(1 0 0 / 8%);
--sidebar-accent-foreground: oklch(0.967 0.001 286.375);
--sidebar-border: oklch(1 0 0 / 12%);
--sidebar-ring: oklch(0.705 0.015 286.067);
--code: color-mix(in srgb, oklch(0.21 0.006 285.885) 80%, oklch(0.141 0.005 285.823));
--code-foreground: var(--foreground);
--code-highlight: oklch(1 0 0 / 8%);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "@calcom/tsconfig/react-library.json",
"compilerOptions": {
"resolveJsonModule": true,
"experimentalDecorators": true,
"baseUrl": "./src",
"paths": {
"@coss/ui/*": ["./*"]
}
},
"include": ["src/**/*"],
"exclude": ["dist", "build", "**/node_modules/**"]
}
+2
View File
@@ -1,6 +1,8 @@
{
"extends": "@calcom/tsconfig/react-library.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"~/*": ["/*"]
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env node
/**
* Script to pull coss ui components from the registry
* and write them to packages/coss-ui/src/components/
* while rewriting imports to use @coss/ui paths.
*/
import { promises as fs } from 'node:fs';
import https from 'node:https';
import path from 'node:path';
const REGISTRY_BASE_URL = 'https://coss.com/ui/r';
const UI_JSON_URL = `${REGISTRY_BASE_URL}/ui.json`;
// Security limits
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
const REQUEST_TIMEOUT = 30000; // 30 seconds
const MAX_FILE_NAME_LENGTH = 255;
const VALID_COMPONENT_NAME_REGEX = /^[a-z0-9-]+$/i;
type ComponentFile = {
type?: string;
path: string;
content: string;
};
type ComponentJson = {
files?: ComponentFile[];
registryDependencies?: string[];
};
type TargetDirs = {
components: string;
};
async function resolvePaths(): Promise<{ targetDirs: TargetDirs; targetRoot: string }> {
const cwd = process.cwd();
// Check if we're running from within packages/coss-ui or from monorepo root
const isInPackage = cwd.endsWith('packages/coss-ui') || cwd.endsWith('packages\\coss-ui');
const targetRoot = isInPackage
? path.resolve(cwd, 'src')
: path.resolve(cwd, 'packages/coss-ui/src');
const targetDirs: TargetDirs = {
components: path.join(targetRoot, 'components'),
};
await fs.mkdir(targetDirs.components, { recursive: true });
return { targetDirs, targetRoot };
}
async function fetchJSON<T>(url: string): Promise<T> {
return new Promise((resolve, reject) => {
const req = https.get(url, (res) => {
// Validate HTTP status code
if (res.statusCode !== 200) {
clearTimeout(timeout);
req.destroy();
reject(new Error(`HTTP ${res.statusCode} error fetching ${url}`));
return;
}
// Validate content type
const contentType = res.headers['content-type'] || '';
if (!contentType.includes('application/json')) {
console.warn(`⚠️ Unexpected content-type: ${contentType} for ${url}`);
}
let data = '';
let size = 0;
res.on('data', (chunk: Buffer | string) => {
size += chunk.length;
if (size > MAX_RESPONSE_SIZE) {
clearTimeout(timeout);
req.destroy();
reject(new Error(`Response too large (max ${MAX_RESPONSE_SIZE} bytes) for ${url}`));
return;
}
data += chunk.toString();
});
res.on('end', () => {
clearTimeout(timeout);
try {
const json = JSON.parse(data) as T;
resolve(json);
} catch (error) {
reject(new Error(`Failed to parse JSON from ${url}: ${(error as Error).message}`));
}
});
})
.on('error', (error: Error) => {
clearTimeout(timeout);
reject(new Error(`Failed to fetch ${url}: ${error.message}`));
});
const timeout = setTimeout(() => {
req.destroy();
reject(new Error(`Request timeout after ${REQUEST_TIMEOUT}ms for ${url}`));
}, REQUEST_TIMEOUT);
});
}
function validateComponentName(name: string): boolean {
if (!name || name.length === 0 || name.length > MAX_FILE_NAME_LENGTH) {
return false;
}
return VALID_COMPONENT_NAME_REGEX.test(name);
}
function getComponentName(registryDependency: string): string {
if (!registryDependency.startsWith('@coss/')) {
throw new Error(`Invalid registry dependency format: ${registryDependency}`);
}
const componentName = registryDependency.replace('@coss/', '');
if (!validateComponentName(componentName)) {
throw new Error(`Invalid component name: ${componentName}`);
}
return componentName;
}
function getFileName(filePath: string): string {
const fileName = path.basename(filePath);
// Additional validation
if (!fileName || fileName.length === 0 || fileName.length > MAX_FILE_NAME_LENGTH) {
throw new Error(`Invalid file name: ${fileName}`);
}
// Ensure it's a .tsx file
if (!fileName.endsWith('.tsx')) {
throw new Error(`File must be a .tsx file: ${fileName}`);
}
// Prevent path traversal attempts
if (fileName.includes('..') || fileName.includes('/') || fileName.includes('\\')) {
throw new Error(`Invalid file name (path traversal detected): ${fileName}`);
}
return fileName;
}
function validateContent(content: string): void {
if (!content || typeof content !== 'string') {
throw new Error('Content must be a non-empty string');
}
if (content.length > MAX_RESPONSE_SIZE) {
throw new Error(`Content too large (max ${MAX_RESPONSE_SIZE} bytes)`);
}
}
function rewriteImports(code: string): string {
let result = code;
// Align imports with @coss/ui paths
result = result.replace(/(['"])@\/lib\//g, "$1@coss/ui/lib/");
result = result.replace(/(['"])@\/hooks\//g, "$1@coss/ui/hooks/");
result = result.replace(/(['"])@\/registry\/default\/ui\//g, "$1@coss/ui/components/");
result = result.replace(/(['"])@\/registry\/default\/hooks\//g, "$1@coss/ui/hooks/");
result = result.replace(/(['"])@\/registry\/default\/lib\//g, "$1@coss/ui/lib/");
return result;
}
async function processComponent(registryDependency: string, targetDirs: TargetDirs): Promise<boolean> {
let componentName: string;
try {
componentName = getComponentName(registryDependency);
} catch (error) {
console.error(`❌ Invalid component dependency: ${registryDependency}`, (error as Error).message);
return false;
}
const componentUrl = `${REGISTRY_BASE_URL}/${componentName}.json`;
console.log(`Pulling ${componentName}...`);
try {
const componentJson = await fetchJSON<ComponentJson>(componentUrl);
// Validate JSON structure
if (!componentJson || typeof componentJson !== 'object') {
throw new Error('Invalid component JSON structure');
}
if (!Array.isArray(componentJson.files)) {
throw new Error('Component files must be an array');
}
const componentFile = componentJson.files.find(
(file) => file.type === 'registry:ui' && file.path.endsWith('.tsx')
);
if (!componentFile) {
console.warn(`⚠️ No component file found for ${componentName}`);
return false;
}
// Validate file structure
if (!componentFile.path || !componentFile.content) {
throw new Error('Component file missing path or content');
}
const fileName = getFileName(componentFile.path);
validateContent(componentFile.content);
const content = rewriteImports(componentFile.content);
const filePath = path.join(targetDirs.components, fileName);
await fs.writeFile(filePath, content, 'utf8');
console.log(`✅ Pulled ${fileName}`);
return true;
} catch (error) {
console.error(`❌ Error pulling ${componentName}:`, (error as Error).message);
return false;
}
}
async function main(): Promise<void> {
console.log('🚀 Pulling coss ui components...\n');
try {
const { targetDirs, targetRoot } = await resolvePaths();
console.log(`Target root: ${targetRoot}`);
console.log(`Components dir: ${targetDirs.components}\n`);
console.log(`Pulling index: ${UI_JSON_URL}...`);
const uiJson = await fetchJSON<ComponentJson>(UI_JSON_URL);
// Validate index structure
if (!uiJson || typeof uiJson !== 'object') {
throw new Error('Invalid UI index JSON structure');
}
if (!Array.isArray(uiJson.registryDependencies)) {
throw new Error('registryDependencies must be an array');
}
const registryDependencies = uiJson.registryDependencies || [];
console.log(`Found ${registryDependencies.length} components\n`);
const results = await Promise.all(
registryDependencies.map((dep) => processComponent(dep, targetDirs))
);
const successCount = results.filter((r) => r === true).length;
const failCount = results.filter((r) => r === false).length;
console.log(`\n✨ Done!`);
console.log(`✅ Successfully pulled: ${successCount} components`);
if (failCount > 0) {
console.log(`❌ Failed: ${failCount} components`);
}
} catch (error) {
console.error('❌ Fatal error:', (error as Error).message);
process.exit(1);
}
}
void main();
+86 -56
View File
@@ -2640,17 +2640,17 @@ __metadata:
languageName: node
linkType: hard
"@base-ui-components/react@npm:^1.0.0-beta.4":
version: 1.0.0-beta.4
resolution: "@base-ui-components/react@npm:1.0.0-beta.4"
"@base-ui/react@npm:^1.0.0":
version: 1.0.0
resolution: "@base-ui/react@npm:1.0.0"
dependencies:
"@babel/runtime": ^7.28.4
"@base-ui-components/utils": 0.1.2
"@base-ui/utils": 0.2.3
"@floating-ui/react-dom": ^2.1.6
"@floating-ui/utils": ^0.2.10
reselect: ^5.1.1
tabbable: ^6.2.0
use-sync-external-store: ^1.5.0
tabbable: ^6.3.0
use-sync-external-store: ^1.6.0
peerDependencies:
"@types/react": ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
@@ -2658,18 +2658,18 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 591dc4359e18b9a02f693c11adc2952aeed4746c3a144b758454276a47221bf5ea61830de4da507ad8a275490040e7ea5a78266f325226e5c23b727b3ef6b7ff
checksum: c7bf7bac226edd3b71a2d5ec7629a4dd135cd8a830dc27776e3a9028555d02d4aa270ebaff7fd090378a84dffcc91beb396c0f1998530f22bcd7313543a04e77
languageName: node
linkType: hard
"@base-ui-components/utils@npm:0.1.2":
version: 0.1.2
resolution: "@base-ui-components/utils@npm:0.1.2"
"@base-ui/utils@npm:0.2.3":
version: 0.2.3
resolution: "@base-ui/utils@npm:0.2.3"
dependencies:
"@babel/runtime": ^7.28.4
"@floating-ui/utils": ^0.2.10
reselect: ^5.1.1
use-sync-external-store: ^1.5.0
use-sync-external-store: ^1.6.0
peerDependencies:
"@types/react": ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
@@ -2677,7 +2677,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: b5213da6b1ede5cb27bd61b2b602ee3812a770b4a9a5a58ea571806b8656ea28fc14b7b2916402c0189a354348d3ac8cc4ae4c2a468530cc9a5003ad3e24d4dc
checksum: 7d287170aa73a73ef1d56f7b59e00a099d30e3b87a9fc14c9baf7e8250d592ef897b01cd21a6c046b41a34262f703beffed4f099a8a6d0576bad768d217a2ffc
languageName: node
linkType: hard
@@ -3207,7 +3207,6 @@ __metadata:
resolution: "@calcom/config@workspace:packages/config"
dependencies:
"@calcom/eslint-plugin-eslint": "workspace:*"
"@tailwindcss/forms": ^0.5.10
"@tailwindcss/typography": ^0.5.4
"@todesktop/tailwind-variants": ^1.0.0
"@trivago/prettier-plugin-sort-imports": 4.1.1
@@ -4977,10 +4976,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "@coss/ui@workspace:packages/coss-ui"
dependencies:
"@base-ui-components/react": ^1.0.0-beta.4
class-variance-authority: ^0.4.0
clsx: ^2.0.0
tailwind-merge: ^2.0.0
"@base-ui/react": ^1.0.0
class-variance-authority: 0.7.1
clsx: ^2.1.1
lucide-react: 0.555.0
tailwind-merge: ^3.4.0
ts-node: ^10.9.2
tsc-absolute: 1.0.0
typescript: 5.9.0-beta
languageName: unknown
linkType: soft
@@ -17268,17 +17271,6 @@ __metadata:
languageName: node
linkType: hard
"@tailwindcss/forms@npm:^0.5.10":
version: 0.5.10
resolution: "@tailwindcss/forms@npm:0.5.10"
dependencies:
mini-svg-data-uri: ^1.2.3
peerDependencies:
tailwindcss: ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
checksum: 4526d02edccc4e44599d9588f83e4ac3e9435d137da5638653de2e74d5b612ade449a8c26d075be21692c1ac00a514aaffdb6723e526e3c8314c9a75a9f45979
languageName: node
linkType: hard
"@tailwindcss/node@npm:4.1.15":
version: 4.1.15
resolution: "@tailwindcss/node@npm:4.1.15"
@@ -23736,6 +23728,15 @@ __metadata:
languageName: node
linkType: hard
"class-variance-authority@npm:0.7.1, class-variance-authority@npm:^0.7.0, class-variance-authority@npm:^0.7.1":
version: 0.7.1
resolution: "class-variance-authority@npm:0.7.1"
dependencies:
clsx: ^2.1.1
checksum: e05ba26ef9ec38f7c675047ce366b067d60af6c954dba08f7802af19a9460a534ae752d8fe1294fff99d0fa94a669b16ccebd87e8a20f637c0736cf2751dd2c5
languageName: node
linkType: hard
"class-variance-authority@npm:^0.4.0":
version: 0.4.0
resolution: "class-variance-authority@npm:0.4.0"
@@ -23748,15 +23749,6 @@ __metadata:
languageName: node
linkType: hard
"class-variance-authority@npm:^0.7.0, class-variance-authority@npm:^0.7.1":
version: 0.7.1
resolution: "class-variance-authority@npm:0.7.1"
dependencies:
clsx: ^2.1.1
checksum: e05ba26ef9ec38f7c675047ce366b067d60af6c954dba08f7802af19a9460a534ae752d8fe1294fff99d0fa94a669b16ccebd87e8a20f637c0736cf2751dd2c5
languageName: node
linkType: hard
"classnames@npm:^2.2.6":
version: 2.3.2
resolution: "classnames@npm:2.3.2"
@@ -35755,6 +35747,15 @@ __metadata:
languageName: node
linkType: hard
"lucide-react@npm:0.555.0":
version: 0.555.0
resolution: "lucide-react@npm:0.555.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 070691202cee3691d9fc8706e4a4c4ba5bad67b9ed9b12b70b3a8bc374c9e6e8ae0e0e167347753ac41f179864b7d8a5498bf3fea655b2ad7fd60a9ae8908510
languageName: node
linkType: hard
"lucide-static@npm:^0.424.0":
version: 0.424.0
resolution: "lucide-static@npm:0.424.0"
@@ -37074,15 +37075,6 @@ __metadata:
languageName: node
linkType: hard
"mini-svg-data-uri@npm:^1.2.3":
version: 1.4.4
resolution: "mini-svg-data-uri@npm:1.4.4"
bin:
mini-svg-data-uri: cli.js
checksum: 997f1fbd8d59a70f03761e18626d335197a3479cb9d1ff75678e4b64b864d32a0b8fc18115eabde035e5299b8b4a354a78e57dd6ac10f9d604162a6170898d09
languageName: node
linkType: hard
"minimal-polyfills@npm:^2.2.3":
version: 2.2.3
resolution: "minimal-polyfills@npm:2.2.3"
@@ -46813,10 +46805,10 @@ __metadata:
languageName: node
linkType: hard
"tabbable@npm:^6.2.0":
version: 6.2.0
resolution: "tabbable@npm:6.2.0"
checksum: f8440277d223949272c74bb627a3371be21735ca9ad34c2570f7e1752bd646ccfc23a9d8b1ee65d6561243f4134f5fbbf1ad6b39ac3c4b586554accaff4a1300
"tabbable@npm:^6.3.0":
version: 6.3.0
resolution: "tabbable@npm:6.3.0"
checksum: 5ffeb2db569db7f3fe11b766f599bc0b67100697034e5c6264995555e8dead029c2e51d6b1e5ea65781e9077bf05ca40d3313c1c47351124a7e68727bc0f3968
languageName: node
linkType: hard
@@ -46827,10 +46819,10 @@ __metadata:
languageName: node
linkType: hard
"tailwind-merge@npm:^2.0.0":
version: 2.6.0
resolution: "tailwind-merge@npm:2.6.0"
checksum: 18976c4096920bc6125f1dc837479805de996d86bcc636f98436f65c297003bde89ffe51dfd325b7c97fc71b1dbba8505459dd96010e7b181badd29aea996440
"tailwind-merge@npm:^3.4.0":
version: 3.4.0
resolution: "tailwind-merge@npm:3.4.0"
checksum: 172d0bc19af4512e037e481354e9038211b2748f1a96f51a6c33559aec2fc4230e6ad52c0b16c4ccc03d8392b87781a615cd1b93badcbd6333b160eb5aca30f7
languageName: node
linkType: hard
@@ -47844,6 +47836,44 @@ __metadata:
languageName: node
linkType: hard
"ts-node@npm:^10.9.2":
version: 10.9.2
resolution: "ts-node@npm:10.9.2"
dependencies:
"@cspotcode/source-map-support": ^0.8.0
"@tsconfig/node10": ^1.0.7
"@tsconfig/node12": ^1.0.7
"@tsconfig/node14": ^1.0.0
"@tsconfig/node16": ^1.0.2
acorn: ^8.4.1
acorn-walk: ^8.1.1
arg: ^4.1.0
create-require: ^1.1.0
diff: ^4.0.1
make-error: ^1.1.1
v8-compile-cache-lib: ^3.0.1
yn: 3.1.1
peerDependencies:
"@swc/core": ">=1.2.50"
"@swc/wasm": ">=1.2.50"
"@types/node": "*"
typescript: ">=2.7"
peerDependenciesMeta:
"@swc/core":
optional: true
"@swc/wasm":
optional: true
bin:
ts-node: dist/bin.js
ts-node-cwd: dist/bin-cwd.js
ts-node-esm: dist/bin-esm.js
ts-node-script: dist/bin-script.js
ts-node-transpile-only: dist/bin-transpile.js
ts-script: dist/bin-script-deprecated.js
checksum: fde256c9073969e234526e2cfead42591b9a2aec5222bac154b0de2fa9e4ceb30efcd717ee8bc785a56f3a119bdd5aa27b333d9dbec94ed254bd26f8944c67ac
languageName: node
linkType: hard
"tsafe@npm:^1.8.5":
version: 1.8.12
resolution: "tsafe@npm:1.8.12"
@@ -47851,7 +47881,7 @@ __metadata:
languageName: node
linkType: hard
"tsc-absolute@npm:^1.0.0":
"tsc-absolute@npm:1.0.0, tsc-absolute@npm:^1.0.0":
version: 1.0.0
resolution: "tsc-absolute@npm:1.0.0"
dependencies:
@@ -49277,7 +49307,7 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.5.0":
"use-sync-external-store@npm:^1.6.0":
version: 1.6.0
resolution: "use-sync-external-store@npm:1.6.0"
peerDependencies: