@voila.dev/ui 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +78 -0
  2. package/src/components/ui/accordion.tsx +79 -0
  3. package/src/components/ui/alert-dialog.tsx +198 -0
  4. package/src/components/ui/alert.tsx +105 -0
  5. package/src/components/ui/aspect-ratio.tsx +30 -0
  6. package/src/components/ui/avatar.tsx +125 -0
  7. package/src/components/ui/badge-variants.ts +166 -0
  8. package/src/components/ui/badge.tsx +45 -0
  9. package/src/components/ui/banner.tsx +84 -0
  10. package/src/components/ui/breadcrumb.tsx +127 -0
  11. package/src/components/ui/button-group.tsx +99 -0
  12. package/src/components/ui/button-variants.ts +108 -0
  13. package/src/components/ui/button.tsx +54 -0
  14. package/src/components/ui/calendar.tsx +266 -0
  15. package/src/components/ui/card.tsx +155 -0
  16. package/src/components/ui/carousel.tsx +319 -0
  17. package/src/components/ui/chat.tsx +896 -0
  18. package/src/components/ui/checkbox-group.tsx +44 -0
  19. package/src/components/ui/checkbox.tsx +34 -0
  20. package/src/components/ui/chip.tsx +60 -0
  21. package/src/components/ui/collapsible.tsx +44 -0
  22. package/src/components/ui/color-picker.tsx +191 -0
  23. package/src/components/ui/combobox.tsx +317 -0
  24. package/src/components/ui/command.tsx +187 -0
  25. package/src/components/ui/confirm-dialog.tsx +125 -0
  26. package/src/components/ui/context-menu.tsx +254 -0
  27. package/src/components/ui/copyable-text.tsx +65 -0
  28. package/src/components/ui/date-picker.tsx +316 -0
  29. package/src/components/ui/date-time-picker.tsx +1071 -0
  30. package/src/components/ui/dialog.tsx +176 -0
  31. package/src/components/ui/direction.tsx +15 -0
  32. package/src/components/ui/drawer.tsx +178 -0
  33. package/src/components/ui/dropdown-menu.tsx +247 -0
  34. package/src/components/ui/empty.tsx +131 -0
  35. package/src/components/ui/field.tsx +257 -0
  36. package/src/components/ui/formatted-input.tsx +219 -0
  37. package/src/components/ui/gallery.tsx +161 -0
  38. package/src/components/ui/hover-card.tsx +77 -0
  39. package/src/components/ui/icon-picker.tsx +193 -0
  40. package/src/components/ui/image-cropper.tsx +787 -0
  41. package/src/components/ui/image-upload-field.tsx +344 -0
  42. package/src/components/ui/input-group.tsx +175 -0
  43. package/src/components/ui/input-otp.tsx +83 -0
  44. package/src/components/ui/input.tsx +21 -0
  45. package/src/components/ui/item.tsx +212 -0
  46. package/src/components/ui/kbd.tsx +62 -0
  47. package/src/components/ui/label.tsx +19 -0
  48. package/src/components/ui/list.tsx +46 -0
  49. package/src/components/ui/menu-variants.ts +71 -0
  50. package/src/components/ui/menubar.tsx +226 -0
  51. package/src/components/ui/money-input.tsx +96 -0
  52. package/src/components/ui/native-date-picker.tsx +120 -0
  53. package/src/components/ui/native-select.tsx +74 -0
  54. package/src/components/ui/navigation-menu.tsx +183 -0
  55. package/src/components/ui/pagination.tsx +150 -0
  56. package/src/components/ui/popover.tsx +104 -0
  57. package/src/components/ui/profile-header.tsx +196 -0
  58. package/src/components/ui/progress.tsx +71 -0
  59. package/src/components/ui/radio-group.tsx +115 -0
  60. package/src/components/ui/rating.tsx +200 -0
  61. package/src/components/ui/resizable.tsx +61 -0
  62. package/src/components/ui/responsive-dialog.tsx +278 -0
  63. package/src/components/ui/responsive-select.tsx +289 -0
  64. package/src/components/ui/responsive-sheet.tsx +225 -0
  65. package/src/components/ui/scroll-area.tsx +51 -0
  66. package/src/components/ui/section.tsx +106 -0
  67. package/src/components/ui/segmented-control.tsx +148 -0
  68. package/src/components/ui/select.tsx +216 -0
  69. package/src/components/ui/separator.tsx +69 -0
  70. package/src/components/ui/sheet.tsx +154 -0
  71. package/src/components/ui/shortcut.tsx +41 -0
  72. package/src/components/ui/sidebar.tsx +739 -0
  73. package/src/components/ui/skeleton.tsx +23 -0
  74. package/src/components/ui/slider.tsx +74 -0
  75. package/src/components/ui/sonner.tsx +64 -0
  76. package/src/components/ui/spinner.tsx +19 -0
  77. package/src/components/ui/stat-card.tsx +138 -0
  78. package/src/components/ui/stepper.tsx +181 -0
  79. package/src/components/ui/sticky-action-bar.tsx +42 -0
  80. package/src/components/ui/switch.tsx +30 -0
  81. package/src/components/ui/table.tsx +132 -0
  82. package/src/components/ui/tabs.tsx +77 -0
  83. package/src/components/ui/textarea.tsx +24 -0
  84. package/src/components/ui/time-picker.tsx +221 -0
  85. package/src/components/ui/toggle-group.tsx +86 -0
  86. package/src/components/ui/toggle.tsx +43 -0
  87. package/src/components/ui/tooltip.tsx +79 -0
  88. package/src/components/ui/translation-input.tsx +126 -0
  89. package/src/components/ui/user-avatar.tsx +74 -0
  90. package/src/css-modules.d.ts +4 -0
  91. package/src/hooks/use-command-palette.ts +28 -0
  92. package/src/hooks/use-mobile.ts +21 -0
  93. package/src/hooks/use-pagination.ts +81 -0
  94. package/src/hooks/use-picker-state.ts +44 -0
  95. package/src/lib/cva.ts +18 -0
  96. package/src/lib/time-math.ts +62 -0
  97. package/src/lib/utils.ts +14 -0
  98. package/src/styles/globals.css +36 -0
  99. package/src/styles.css +10 -0
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@voila.dev/ui",
3
+ "version": "1.1.9",
4
+ "type": "module",
5
+ "description": "85 components, one convention. The floor everything else stands on.",
6
+ "license": "MIT",
7
+ "homepage": "https://ui.voila.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/voila-voila-dev/ui.git",
11
+ "directory": "packages/ui"
12
+ },
13
+ "keywords": [
14
+ "react",
15
+ "ui",
16
+ "components",
17
+ "tailwindcss",
18
+ "base-ui",
19
+ "design-system"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "!src/**/*.test.ts",
27
+ "!src/**/*.test.tsx",
28
+ "!src/**/*.stories.ts",
29
+ "!src/**/*.stories.tsx"
30
+ ],
31
+ "sideEffects": [
32
+ "**/*.css"
33
+ ],
34
+ "exports": {
35
+ "./styles.css": "./src/styles.css",
36
+ "./styles/globals.css": "./src/styles/globals.css",
37
+ "./lib/utils": "./src/lib/utils.ts",
38
+ "./lib/time-math": "./src/lib/time-math.ts",
39
+ "./cva": "./src/lib/cva.ts",
40
+ "./badge-variants": "./src/components/ui/badge-variants.ts",
41
+ "./button-variants": "./src/components/ui/button-variants.ts",
42
+ "./menu-variants": "./src/components/ui/menu-variants.ts",
43
+ "./components/*": "./src/components/ui/*.tsx",
44
+ "./hooks/*": "./src/hooks/*.ts"
45
+ },
46
+ "imports": {
47
+ "#/*": "./src/*"
48
+ },
49
+ "scripts": {
50
+ "check": "biome check",
51
+ "check-types": "tsc --noEmit",
52
+ "format": "biome format",
53
+ "lint": "biome lint",
54
+ "test": "vitest run"
55
+ },
56
+ "dependencies": {
57
+ "@base-ui/react": "^1.4.1",
58
+ "@phosphor-icons/react": "2.1.10",
59
+ "@voila.dev/ui-icon": "0.0.0",
60
+ "@voila.dev/ui-tokens": "0.0.0",
61
+ "clsx": "^2.1.1",
62
+ "cmdk": "^1.1.1",
63
+ "cva": "^1.0.0-beta.4",
64
+ "embla-carousel-react": "^8.6.0",
65
+ "input-otp": "^1.4.2",
66
+ "react-day-picker": "^10.0.1",
67
+ "react-resizable-panels": "^4.11.1",
68
+ "sonner": "^2.0.7",
69
+ "tailwind-merge": "^3.3.1",
70
+ "tw-animate-css": "^1.4.0",
71
+ "vaul": "^1.1.2"
72
+ },
73
+ "peerDependencies": {
74
+ "react": "^19.0.0",
75
+ "react-dom": "^19.0.0",
76
+ "tailwindcss": "^4.0.0"
77
+ }
78
+ }
@@ -0,0 +1,79 @@
1
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
2
+ import { CaretDownIcon } from "@phosphor-icons/react";
3
+ import { cn, proseLinkClassName } from "#/lib/utils.ts";
4
+
5
+ function Accordion({
6
+ className,
7
+ // Deliberate kit default: one item open at a time. Pass `multiple` to opt out.
8
+ multiple = false,
9
+ ...props
10
+ }: AccordionPrimitive.Root.Props) {
11
+ return (
12
+ <AccordionPrimitive.Root
13
+ data-slot="accordion"
14
+ className={cn("flex w-full flex-col", className)}
15
+ multiple={multiple}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
22
+ return (
23
+ <AccordionPrimitive.Item
24
+ data-slot="accordion-item"
25
+ className={cn("not-last:border-b", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function AccordionTrigger({
32
+ className,
33
+ children,
34
+ ...props
35
+ }: AccordionPrimitive.Trigger.Props) {
36
+ return (
37
+ <AccordionPrimitive.Header className="flex">
38
+ <AccordionPrimitive.Trigger
39
+ data-slot="accordion-trigger"
40
+ className={cn(
41
+ "group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <CaretDownIcon
48
+ data-slot="accordion-trigger-icon"
49
+ className="pointer-events-none shrink-0 transition-transform duration-200 group-aria-expanded/accordion-trigger:rotate-180 motion-reduce:transition-none"
50
+ />
51
+ </AccordionPrimitive.Trigger>
52
+ </AccordionPrimitive.Header>
53
+ );
54
+ }
55
+
56
+ function AccordionContent({
57
+ className,
58
+ children,
59
+ ...props
60
+ }: AccordionPrimitive.Panel.Props) {
61
+ return (
62
+ <AccordionPrimitive.Panel
63
+ data-slot="accordion-content"
64
+ className={cn(
65
+ "h-(--accordion-panel-height) overflow-hidden text-sm transition-[height] duration-200 ease-out data-ending-style:h-0 data-starting-style:h-0 motion-reduce:transition-none",
66
+ className,
67
+ )}
68
+ {...props}
69
+ >
70
+ <div
71
+ className={cn("pb-2.5 [&_p:not(:last-child)]:mb-4", proseLinkClassName)}
72
+ >
73
+ {children}
74
+ </div>
75
+ </AccordionPrimitive.Panel>
76
+ );
77
+ }
78
+
79
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -0,0 +1,198 @@
1
+ import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
2
+ import type * as React from "react";
3
+ import { Button } from "#/components/ui/button.tsx";
4
+ import { cn } from "#/lib/utils.ts";
5
+
6
+ function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
7
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
8
+ }
9
+
10
+ function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
11
+ return (
12
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
13
+ );
14
+ }
15
+
16
+ function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
17
+ return (
18
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
19
+ );
20
+ }
21
+
22
+ function AlertDialogClose({ ...props }: AlertDialogPrimitive.Close.Props) {
23
+ return (
24
+ <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
25
+ );
26
+ }
27
+
28
+ function AlertDialogOverlay({
29
+ className,
30
+ ...props
31
+ }: AlertDialogPrimitive.Backdrop.Props) {
32
+ return (
33
+ <AlertDialogPrimitive.Backdrop
34
+ data-slot="alert-dialog-overlay"
35
+ className={cn(
36
+ "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 motion-reduce:animate-none",
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ type AlertDialogContentSize = "default" | "sm";
45
+
46
+ function AlertDialogContent({
47
+ className,
48
+ size = "default",
49
+ ...props
50
+ }: AlertDialogPrimitive.Popup.Props & {
51
+ size?: AlertDialogContentSize;
52
+ }) {
53
+ return (
54
+ <AlertDialogPortal>
55
+ <AlertDialogOverlay />
56
+ <AlertDialogPrimitive.Popup
57
+ data-slot="alert-dialog-content"
58
+ data-size={size}
59
+ className={cn(
60
+ "group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full max-w-xs -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 motion-reduce:animate-none",
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ </AlertDialogPortal>
66
+ );
67
+ }
68
+
69
+ function AlertDialogHeader({
70
+ className,
71
+ ...props
72
+ }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="alert-dialog-header"
76
+ className={cn(
77
+ "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function AlertDialogFooter({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<"div">) {
89
+ return (
90
+ <div
91
+ data-slot="alert-dialog-footer"
92
+ className={cn(
93
+ "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
94
+ className,
95
+ )}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ function AlertDialogMedia({
102
+ className,
103
+ ...props
104
+ }: React.ComponentProps<"div">) {
105
+ return (
106
+ <div
107
+ data-slot="alert-dialog-media"
108
+ className={cn(
109
+ "mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
110
+ className,
111
+ )}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function AlertDialogTitle({
118
+ className,
119
+ ...props
120
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
121
+ return (
122
+ <AlertDialogPrimitive.Title
123
+ data-slot="alert-dialog-title"
124
+ className={cn(
125
+ "text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
126
+ className,
127
+ )}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+
133
+ function AlertDialogDescription({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
137
+ return (
138
+ <AlertDialogPrimitive.Description
139
+ data-slot="alert-dialog-description"
140
+ className={cn(
141
+ "text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
142
+ className,
143
+ )}
144
+ {...props}
145
+ />
146
+ );
147
+ }
148
+
149
+ function AlertDialogAction({
150
+ className,
151
+ variant = "default",
152
+ size = "default",
153
+ ...props
154
+ }: AlertDialogPrimitive.Close.Props &
155
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
156
+ return (
157
+ <AlertDialogPrimitive.Close
158
+ data-slot="alert-dialog-action"
159
+ className={className}
160
+ render={<Button variant={variant} size={size} />}
161
+ {...props}
162
+ />
163
+ );
164
+ }
165
+
166
+ function AlertDialogCancel({
167
+ className,
168
+ variant = "outline",
169
+ size = "default",
170
+ ...props
171
+ }: AlertDialogPrimitive.Close.Props &
172
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
173
+ return (
174
+ <AlertDialogPrimitive.Close
175
+ data-slot="alert-dialog-cancel"
176
+ className={className}
177
+ render={<Button variant={variant} size={size} />}
178
+ {...props}
179
+ />
180
+ );
181
+ }
182
+
183
+ export {
184
+ AlertDialog,
185
+ AlertDialogAction,
186
+ AlertDialogCancel,
187
+ AlertDialogClose,
188
+ AlertDialogContent,
189
+ type AlertDialogContentSize,
190
+ AlertDialogDescription,
191
+ AlertDialogFooter,
192
+ AlertDialogHeader,
193
+ AlertDialogMedia,
194
+ AlertDialogOverlay,
195
+ AlertDialogPortal,
196
+ AlertDialogTitle,
197
+ AlertDialogTrigger,
198
+ };
@@ -0,0 +1,105 @@
1
+ import { XIcon } from "@phosphor-icons/react";
2
+ import type * as React from "react";
3
+ import { buttonVariants } from "#/components/ui/button-variants.ts";
4
+ import { cva, type VariantProps } from "#/lib/cva.ts";
5
+
6
+ import { cn, proseLinkClassName } from "#/lib/utils.ts";
7
+
8
+ const alertVariants = cva({
9
+ base: "group/alert relative grid w-full grid-cols-1 gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-data-[slot=alert-action]:gap-x-2 has-[>svg]:has-data-[slot=alert-action]:grid-cols-[auto_1fr_auto] *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
10
+ variants: {
11
+ variant: {
12
+ default: "bg-card text-card-foreground",
13
+ destructive:
14
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
15
+ success:
16
+ "bg-card text-success *:data-[slot=alert-description]:text-success/90 *:[svg]:text-current",
17
+ warning:
18
+ "bg-card text-warning *:data-[slot=alert-description]:text-warning/90 *:[svg]:text-current",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ },
24
+ });
25
+
26
+ function Alert({
27
+ className,
28
+ variant,
29
+ ...props
30
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
31
+ return (
32
+ <div
33
+ data-slot="alert"
34
+ role="alert"
35
+ className={cn(alertVariants({ variant }), className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="alert-title"
45
+ className={cn(
46
+ "font-medium group-has-[>svg]/alert:col-start-2",
47
+ proseLinkClassName,
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function AlertDescription({
56
+ className,
57
+ ...props
58
+ }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="alert-description"
62
+ className={cn(
63
+ "text-sm text-balance text-muted-foreground md:text-pretty [&_p:not(:last-child)]:mb-4",
64
+ proseLinkClassName,
65
+ className,
66
+ )}
67
+ {...props}
68
+ />
69
+ );
70
+ }
71
+
72
+ function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="alert-action"
76
+ className={cn(
77
+ "col-start-2 row-span-2 row-start-1 self-center group-has-[>svg]/alert:col-start-3",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function AlertClose({ className, ...props }: React.ComponentProps<"button">) {
86
+ return (
87
+ <AlertAction>
88
+ <button
89
+ type="button"
90
+ data-slot="alert-close"
91
+ aria-label="Dismiss"
92
+ className={cn(
93
+ buttonVariants({ variant: "ghost", size: "icon-xs" }),
94
+ "text-muted-foreground",
95
+ className,
96
+ )}
97
+ {...props}
98
+ >
99
+ <XIcon />
100
+ </button>
101
+ </AlertAction>
102
+ );
103
+ }
104
+
105
+ export { Alert, AlertAction, AlertClose, AlertDescription, AlertTitle };
@@ -0,0 +1,30 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import type * as React from "react";
4
+ import { cn } from "#/lib/utils.ts";
5
+
6
+ function AspectRatio({
7
+ ratio = 1,
8
+ className,
9
+ render,
10
+ ...props
11
+ }: useRender.ComponentProps<"div"> & { ratio?: number | string }) {
12
+ return useRender({
13
+ defaultTagName: "div",
14
+ props: mergeProps<"div">(
15
+ {
16
+ className: cn("relative aspect-(--ratio)", className),
17
+ style: {
18
+ "--ratio": ratio,
19
+ } as React.CSSProperties,
20
+ },
21
+ props,
22
+ ),
23
+ render,
24
+ state: {
25
+ slot: "aspect-ratio",
26
+ },
27
+ });
28
+ }
29
+
30
+ export { AspectRatio };
@@ -0,0 +1,125 @@
1
+ import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
2
+ import type * as React from "react";
3
+
4
+ import { cn } from "#/lib/utils.ts";
5
+
6
+ function Avatar({
7
+ className,
8
+ size = "default",
9
+ ...props
10
+ }: AvatarPrimitive.Root.Props & {
11
+ size?: "default" | "sm" | "lg";
12
+ }) {
13
+ return (
14
+ <AvatarPrimitive.Root
15
+ data-slot="avatar"
16
+ data-size={size}
17
+ className={cn(
18
+ "group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
27
+ return (
28
+ <AvatarPrimitive.Image
29
+ data-slot="avatar-image"
30
+ className={cn(
31
+ "aspect-square size-full animate-in rounded-full object-cover duration-300 fade-in",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function AvatarFallback({
40
+ className,
41
+ ...props
42
+ }: AvatarPrimitive.Fallback.Props) {
43
+ return (
44
+ <AvatarPrimitive.Fallback
45
+ data-slot="avatar-fallback"
46
+ className={cn(
47
+ "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=lg]/avatar:text-base group-data-[size=sm]/avatar:text-xs",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ const avatarBadgeStatusClassNames = {
56
+ online: "bg-success text-success-foreground",
57
+ offline: "bg-muted-foreground text-muted",
58
+ busy: "bg-destructive text-destructive-foreground",
59
+ } as const;
60
+
61
+ function AvatarBadge({
62
+ className,
63
+ status,
64
+ ...props
65
+ }: React.ComponentProps<"span"> & {
66
+ status?: "online" | "offline" | "busy";
67
+ }) {
68
+ return (
69
+ <span
70
+ data-slot="avatar-badge"
71
+ data-status={status}
72
+ className={cn(
73
+ "absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background select-none",
74
+ status && avatarBadgeStatusClassNames[status],
75
+ "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
76
+ "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
77
+ "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
86
+ return (
87
+ <div
88
+ data-slot="avatar-group"
89
+ className={cn(
90
+ "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ );
96
+ }
97
+
98
+ function AvatarGroupCount({
99
+ className,
100
+ size = "default",
101
+ ...props
102
+ }: React.ComponentProps<"div"> & {
103
+ size?: "default" | "sm" | "lg";
104
+ }) {
105
+ return (
106
+ <div
107
+ data-slot="avatar-group-count"
108
+ data-size={size}
109
+ className={cn(
110
+ "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background data-[size=lg]:size-10 data-[size=sm]:size-6 data-[size=lg]:text-base data-[size=sm]:text-xs [&>svg]:size-4 data-[size=lg]:[&>svg]:size-5 data-[size=sm]:[&>svg]:size-3",
111
+ className,
112
+ )}
113
+ {...props}
114
+ />
115
+ );
116
+ }
117
+
118
+ export {
119
+ Avatar,
120
+ AvatarBadge,
121
+ AvatarFallback,
122
+ AvatarGroup,
123
+ AvatarGroupCount,
124
+ AvatarImage,
125
+ };