diff --git a/packages/ui/src/blur-image.tsx b/apps/web/src/components/ui/blur-image.tsx
similarity index 98%
rename from packages/ui/src/blur-image.tsx
rename to apps/web/src/components/ui/blur-image.tsx
index bc61d4fc4..d45a02d4c 100644
--- a/packages/ui/src/blur-image.tsx
+++ b/apps/web/src/components/ui/blur-image.tsx
@@ -1,3 +1,4 @@
+'use client'
/**
* website
* Copyright (c) Delba de Oliveira
diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css
index 3182490a7..265189bce 100644
--- a/apps/web/src/styles/globals.css
+++ b/apps/web/src/styles/globals.css
@@ -16,29 +16,27 @@
--font-mono: var(--font-geist-mono);
}
-@layer base {
- :root {
- --anchor: rgb(255, 34, 14);
-
- --nav-link-indicator: radial-gradient(
- 44.6% 825% at 50% 50%,
- rgb(255 133 133) 0%,
- rgb(255 72 109 / 0) 100%
- );
- --email-button: linear-gradient(180deg, rgb(210 10 30) 5%, rgb(239 90 90) 100%);
-
- --feature-card: 0 -1px 3px 0 rgb(0 0 0 / 0.05);
- }
-
- .dark {
- --anchor: rgb(255, 69, 51);
-
- --nav-link-indicator: radial-gradient(
- 44.6% 825% at 50% 50%,
- rgb(255 28 28) 0%,
- rgb(255 72 109 / 0) 100%
- );
-
- --feature-card: 0 0 0 1px rgb(255 255 255 / 0.06), 0 -1px rgb(255 255 255 / 0.1);
- }
+:root {
+ --anchor: rgb(255, 34, 14);
+
+ --nav-link-indicator: radial-gradient(
+ 44.6% 825% at 50% 50%,
+ rgb(255 133 133) 0%,
+ rgb(255 72 109 / 0) 100%
+ );
+ --email-button: linear-gradient(180deg, rgb(210 10 30) 5%, rgb(239 90 90) 100%);
+
+ --feature-card: 0 -1px 3px 0 rgb(0 0 0 / 0.05);
+}
+
+.dark {
+ --anchor: rgb(255, 69, 51);
+
+ --nav-link-indicator: radial-gradient(
+ 44.6% 825% at 50% 50%,
+ rgb(255 28 28) 0%,
+ rgb(255 72 109 / 0) 100%
+ );
+
+ --feature-card: 0 0 0 1px rgb(255 255 255 / 0.06), 0 -1px rgb(255 255 255 / 0.1);
}
diff --git a/knip.config.ts b/knip.config.ts
index 85376b841..4885b6dfe 100644
--- a/knip.config.ts
+++ b/knip.config.ts
@@ -1,7 +1,6 @@
import type { KnipConfig } from 'knip'
const config: KnipConfig = {
- ignore: ['**/fixtures/**'],
ignoreDependencies: [
'prettier-plugin-*',
'sharp',
@@ -34,7 +33,7 @@ const config: KnipConfig = {
},
'packages/ui': {
// @see https://github.com/shadcn-ui/ui/issues/4792
- ignoreDependencies: ['@radix-ui/react-context', '@tailwindcss/typography']
+ ignoreDependencies: ['tw-animate-css', '@tailwindcss/typography']
}
}
}
diff --git a/packages/i18n/src/messages/en.json b/packages/i18n/src/messages/en.json
index 4bafbb207..3fe1f5fdd 100644
--- a/packages/i18n/src/messages/en.json
+++ b/packages/i18n/src/messages/en.json
@@ -101,11 +101,7 @@
},
"no-results": "No results found.",
"open-menu": "Open command menu",
- "placeholder": "Type a command or search",
- "trigger": {
- "open-command": "Open Command",
- "open-link": "Open Link"
- }
+ "placeholder": "Type a command or search"
},
"common": {
"cancel": "Cancel",
diff --git a/packages/i18n/src/messages/zh-CN.json b/packages/i18n/src/messages/zh-CN.json
index 110b75b7a..e1214f1b6 100644
--- a/packages/i18n/src/messages/zh-CN.json
+++ b/packages/i18n/src/messages/zh-CN.json
@@ -101,11 +101,7 @@
},
"no-results": "没有找到结果。",
"open-menu": "开启指令选单",
- "placeholder": "输入指令或搜寻",
- "trigger": {
- "open-command": "打开命令",
- "open-link": "打开链接"
- }
+ "placeholder": "输入指令或搜寻"
},
"common": {
"cancel": "取消",
diff --git a/packages/i18n/src/messages/zh-TW.json b/packages/i18n/src/messages/zh-TW.json
index aa836f767..26fa80b74 100644
--- a/packages/i18n/src/messages/zh-TW.json
+++ b/packages/i18n/src/messages/zh-TW.json
@@ -101,11 +101,7 @@
},
"no-results": "沒有找到結果。",
"open-menu": "開啟指令選單",
- "placeholder": "輸入指令或搜尋",
- "trigger": {
- "open-command": "打開命令",
- "open-link": "打開連結"
- }
+ "placeholder": "輸入指令或搜尋"
},
"common": {
"cancel": "取消",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 3f879b901..b40d4da8f 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -41,7 +41,6 @@
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
- "@radix-ui/react-context": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
@@ -50,17 +49,18 @@
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
+ "@radix-ui/react-progress": "^1.1.3",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
+ "@radix-ui/react-slider": "^1.2.4",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
- "@radix-ui/react-visually-hidden": "^1.1.2",
"@tailwindcss/typography": "^0.5.16",
"cmdk": "^1.1.1",
"cva": "^1.0.0-beta.3",
@@ -68,30 +68,34 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"merge-refs": "^2.0.0",
- "motion": "^12.6.3",
"react-hook-form": "^7.55.0",
"react-resizable-panels": "^2.1.7",
"react-textarea-autosize": "^8.5.9",
"sonner": "^2.0.3",
+ "tw-animate-css": "^1.2.5",
"vaul": "^1.1.2"
},
"peerDependencies": {
- "@tanstack/react-table": "^8",
"@tszhong0411/utils": ">=0",
"next": "^15",
+ "next-themes": "^0.4.0",
"react": "^19",
- "react-dom": "^19"
+ "react-day-picker": "^8",
+ "react-dom": "^19",
+ "recharts": "^2"
},
"devDependencies": {
- "@tanstack/react-table": "^8.21.2",
"@tszhong0411/eslint-config": "workspace:*",
"@tszhong0411/tsconfig": "workspace:*",
"@tszhong0411/utils": "workspace:*",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.2",
"next": "^15.3.0",
+ "next-themes": "^0.4.6",
"react": "^19.1.0",
+ "react-day-picker": "8.10.1",
"react-dom": "^19.1.0",
+ "recharts": "^2.15.2",
"tailwindcss": "^4.1.3"
},
"publishConfig": {
diff --git a/packages/ui/src/accordion.tsx b/packages/ui/src/accordion.tsx
index faec62ba7..b94b82c40 100644
--- a/packages/ui/src/accordion.tsx
+++ b/packages/ui/src/accordion.tsx
@@ -2,14 +2,24 @@ import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { cn } from '@tszhong0411/utils'
import { ChevronDownIcon } from 'lucide-react'
-const Accordion = AccordionPrimitive.Root
+type AccordionProps = React.ComponentProps
+
+const Accordion = (props: AccordionProps) => (
+
+)
type AccordionItemProps = React.ComponentProps
const AccordionItem = (props: AccordionItemProps) => {
const { className, ...rest } = props
- return
+ return (
+
+ )
}
type AccordionTriggerProps = React.ComponentProps
@@ -20,15 +30,19 @@ const AccordionTrigger = (props: AccordionTriggerProps) => {
return (
svg]:rotate-180',
className
)}
{...rest}
>
{children}
-
+
)
@@ -41,6 +55,7 @@ const AccordionContent = (props: AccordionContentProps) => {
return (
+
+const AlertDialog = (props: AlertDialogProps) => (
+
+)
+
+type AlertDialogTriggerProps = React.ComponentProps
+
+const AlertDialogTrigger = (props: AlertDialogTriggerProps) => (
+
+)
+
+type AlertDialogPortalProps = React.ComponentProps
+
+const AlertDialogPortal = (props: AlertDialogPortalProps) => (
+
+)
type AlertDialogOverlayProps = React.ComponentProps
@@ -14,8 +28,9 @@ const AlertDialogOverlay = (props: AlertDialogOverlayProps) => {
return (
{
const AlertDialogHeader = (props: AlertDialogHeaderProps) => {
const { className, ...rest } = props
- return
+ return (
+
+ )
}
type AlertDialogFooterProps = React.ComponentProps<'div'>
@@ -61,6 +84,7 @@ const AlertDialogFooter = (props: AlertDialogFooterProps) => {
return (
@@ -72,7 +96,13 @@ type AlertDialogTitleProps = React.ComponentProps {
const { className, ...rest } = props
- return
+ return (
+
+ )
}
type AlertDialogDescriptionProps = React.ComponentProps
@@ -82,6 +112,7 @@ const AlertDialogDescription = (props: AlertDialogDescriptionProps) => {
return (
diff --git a/packages/ui/src/alert.tsx b/packages/ui/src/alert.tsx
new file mode 100644
index 000000000..10e6131cc
--- /dev/null
+++ b/packages/ui/src/alert.tsx
@@ -0,0 +1,68 @@
+import { cn } from '@tszhong0411/utils'
+import { cva, type VariantProps } from 'cva'
+
+const alertVariants = cva({
+ base: [
+ 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm',
+ 'has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3',
+ '[&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current'
+ ],
+ variants: {
+ variant: {
+ default: 'bg-card text-card-foreground',
+ destructive:
+ 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current'
+ }
+ },
+ defaultVariants: {
+ variant: 'default'
+ }
+})
+
+type AlertProps = React.ComponentProps<'div'> & VariantProps
+
+const Alert = (props: AlertProps) => {
+ const { className, variant, ...rest } = props
+
+ return (
+
+ )
+}
+
+type AlertTitleProps = React.ComponentProps<'div'>
+
+const AlertTitle = (props: AlertTitleProps) => {
+ const { className, ...rest } = props
+
+ return (
+
+ )
+}
+
+type AlertDescriptionProps = React.ComponentProps<'div'>
+
+const AlertDescription = (props: AlertDescriptionProps) => {
+ const { className, ...rest } = props
+
+ return (
+
+ )
+}
+
+export { Alert, AlertDescription, AlertTitle }
diff --git a/packages/ui/src/aspect-ratio.ts b/packages/ui/src/aspect-ratio.ts
deleted file mode 100644
index 07bc6747e..000000000
--- a/packages/ui/src/aspect-ratio.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
-
-const AspectRatio = AspectRatioPrimitive.Root
-
-export { AspectRatio }
diff --git a/packages/ui/src/aspect-ratio.tsx b/packages/ui/src/aspect-ratio.tsx
new file mode 100644
index 000000000..c98c905ad
--- /dev/null
+++ b/packages/ui/src/aspect-ratio.tsx
@@ -0,0 +1,9 @@
+import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
+
+type AspectRatioProps = React.ComponentProps
+
+const AspectRatio = (props: AspectRatioProps) => (
+
+)
+
+export { AspectRatio }
diff --git a/packages/ui/src/avatar.tsx b/packages/ui/src/avatar.tsx
index 125f5a9bd..5ee92eb00 100644
--- a/packages/ui/src/avatar.tsx
+++ b/packages/ui/src/avatar.tsx
@@ -8,7 +8,8 @@ const Avatar = (props: AvatarProps) => {
return (
)
@@ -19,7 +20,13 @@ type AvatarImageProps = React.ComponentProps
const AvatarImage = (props: AvatarImageProps) => {
const { className, ...rest } = props
- return
+ return (
+
+ )
}
type AvatarFallbackProps = React.ComponentProps
@@ -29,23 +36,11 @@ const AvatarFallback = (props: AvatarFallbackProps) => {
return (
)
}
-const getAvatarAbbreviation = (name: string) => {
- const abbreviation = name
- .split(' ')
- .map((n) => n[0])
- .join('')
-
- if (abbreviation.length > 2) {
- return abbreviation.slice(0, 2)
- }
-
- return abbreviation
-}
-
-export { Avatar, AvatarFallback, AvatarImage, getAvatarAbbreviation }
+export { Avatar, AvatarFallback, AvatarImage }
diff --git a/packages/ui/src/badge.tsx b/packages/ui/src/badge.tsx
index 747524b32..b1bb5bca3 100644
--- a/packages/ui/src/badge.tsx
+++ b/packages/ui/src/badge.tsx
@@ -1,14 +1,23 @@
+import { Slot } from '@radix-ui/react-slot'
import { cn } from '@tszhong0411/utils'
import { cva, type VariantProps } from 'cva'
const badgeVariants = cva({
- base: 'inline-flex items-center justify-center rounded-full border px-2.5 py-0.5 text-xs font-semibold',
+ base: [
+ 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow]',
+ 'dark:aria-invalid:ring-destructive/40',
+ 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
+ 'aria-invalid:ring-destructive/20 aria-invalid:border-destructive',
+ '[&>svg]:pointer-events-none [&>svg]:size-3'
+ ],
variants: {
variant: {
- default: 'bg-primary text-primary-foreground border-transparent shadow-sm',
- secondary: 'bg-secondary text-secondary-foreground border-transparent shadow-sm',
- destructive: 'bg-destructive text-destructive-foreground border-transparent shadow-sm',
- outline: 'text-foreground'
+ default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
+ secondary:
+ 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
+ destructive:
+ 'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 border-transparent text-white',
+ outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
}
},
defaultVariants: {
@@ -16,12 +25,16 @@ const badgeVariants = cva({
}
})
-type BadgeProps = React.ComponentProps<'div'> & VariantProps
+type BadgeProps = React.ComponentProps<'span'> &
+ VariantProps & {
+ asChild?: boolean
+ }
const Badge = (props: BadgeProps) => {
- const { className, variant, ...rest } = props
+ const { className, variant, asChild = false, ...rest } = props
+ const Comp = asChild ? Slot : 'span'
- return
+ return
}
export { Badge, badgeVariants }
diff --git a/packages/ui/src/blur-fade.tsx b/packages/ui/src/blur-fade.tsx
deleted file mode 100644
index 578da9d00..000000000
--- a/packages/ui/src/blur-fade.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import {
- AnimatePresence,
- motion,
- useInView,
- type UseInViewOptions,
- type Variants
-} from 'motion/react'
-import { useRef } from 'react'
-
-type BlurFadeProps = {
- children: React.ReactNode
- className?: string
- variant?: {
- hidden: { y: number }
- visible: { y: number }
- }
- duration?: number
- delay?: number
- yOffset?: number
- inView?: boolean
- inViewMargin?: UseInViewOptions['margin']
- blur?: string
-}
-
-const BlurFade = (props: BlurFadeProps) => {
- const {
- children,
- className,
- variant,
- duration = 0.4,
- delay = 0,
- yOffset = 6,
- inView = false,
- inViewMargin = '-50px',
- blur = '6px'
- } = props
- const ref = useRef(null)
- const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
- const isInView = !inView || inViewResult
-
- const defaultVariants: Variants = {
- hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
- visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` }
- }
-
- const combinedVariants = variant ?? defaultVariants
-
- return (
-
-
- {children}
-
-
- )
-}
-
-export { BlurFade }
diff --git a/packages/ui/src/breadcrumb.tsx b/packages/ui/src/breadcrumb.tsx
index d14997cc2..e5c12fa1e 100644
--- a/packages/ui/src/breadcrumb.tsx
+++ b/packages/ui/src/breadcrumb.tsx
@@ -1,13 +1,12 @@
import { Slot } from '@radix-ui/react-slot'
import { cn } from '@tszhong0411/utils'
import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
-import { usePathname } from 'next/navigation'
-type BreadcrumbProps = {
- separator?: React.ReactNode
-} & React.ComponentProps<'nav'>
+type BreadcrumbProps = React.ComponentProps<'nav'>
-const Breadcrumb = (props: BreadcrumbProps) =>
+const Breadcrumb = (props: BreadcrumbProps) => (
+
+)
type BreadcrumbListProps = React.ComponentProps<'ol'>
@@ -16,6 +15,7 @@ const BreadcrumbList = (props: BreadcrumbListProps) => {
return (
const BreadcrumbItem = (props: BreadcrumbItemProps) => {
const { className, ...rest } = props
- return
+ return (
+
+ )
}
-type BreadcrumbLinkProps = {
+type BreadcrumbLinkProps = React.ComponentProps<'a'> & {
asChild?: boolean
-} & React.ComponentProps<'a'>
+}
const BreadcrumbLink = (props: BreadcrumbLinkProps) => {
- const { asChild, className, ...rest } = props
+ const { className, asChild, ...rest } = props
const Comp = asChild ? Slot : 'a'
- const pathname = usePathname()
return (
)
@@ -58,10 +63,11 @@ const BreadcrumbPage = (props: BreadcrumbPageProps) => {
return (
)
@@ -74,12 +80,13 @@ const BreadcrumbSeparator = (props: BreadcrumbSeparatorProps) => {
return (
- svg]:size-3.5', className)}
{...rest}
>
- {children ?? }
+ {children ?? }
)
}
@@ -91,12 +98,14 @@ const BreadcrumbEllipsis = (props: BreadcrumbEllipsisProps) => {
return (
+ More
)
}
diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx
index 25feac4be..cc572b310 100644
--- a/packages/ui/src/button.tsx
+++ b/packages/ui/src/button.tsx
@@ -1,27 +1,32 @@
import { cn } from '@tszhong0411/utils'
import { cva, type VariantProps } from 'cva'
-import { LoaderIcon } from 'lucide-react'
const buttonVariants = cva({
base: [
- 'ring-offset-background inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-colors',
- 'focus-visible:ring-ring focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2',
- 'disabled:pointer-events-none disabled:opacity-50'
+ 'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all',
+ 'dark:aria-invalid:ring-destructive/40',
+ 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
+ 'aria-invalid:ring-destructive/20 aria-invalid:border-destructive',
+ 'disabled:pointer-events-none disabled:opacity-50',
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0',
+ "[&_svg:not([class*='size-'])]:size-4"
],
variants: {
variant: {
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
- outline: 'border-input bg-background hover:bg-accent hover:text-accent-foreground border',
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
+ destructive:
+ 'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 shadow-xs text-white',
+ outline:
+ 'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 shadow-xs border',
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs',
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
- default: 'h-10 px-4 py-2',
- sm: 'h-9 px-3',
- lg: 'h-11 px-8',
- icon: 'size-10'
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9'
}
},
defaultVariants: {
@@ -30,32 +35,19 @@ const buttonVariants = cva({
}
})
-type ButtonProps = { isPending?: boolean } & React.ComponentProps<'button'> &
- VariantProps
+type ButtonProps = React.ComponentProps<'button'> & VariantProps
const Button = (props: ButtonProps) => {
- const {
- className,
- variant,
- size,
- type = 'button',
- isPending,
- disabled,
- children,
- ...rest
- } = props
+ const { className, variant, size, type = 'button', ...rest } = props
return (
+ />
)
}
-export { Button, type ButtonProps, buttonVariants }
+export { Button, buttonVariants }
diff --git a/packages/ui/src/calendar.tsx b/packages/ui/src/calendar.tsx
new file mode 100644
index 000000000..7a11f6afe
--- /dev/null
+++ b/packages/ui/src/calendar.tsx
@@ -0,0 +1,80 @@
+import { cn } from '@tszhong0411/utils'
+import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
+import { DayPicker } from 'react-day-picker'
+
+import { buttonVariants } from './button'
+
+type CalendarProps = React.ComponentProps
+
+const Calendar = (props: CalendarProps) => {
+ const { className, classNames, showOutsideDays = true, ...rest } = props
+
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
+ : '[&:has([aria-selected])]:rounded-md'
+ ),
+ day: cn(
+ buttonVariants({ variant: 'ghost' }),
+ 'size-8 p-0 font-normal aria-selected:opacity-100'
+ ),
+ day_range_start:
+ 'day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground',
+ day_range_end:
+ 'day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground',
+ day_selected:
+ 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
+ day_today: 'bg-accent text-accent-foreground',
+ day_outside: 'day-outside text-muted-foreground aria-selected:text-muted-foreground',
+ day_disabled: 'text-muted-foreground opacity-50',
+ day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
+ day_hidden: 'invisible',
+ ...classNames
+ }}
+ components={{
+ IconLeft,
+ IconRight
+ }}
+ {...rest}
+ />
+ )
+}
+
+type IconLeftProps = React.ComponentProps
+
+const IconLeft = (props: IconLeftProps) => {
+ const { className, ...rest } = props
+
+ return
+}
+
+type IconRightProps = React.ComponentProps
+
+const IconRight = (props: IconRightProps) => {
+ const { className, ...rest } = props
+
+ return
+}
+
+export { Calendar }
diff --git a/packages/ui/src/callout.tsx b/packages/ui/src/callout.tsx
deleted file mode 100644
index ca49314db..000000000
--- a/packages/ui/src/callout.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { cn } from '@tszhong0411/utils'
-import { AlertOctagonIcon, AlertTriangleIcon, InfoIcon } from 'lucide-react'
-
-type CalloutProps = {
- title?: React.ReactNode
- type?: 'info' | 'warning' | 'error'
- icon?: React.ReactNode
-} & React.ComponentProps<'div'>
-
-const Callout = (props: CalloutProps) => {
- const { title, type = 'info', icon, className, children, ...rest } = props
-
- const icons = {
- info: ,
- warning: ,
- error:
- }
-
- return (
-
- {icon ?? icons[type]}
-
- {title ?
{title}
: null}
-
{children}
-
-
- )
-}
-
-export { Callout }
diff --git a/packages/ui/src/card.tsx b/packages/ui/src/card.tsx
index eaaef6328..c5a647760 100644
--- a/packages/ui/src/card.tsx
+++ b/packages/ui/src/card.tsx
@@ -7,7 +7,11 @@ const Card = (props: CardProps) => {
return (
)
@@ -18,26 +22,54 @@ type CardHeaderProps = React.ComponentProps<'div'>
const CardHeader = (props: CardHeaderProps) => {
const { className, ...rest } = props
- return
+ return (
+
+ )
}
-type CardTitleProps = React.ComponentProps<'h3'>
+type CardTitleProps = React.ComponentProps<'div'>
const CardTitle = (props: CardTitleProps) => {
const { className, ...rest } = props
return (
- // eslint-disable-next-line jsx-a11y/heading-has-content -- content is passed via children
-
+
)
}
-type CardDescriptionProps = React.ComponentProps<'p'>
+type CardDescriptionProps = React.ComponentProps<'div'>
const CardDescription = (props: CardDescriptionProps) => {
const { className, ...rest } = props
- return
+ return (
+
+ )
+}
+
+type CardActionProps = React.ComponentProps<'div'>
+
+const CardAction = (props: CardActionProps) => {
+ const { className, ...rest } = props
+
+ return (
+
+ )
}
type CardContentProps = React.ComponentProps<'div'>
@@ -45,7 +77,7 @@ type CardContentProps = React.ComponentProps<'div'>
const CardContent = (props: CardContentProps) => {
const { className, ...rest } = props
- return
+ return
}
type CardFooterProps = React.ComponentProps<'div'>
@@ -53,7 +85,13 @@ type CardFooterProps = React.ComponentProps<'div'>
const CardFooter = (props: CardFooterProps) => {
const { className, ...rest } = props
- return
+ return (
+
+ )
}
-export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
+export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
diff --git a/packages/ui/src/carousel.tsx b/packages/ui/src/carousel.tsx
index 99c14cb7d..39f664669 100644
--- a/packages/ui/src/carousel.tsx
+++ b/packages/ui/src/carousel.tsx
@@ -1,6 +1,6 @@
import { cn } from '@tszhong0411/utils'
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'
-import { ArrowLeft, ArrowRight } from 'lucide-react'
+import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react'
import { createContext, use, useCallback, useEffect, useMemo, useState } from 'react'
import { Button } from './button'
@@ -15,7 +15,7 @@ type CarouselProps = {
plugins?: CarouselPlugin
orientation?: 'horizontal' | 'vertical'
setApi?: (api: CarouselApi) => void
-}
+} & React.ComponentProps<'div'>
type CarouselContextProps = {
carouselRef: ReturnType[0]
@@ -39,9 +39,7 @@ const useCarousel = () => {
return context
}
-type CarouselRootProps = React.ComponentProps<'div'> & CarouselProps
-
-const Carousel = (props: CarouselRootProps) => {
+const Carousel = (props: CarouselProps) => {
const {
orientation = 'horizontal',
options,
@@ -51,6 +49,7 @@ const Carousel = (props: CarouselRootProps) => {
children,
...rest
} = props
+
const [carouselRef, api] = useEmblaCarousel(
{
...options,
@@ -63,13 +62,11 @@ const Carousel = (props: CarouselRootProps) => {
const onSelect = useCallback((a: CarouselApi) => {
if (!a) return
-
setCanScrollPrev(a.canScrollPrev())
setCanScrollNext(a.canScrollNext())
}, [])
const scrollPrev = useCallback(() => api?.scrollPrev(), [api])
-
const scrollNext = useCallback(() => api?.scrollNext(), [api])
const handleKeyDown = useCallback(
@@ -99,7 +96,6 @@ const Carousel = (props: CarouselRootProps) => {
api.on('select', onSelect)
return () => {
- api.off('reInit', onSelect)
api.off('select', onSelect)
}
}, [api, onSelect])
@@ -107,7 +103,7 @@ const Carousel = (props: CarouselRootProps) => {
const value = useMemo(
() => ({
carouselRef,
- api: api,
+ api,
options,
orientation,
scrollPrev,
@@ -117,6 +113,7 @@ const Carousel = (props: CarouselRootProps) => {
}),
[carouselRef, api, options, orientation, scrollPrev, scrollNext, canScrollPrev, canScrollNext]
)
+
return (
{
className={cn('relative', className)}
role='region'
aria-roledescription='carousel'
+ data-slot='carousel'
{...rest}
>
{children}
@@ -139,7 +137,7 @@ const CarouselContent = (props: CarouselContentProps) => {
const { carouselRef, orientation } = useCarousel()
return (
-
+
{
{
return (
)
}
@@ -203,6 +203,7 @@ const CarouselNext = (props: CarouselNextProps) => {
return (
)
}
-export { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious }
+export { Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious }
diff --git a/packages/ui/src/chart.tsx b/packages/ui/src/chart.tsx
new file mode 100644
index 000000000..c03f18731
--- /dev/null
+++ b/packages/ui/src/chart.tsx
@@ -0,0 +1,342 @@
+import { cn } from '@tszhong0411/utils'
+import { createContext, use, useId, useMemo } from 'react'
+import * as RechartsPrimitive from 'recharts'
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: '', dark: '.dark' } as const
+
+export type ChartConfig = Record<
+ string,
+ {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record
}
+ )
+>
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = createContext(null)
+ChartContext.displayName = 'ChartContext'
+
+const useChart = () => {
+ const context = use(ChartContext)
+
+ if (!context) {
+ throw new Error('useChart must be used within a ')
+ }
+
+ return context
+}
+
+type ChartContainerProps = React.ComponentProps<'div'> & {
+ config: ChartConfig
+ children: React.ComponentProps['children']
+}
+
+const ChartContainer = (props: ChartContainerProps) => {
+ const { id, className, children, config, ...rest } = props
+
+ const uniqueId = useId()
+ const chartId = `chart-${id ?? uniqueId.replaceAll(':', '')}`
+
+ const value = useMemo(() => ({ config }), [config])
+
+ return (
+
+
+
+ {children}
+
+
+ )
+}
+
+type ChartStyleProps = {
+ id: string
+ config: ChartConfig
+}
+
+const ChartStyle = (props: ChartStyleProps) => {
+ const { id, config } = props
+ const colorConfig = Object.entries(config).filter(([, c]) => c.theme ?? c.color)
+
+ if (colorConfig.length === 0) {
+ return null
+ }
+
+ return (
+