@oppulence/design-system 1.0.2
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.
- package/README.md +115 -0
- package/components.json +21 -0
- package/hooks/use-mobile.tsx +21 -0
- package/lib/utils.ts +6 -0
- package/package.json +104 -0
- package/postcss.config.mjs +8 -0
- package/src/components/atoms/aspect-ratio.tsx +21 -0
- package/src/components/atoms/avatar.tsx +91 -0
- package/src/components/atoms/badge.tsx +47 -0
- package/src/components/atoms/button.tsx +128 -0
- package/src/components/atoms/checkbox.tsx +24 -0
- package/src/components/atoms/container.tsx +42 -0
- package/src/components/atoms/heading.tsx +56 -0
- package/src/components/atoms/index.ts +21 -0
- package/src/components/atoms/input.tsx +18 -0
- package/src/components/atoms/kbd.tsx +23 -0
- package/src/components/atoms/label.tsx +15 -0
- package/src/components/atoms/logo.tsx +52 -0
- package/src/components/atoms/progress.tsx +79 -0
- package/src/components/atoms/separator.tsx +17 -0
- package/src/components/atoms/skeleton.tsx +13 -0
- package/src/components/atoms/slider.tsx +56 -0
- package/src/components/atoms/spinner.tsx +14 -0
- package/src/components/atoms/stack.tsx +126 -0
- package/src/components/atoms/switch.tsx +26 -0
- package/src/components/atoms/text.tsx +69 -0
- package/src/components/atoms/textarea.tsx +19 -0
- package/src/components/atoms/toggle.tsx +40 -0
- package/src/components/molecules/accordion.tsx +72 -0
- package/src/components/molecules/ai-chat.tsx +251 -0
- package/src/components/molecules/alert.tsx +131 -0
- package/src/components/molecules/breadcrumb.tsx +301 -0
- package/src/components/molecules/button-group.tsx +96 -0
- package/src/components/molecules/card.tsx +184 -0
- package/src/components/molecules/collapsible.tsx +21 -0
- package/src/components/molecules/command-search.tsx +148 -0
- package/src/components/molecules/empty.tsx +98 -0
- package/src/components/molecules/field.tsx +217 -0
- package/src/components/molecules/grid.tsx +141 -0
- package/src/components/molecules/hover-card.tsx +45 -0
- package/src/components/molecules/index.ts +29 -0
- package/src/components/molecules/input-group.tsx +151 -0
- package/src/components/molecules/input-otp.tsx +74 -0
- package/src/components/molecules/item.tsx +194 -0
- package/src/components/molecules/page-header.tsx +89 -0
- package/src/components/molecules/pagination.tsx +130 -0
- package/src/components/molecules/popover.tsx +96 -0
- package/src/components/molecules/radio-group.tsx +37 -0
- package/src/components/molecules/resizable.tsx +52 -0
- package/src/components/molecules/scroll-area.tsx +45 -0
- package/src/components/molecules/section.tsx +108 -0
- package/src/components/molecules/select.tsx +201 -0
- package/src/components/molecules/settings.tsx +197 -0
- package/src/components/molecules/table.tsx +111 -0
- package/src/components/molecules/tabs.tsx +74 -0
- package/src/components/molecules/theme-switcher.tsx +187 -0
- package/src/components/molecules/toggle-group.tsx +89 -0
- package/src/components/molecules/tooltip.tsx +66 -0
- package/src/components/organisms/alert-dialog.tsx +152 -0
- package/src/components/organisms/app-shell.tsx +939 -0
- package/src/components/organisms/calendar.tsx +212 -0
- package/src/components/organisms/carousel.tsx +230 -0
- package/src/components/organisms/chart.tsx +333 -0
- package/src/components/organisms/combobox.tsx +274 -0
- package/src/components/organisms/command.tsx +200 -0
- package/src/components/organisms/context-menu.tsx +229 -0
- package/src/components/organisms/dialog.tsx +134 -0
- package/src/components/organisms/drawer.tsx +123 -0
- package/src/components/organisms/dropdown-menu.tsx +256 -0
- package/src/components/organisms/index.ts +17 -0
- package/src/components/organisms/menubar.tsx +203 -0
- package/src/components/organisms/navigation-menu.tsx +143 -0
- package/src/components/organisms/page-layout.tsx +105 -0
- package/src/components/organisms/sheet.tsx +126 -0
- package/src/components/organisms/sidebar.tsx +723 -0
- package/src/components/organisms/sonner.tsx +41 -0
- package/src/components/ui/index.ts +3 -0
- package/src/index.ts +3 -0
- package/src/styles/globals.css +297 -0
- package/tailwind.config.ts +77 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { Heading } from "../atoms/heading";
|
|
5
|
+
import { Stack } from "../atoms/stack";
|
|
6
|
+
import { Text } from "../atoms/text";
|
|
7
|
+
|
|
8
|
+
const settingRowVariants = cva(
|
|
9
|
+
"flex w-full items-start justify-between gap-4 py-4 first:pt-0 last:pb-0",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
size: {
|
|
13
|
+
default: "py-4",
|
|
14
|
+
sm: "py-3",
|
|
15
|
+
lg: "py-5",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
size: "default",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
interface SettingRowProps
|
|
25
|
+
extends
|
|
26
|
+
Omit<React.ComponentProps<"div">, "className">,
|
|
27
|
+
VariantProps<typeof settingRowVariants> {
|
|
28
|
+
/** The setting label */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Optional description text */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Whether the setting is disabled */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A horizontal row for a single setting with label/description on the left
|
|
38
|
+
* and a control (switch, button, select, etc.) on the right.
|
|
39
|
+
*/
|
|
40
|
+
function SettingRow({
|
|
41
|
+
label,
|
|
42
|
+
description,
|
|
43
|
+
disabled,
|
|
44
|
+
size,
|
|
45
|
+
children,
|
|
46
|
+
...props
|
|
47
|
+
}: SettingRowProps) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
data-slot="setting-row"
|
|
51
|
+
data-disabled={disabled || undefined}
|
|
52
|
+
className={settingRowVariants({ size })}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<div className="min-w-0 flex-1">
|
|
56
|
+
<Stack gap="1">
|
|
57
|
+
<Text weight="medium" variant={disabled ? "muted" : undefined}>
|
|
58
|
+
{label}
|
|
59
|
+
</Text>
|
|
60
|
+
{description && (
|
|
61
|
+
<Text size="sm" variant="muted">
|
|
62
|
+
{description}
|
|
63
|
+
</Text>
|
|
64
|
+
)}
|
|
65
|
+
</Stack>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex shrink-0 items-center">{children}</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface SettingGroupProps extends Omit<
|
|
73
|
+
React.ComponentProps<"div">,
|
|
74
|
+
"className"
|
|
75
|
+
> {
|
|
76
|
+
/** Whether to show dividers between rows */
|
|
77
|
+
divided?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Groups multiple SettingRow components together, optionally with dividers.
|
|
82
|
+
*/
|
|
83
|
+
function SettingGroup({
|
|
84
|
+
divided = true,
|
|
85
|
+
children,
|
|
86
|
+
...props
|
|
87
|
+
}: SettingGroupProps) {
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
data-slot="setting-group"
|
|
91
|
+
data-divided={divided || undefined}
|
|
92
|
+
className={
|
|
93
|
+
divided
|
|
94
|
+
? "[&>[data-slot=setting-row]:not(:last-child)]:border-border [&>[data-slot=setting-row]:not(:last-child)]:border-b"
|
|
95
|
+
: ""
|
|
96
|
+
}
|
|
97
|
+
{...props}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface SettingLabelProps extends Omit<
|
|
105
|
+
React.ComponentProps<"div">,
|
|
106
|
+
"className"
|
|
107
|
+
> {}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Custom label area for complex setting rows. Use when you need more than just text.
|
|
111
|
+
*/
|
|
112
|
+
function SettingLabel({ children, ...props }: SettingLabelProps) {
|
|
113
|
+
return (
|
|
114
|
+
<div data-slot="setting-label" className="min-w-0 flex-1" {...props}>
|
|
115
|
+
{children}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface SettingControlProps extends Omit<
|
|
121
|
+
React.ComponentProps<"div">,
|
|
122
|
+
"className"
|
|
123
|
+
> {}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Control area for setting rows. Use when you need multiple controls or custom layout.
|
|
127
|
+
*/
|
|
128
|
+
function SettingControl({ children, ...props }: SettingControlProps) {
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
data-slot="setting-control"
|
|
132
|
+
className="flex shrink-0 items-center gap-2"
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface SettingsCardProps extends Omit<
|
|
141
|
+
React.ComponentProps<"div">,
|
|
142
|
+
"className"
|
|
143
|
+
> {
|
|
144
|
+
/** Card title */
|
|
145
|
+
title: string;
|
|
146
|
+
/** Optional description */
|
|
147
|
+
description?: string;
|
|
148
|
+
/** Hint text shown in footer (left side) */
|
|
149
|
+
hint?: React.ReactNode;
|
|
150
|
+
/** Action button/content shown in footer (right side) */
|
|
151
|
+
action?: React.ReactNode;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A self-contained settings card with title, description, content area, and footer.
|
|
156
|
+
* Footer shows hint text on left and action button on right.
|
|
157
|
+
*/
|
|
158
|
+
function SettingsCard({
|
|
159
|
+
title,
|
|
160
|
+
description,
|
|
161
|
+
hint,
|
|
162
|
+
action,
|
|
163
|
+
children,
|
|
164
|
+
...props
|
|
165
|
+
}: SettingsCardProps) {
|
|
166
|
+
const hasFooter = hint || action;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
data-slot="settings-card"
|
|
171
|
+
className="bg-card text-card-foreground rounded-xl border shadow-sm"
|
|
172
|
+
{...props}
|
|
173
|
+
>
|
|
174
|
+
<div className="px-6 pt-6 pb-4">
|
|
175
|
+
<Stack gap="1">
|
|
176
|
+
<Text weight="semibold">{title}</Text>
|
|
177
|
+
{description && (
|
|
178
|
+
<Text size="sm" variant="muted">
|
|
179
|
+
{description}
|
|
180
|
+
</Text>
|
|
181
|
+
)}
|
|
182
|
+
</Stack>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="px-6 pb-6">{children}</div>
|
|
185
|
+
{hasFooter && (
|
|
186
|
+
<div className="border-t bg-muted/30 px-6 py-4">
|
|
187
|
+
<div className="flex items-center justify-between gap-4">
|
|
188
|
+
<div className="text-muted-foreground text-sm">{hint}</div>
|
|
189
|
+
<div className="flex shrink-0 items-center gap-2">{action}</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { SettingControl, SettingGroup, SettingLabel, SettingRow, SettingsCard };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
function Table({
|
|
4
|
+
variant = "default",
|
|
5
|
+
...props
|
|
6
|
+
}: Omit<React.ComponentProps<"table">, "className"> & {
|
|
7
|
+
variant?: "default" | "bordered";
|
|
8
|
+
}) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
data-slot="table-container"
|
|
12
|
+
data-variant={variant}
|
|
13
|
+
className="relative w-full overflow-x-auto data-[variant=bordered]:border data-[variant=bordered]:rounded-lg"
|
|
14
|
+
>
|
|
15
|
+
<table
|
|
16
|
+
data-slot="table"
|
|
17
|
+
className="w-full caption-bottom text-sm"
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function TableHeader({
|
|
25
|
+
...props
|
|
26
|
+
}: Omit<React.ComponentProps<"thead">, "className">) {
|
|
27
|
+
return (
|
|
28
|
+
<thead data-slot="table-header" className="[&_tr]:border-b" {...props} />
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function TableBody({
|
|
33
|
+
...props
|
|
34
|
+
}: Omit<React.ComponentProps<"tbody">, "className">) {
|
|
35
|
+
return (
|
|
36
|
+
<tbody
|
|
37
|
+
data-slot="table-body"
|
|
38
|
+
className="[&_tr:last-child]:border-0"
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function TableFooter({
|
|
45
|
+
...props
|
|
46
|
+
}: Omit<React.ComponentProps<"tfoot">, "className">) {
|
|
47
|
+
return (
|
|
48
|
+
<tfoot
|
|
49
|
+
data-slot="table-footer"
|
|
50
|
+
className="bg-muted/50 border-t font-medium [&>tr]:last:border-b-0"
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function TableRow({ ...props }: Omit<React.ComponentProps<"tr">, "className">) {
|
|
57
|
+
return (
|
|
58
|
+
<tr
|
|
59
|
+
data-slot="table-row"
|
|
60
|
+
className="hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors"
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TableHead({
|
|
67
|
+
...props
|
|
68
|
+
}: Omit<React.ComponentProps<"th">, "className">) {
|
|
69
|
+
return (
|
|
70
|
+
<th
|
|
71
|
+
data-slot="table-head"
|
|
72
|
+
className="text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0"
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function TableCell({
|
|
79
|
+
...props
|
|
80
|
+
}: Omit<React.ComponentProps<"td">, "className">) {
|
|
81
|
+
return (
|
|
82
|
+
<td
|
|
83
|
+
data-slot="table-cell"
|
|
84
|
+
className="p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0"
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function TableCaption({
|
|
91
|
+
...props
|
|
92
|
+
}: Omit<React.ComponentProps<"caption">, "className">) {
|
|
93
|
+
return (
|
|
94
|
+
<caption
|
|
95
|
+
data-slot="table-caption"
|
|
96
|
+
className="text-muted-foreground mt-4 text-sm"
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
Table,
|
|
104
|
+
TableBody,
|
|
105
|
+
TableCaption,
|
|
106
|
+
TableCell,
|
|
107
|
+
TableFooter,
|
|
108
|
+
TableHead,
|
|
109
|
+
TableHeader,
|
|
110
|
+
TableRow,
|
|
111
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
|
|
6
|
+
function Tabs({
|
|
7
|
+
orientation = "horizontal",
|
|
8
|
+
...props
|
|
9
|
+
}: Omit<TabsPrimitive.Root.Props, "className">) {
|
|
10
|
+
return (
|
|
11
|
+
<TabsPrimitive.Root
|
|
12
|
+
data-slot="tabs"
|
|
13
|
+
data-orientation={orientation}
|
|
14
|
+
className="group/tabs flex data-[orientation=horizontal]:flex-col"
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tabsListVariants = cva(
|
|
21
|
+
"rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none data-[variant=underline]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
|
22
|
+
{
|
|
23
|
+
variants: {
|
|
24
|
+
variant: {
|
|
25
|
+
default: "bg-muted",
|
|
26
|
+
line: "gap-1 bg-transparent",
|
|
27
|
+
underline:
|
|
28
|
+
"p-0 pb-px bg-transparent w-full justify-start items-stretch shadow-[inset_0_-1px_0_0_var(--color-border)]",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
variant: "default",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function TabsList({
|
|
38
|
+
variant = "default",
|
|
39
|
+
...props
|
|
40
|
+
}: Omit<TabsPrimitive.List.Props, "className"> &
|
|
41
|
+
VariantProps<typeof tabsListVariants>) {
|
|
42
|
+
return (
|
|
43
|
+
<TabsPrimitive.List
|
|
44
|
+
data-slot="tabs-list"
|
|
45
|
+
data-variant={variant}
|
|
46
|
+
className={tabsListVariants({ variant })}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function TabsTrigger({ ...props }: Omit<TabsPrimitive.Tab.Props, "className">) {
|
|
53
|
+
return (
|
|
54
|
+
<TabsPrimitive.Tab
|
|
55
|
+
data-slot="tabs-trigger"
|
|
56
|
+
className="gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none group-data-[variant=underline]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=underline]/tabs-list:bg-transparent group-data-[variant=underline]/tabs-list:data-active:bg-transparent group-data-[variant=underline]/tabs-list:px-3 group-data-[variant=underline]/tabs-list:py-2 group-data-[variant=underline]/tabs-list:mb-0 group-data-[variant=underline]/tabs-list:hover:bg-muted group-data-[variant=underline]/tabs-list:rounded-t-md group-data-[variant=underline]/tabs-list:flex-none dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=underline]/tabs-list:data-active:border-transparent dark:group-data-[variant=underline]/tabs-list:data-active:bg-transparent data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=underline]/tabs-list:after:bg-primary group-data-[variant=line]/tabs-list:data-active:after:opacity-100 group-data-[variant=underline]/tabs-list:after:h-[3px] group-data-[variant=underline]/tabs-list:after:bottom-0 group-data-[variant=underline]/tabs-list:after:rounded-t-sm group-data-[variant=underline]/tabs-list:h-auto group-data-[variant=underline]/tabs-list:data-active:after:opacity-100"
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function TabsContent({
|
|
63
|
+
...props
|
|
64
|
+
}: Omit<TabsPrimitive.Panel.Props, "className">) {
|
|
65
|
+
return (
|
|
66
|
+
<TabsPrimitive.Panel
|
|
67
|
+
data-slot="tabs-content"
|
|
68
|
+
className="text-sm flex-1 outline-none animate-in fade-in-0 duration-200"
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Tabs, TabsContent, TabsList, tabsListVariants, TabsTrigger };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
const themeSwitcherVariants = cva(
|
|
8
|
+
"inline-flex items-center rounded-full p-0.5 bg-muted",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
sm: "gap-0.5",
|
|
13
|
+
default: "gap-0.5",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
size: "default",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const themeSwitcherButtonVariants = cva(
|
|
23
|
+
"inline-flex items-center justify-center rounded-full transition-all duration-200",
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
size: {
|
|
27
|
+
sm: "size-6 [&_svg]:size-3",
|
|
28
|
+
default: "size-7 [&_svg]:size-3.5",
|
|
29
|
+
},
|
|
30
|
+
isActive: {
|
|
31
|
+
true: "bg-background text-foreground shadow-sm",
|
|
32
|
+
false: "text-muted-foreground hover:text-foreground",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
size: "default",
|
|
37
|
+
isActive: false,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
type Theme = "light" | "dark" | "system";
|
|
43
|
+
|
|
44
|
+
interface ThemeSwitcherProps
|
|
45
|
+
extends
|
|
46
|
+
Omit<React.ComponentProps<"div">, "className" | "onChange">,
|
|
47
|
+
VariantProps<typeof themeSwitcherVariants> {
|
|
48
|
+
/** Current theme value */
|
|
49
|
+
value?: Theme;
|
|
50
|
+
/** Default theme value (uncontrolled) */
|
|
51
|
+
defaultValue?: Theme;
|
|
52
|
+
/** Called when theme changes */
|
|
53
|
+
onChange?: (theme: Theme) => void;
|
|
54
|
+
/** Show system option */
|
|
55
|
+
showSystem?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ThemeSwitcher({
|
|
59
|
+
value: valueProp,
|
|
60
|
+
defaultValue = "system",
|
|
61
|
+
onChange,
|
|
62
|
+
showSystem = true,
|
|
63
|
+
size = "default",
|
|
64
|
+
...props
|
|
65
|
+
}: ThemeSwitcherProps) {
|
|
66
|
+
const [internalValue, setInternalValue] = React.useState<Theme>(defaultValue);
|
|
67
|
+
const value = valueProp ?? internalValue;
|
|
68
|
+
|
|
69
|
+
const handleChange = (newTheme: Theme) => {
|
|
70
|
+
if (valueProp === undefined) {
|
|
71
|
+
setInternalValue(newTheme);
|
|
72
|
+
}
|
|
73
|
+
onChange?.(newTheme);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const options: { value: Theme; icon: React.ReactNode; label: string }[] = [
|
|
77
|
+
{ value: "light", icon: <SunIcon />, label: "Light mode" },
|
|
78
|
+
{ value: "dark", icon: <MoonIcon />, label: "Dark mode" },
|
|
79
|
+
...(showSystem
|
|
80
|
+
? [
|
|
81
|
+
{
|
|
82
|
+
value: "system" as Theme,
|
|
83
|
+
icon: <MonitorIcon />,
|
|
84
|
+
label: "System theme",
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
: []),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
data-slot="theme-switcher"
|
|
93
|
+
role="radiogroup"
|
|
94
|
+
aria-label="Theme"
|
|
95
|
+
className={themeSwitcherVariants({ size })}
|
|
96
|
+
{...props}
|
|
97
|
+
>
|
|
98
|
+
{options.map((option) => (
|
|
99
|
+
<button
|
|
100
|
+
key={option.value}
|
|
101
|
+
type="button"
|
|
102
|
+
role="radio"
|
|
103
|
+
aria-checked={value === option.value}
|
|
104
|
+
aria-label={option.label}
|
|
105
|
+
onClick={() => handleChange(option.value)}
|
|
106
|
+
className={themeSwitcherButtonVariants({
|
|
107
|
+
size,
|
|
108
|
+
isActive: value === option.value,
|
|
109
|
+
})}
|
|
110
|
+
>
|
|
111
|
+
{option.icon}
|
|
112
|
+
</button>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Simple light/dark toggle for compact spaces
|
|
119
|
+
interface ThemeToggleProps extends Omit<
|
|
120
|
+
React.ComponentProps<"button">,
|
|
121
|
+
"className" | "onChange"
|
|
122
|
+
> {
|
|
123
|
+
/** Current theme - true for dark, false for light */
|
|
124
|
+
isDark?: boolean;
|
|
125
|
+
/** Called when theme changes */
|
|
126
|
+
onChange?: (isDark: boolean) => void;
|
|
127
|
+
/** Size variant */
|
|
128
|
+
size?: "sm" | "default";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ThemeToggle({
|
|
132
|
+
isDark: isDarkProp,
|
|
133
|
+
onChange,
|
|
134
|
+
size = "default",
|
|
135
|
+
...props
|
|
136
|
+
}: ThemeToggleProps) {
|
|
137
|
+
const [internalIsDark, setInternalIsDark] = React.useState(false);
|
|
138
|
+
const isDark = isDarkProp ?? internalIsDark;
|
|
139
|
+
|
|
140
|
+
const handleToggle = () => {
|
|
141
|
+
const newValue = !isDark;
|
|
142
|
+
if (isDarkProp === undefined) {
|
|
143
|
+
setInternalIsDark(newValue);
|
|
144
|
+
}
|
|
145
|
+
onChange?.(newValue);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const iconSize = size === "sm" ? "size-3" : "size-3.5";
|
|
149
|
+
const buttonSize = size === "sm" ? "h-6 w-12" : "h-7 w-14";
|
|
150
|
+
const thumbSize = size === "sm" ? "size-5" : "size-6";
|
|
151
|
+
const thumbTranslate =
|
|
152
|
+
size === "sm" ? "translate-x-[26px]" : "translate-x-[30px]";
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
role="switch"
|
|
158
|
+
aria-checked={isDark}
|
|
159
|
+
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
|
160
|
+
onClick={handleToggle}
|
|
161
|
+
data-slot="theme-toggle"
|
|
162
|
+
className={`${buttonSize} relative inline-flex items-center rounded-full bg-muted p-0.5 transition-colors`}
|
|
163
|
+
{...props}
|
|
164
|
+
>
|
|
165
|
+
{/* Background icons */}
|
|
166
|
+
<span className="absolute inset-0 flex items-center justify-between px-1.5">
|
|
167
|
+
<SunIcon className={`${iconSize} text-amber-500`} />
|
|
168
|
+
<MoonIcon className={`${iconSize} text-blue-400`} />
|
|
169
|
+
</span>
|
|
170
|
+
{/* Sliding thumb */}
|
|
171
|
+
<span
|
|
172
|
+
className={`${thumbSize} relative z-10 flex items-center justify-center rounded-full bg-background shadow-sm transition-transform duration-200 ${
|
|
173
|
+
isDark ? thumbTranslate : "translate-x-0"
|
|
174
|
+
}`}
|
|
175
|
+
>
|
|
176
|
+
{isDark ? (
|
|
177
|
+
<MoonIcon className={`${iconSize} text-blue-500`} />
|
|
178
|
+
) : (
|
|
179
|
+
<SunIcon className={`${iconSize} text-amber-500`} />
|
|
180
|
+
)}
|
|
181
|
+
</span>
|
|
182
|
+
</button>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export { ThemeSwitcher, ThemeToggle };
|
|
187
|
+
export type { ThemeSwitcherProps, ThemeToggleProps, Theme };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
|
|
4
|
+
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
|
|
5
|
+
import { type VariantProps } from "class-variance-authority";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../../lib/utils";
|
|
9
|
+
import { toggleVariants } from "../atoms/toggle";
|
|
10
|
+
|
|
11
|
+
const ToggleGroupContext = React.createContext<
|
|
12
|
+
VariantProps<typeof toggleVariants> & {
|
|
13
|
+
spacing?: number;
|
|
14
|
+
orientation?: "horizontal" | "vertical";
|
|
15
|
+
}
|
|
16
|
+
>({
|
|
17
|
+
size: "default",
|
|
18
|
+
variant: "default",
|
|
19
|
+
spacing: 0,
|
|
20
|
+
orientation: "horizontal",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
type ToggleGroupProps = Omit<ToggleGroupPrimitive.Props, "className"> &
|
|
24
|
+
VariantProps<typeof toggleVariants> & {
|
|
25
|
+
spacing?: number;
|
|
26
|
+
orientation?: "horizontal" | "vertical";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function ToggleGroup({
|
|
30
|
+
variant,
|
|
31
|
+
size,
|
|
32
|
+
spacing = 0,
|
|
33
|
+
orientation = "horizontal",
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: ToggleGroupProps) {
|
|
37
|
+
return (
|
|
38
|
+
<ToggleGroupPrimitive
|
|
39
|
+
data-slot="toggle-group"
|
|
40
|
+
data-variant={variant}
|
|
41
|
+
data-size={size}
|
|
42
|
+
data-spacing={spacing}
|
|
43
|
+
data-orientation={orientation}
|
|
44
|
+
style={{ "--gap": spacing } as React.CSSProperties}
|
|
45
|
+
className="rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch"
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
<ToggleGroupContext.Provider
|
|
49
|
+
value={{ variant, size, spacing, orientation }}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</ToggleGroupContext.Provider>
|
|
53
|
+
</ToggleGroupPrimitive>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ToggleGroupItemProps = Omit<TogglePrimitive.Props, "className"> &
|
|
58
|
+
VariantProps<typeof toggleVariants>;
|
|
59
|
+
|
|
60
|
+
function ToggleGroupItem({
|
|
61
|
+
children,
|
|
62
|
+
variant = "default",
|
|
63
|
+
size = "default",
|
|
64
|
+
...props
|
|
65
|
+
}: ToggleGroupItemProps) {
|
|
66
|
+
const context = React.useContext(ToggleGroupContext);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<TogglePrimitive
|
|
70
|
+
data-slot="toggle-group-item"
|
|
71
|
+
data-variant={context.variant || variant}
|
|
72
|
+
data-size={context.size || size}
|
|
73
|
+
data-spacing={context.spacing}
|
|
74
|
+
className={cn(
|
|
75
|
+
"data-[state=on]:bg-muted group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md shrink-0 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
|
|
76
|
+
toggleVariants({
|
|
77
|
+
variant: context.variant || variant,
|
|
78
|
+
size: context.size || size,
|
|
79
|
+
}),
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
</TogglePrimitive>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { ToggleGroup, ToggleGroupItem };
|
|
89
|
+
export type { ToggleGroupItemProps, ToggleGroupProps };
|