@trycompai/design-system 1.0.1 → 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/package.json +5 -2
- package/src/components/atoms/badge.tsx +8 -7
- package/src/components/atoms/button.tsx +6 -1
- package/src/components/atoms/checkbox.tsx +3 -3
- package/src/components/atoms/heading.tsx +6 -6
- package/src/components/atoms/index.ts +2 -0
- package/src/components/atoms/logo.tsx +52 -0
- package/src/components/atoms/spinner.tsx +3 -3
- package/src/components/atoms/stack.tsx +97 -0
- package/src/components/atoms/switch.tsx +1 -1
- package/src/components/atoms/text.tsx +5 -1
- package/src/components/atoms/toggle.tsx +1 -1
- package/src/components/molecules/accordion.tsx +3 -3
- package/src/components/molecules/ai-chat.tsx +217 -0
- package/src/components/molecules/alert.tsx +5 -5
- package/src/components/molecules/breadcrumb.tsx +8 -7
- package/src/components/molecules/card.tsx +24 -5
- package/src/components/molecules/command-search.tsx +147 -0
- package/src/components/molecules/index.ts +4 -1
- package/src/components/molecules/input-otp.tsx +2 -2
- package/src/components/molecules/page-header.tsx +33 -4
- package/src/components/molecules/pagination.tsx +4 -4
- package/src/components/molecules/popover.tsx +4 -2
- package/src/components/molecules/radio-group.tsx +2 -2
- package/src/components/molecules/section.tsx +1 -1
- package/src/components/molecules/select.tsx +5 -5
- package/src/components/molecules/settings.tsx +169 -0
- package/src/components/molecules/table.tsx +5 -1
- package/src/components/molecules/tabs.tsx +5 -4
- package/src/components/molecules/theme-switcher.tsx +176 -0
- package/src/components/organisms/app-shell.tsx +822 -0
- package/src/components/organisms/calendar.tsx +4 -4
- package/src/components/organisms/carousel.tsx +3 -3
- package/src/components/organisms/combobox.tsx +5 -5
- package/src/components/organisms/command.tsx +3 -3
- package/src/components/organisms/context-menu.tsx +4 -4
- package/src/components/organisms/dialog.tsx +2 -2
- package/src/components/organisms/dropdown-menu.tsx +8 -6
- package/src/components/organisms/index.ts +1 -0
- package/src/components/organisms/menubar.tsx +3 -3
- package/src/components/organisms/navigation-menu.tsx +2 -2
- package/src/components/organisms/page-layout.tsx +50 -20
- package/src/components/organisms/sheet.tsx +2 -2
- package/src/components/organisms/sidebar.tsx +22 -6
- package/src/components/organisms/sonner.tsx +11 -11
- package/src/fonts/TWKLausanne/TWKLausanne-300-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-300-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-300.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-300.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700.woff2 +0 -0
- package/src/styles/globals.css +155 -23
- package/src/components/molecules/stack.tsx +0 -72
|
@@ -2,7 +2,7 @@ import { mergeProps } from '@base-ui/react/merge-props';
|
|
|
2
2
|
import { useRender } from '@base-ui/react/use-render';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { ArrowRight, ChevronRight, OverflowMenuHorizontal } from '@carbon/icons-react';
|
|
6
6
|
import {
|
|
7
7
|
DropdownMenu,
|
|
8
8
|
DropdownMenuContent,
|
|
@@ -26,9 +26,10 @@ interface BreadcrumbItemData {
|
|
|
26
26
|
type BreadcrumbSeparatorType = 'chevron' | 'slash' | 'arrow';
|
|
27
27
|
|
|
28
28
|
const separatorIcons: Record<BreadcrumbSeparatorType, React.ReactNode> = {
|
|
29
|
-
chevron: <
|
|
30
|
-
slash
|
|
31
|
-
|
|
29
|
+
chevron: <ChevronRight />,
|
|
30
|
+
// Carbon doesn't ship a slash glyph icon; render it as text.
|
|
31
|
+
slash: <span className="text-xs leading-none">/</span>,
|
|
32
|
+
arrow: <ArrowRight />,
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
interface BreadcrumbProps extends Omit<React.ComponentProps<'nav'>, 'children' | 'className'> {
|
|
@@ -201,7 +202,7 @@ function BreadcrumbSeparator({
|
|
|
201
202
|
className="[&>svg]:size-3.5"
|
|
202
203
|
{...props}
|
|
203
204
|
>
|
|
204
|
-
{children ?? <
|
|
205
|
+
{children ?? <ChevronRight />}
|
|
205
206
|
</li>
|
|
206
207
|
);
|
|
207
208
|
}
|
|
@@ -215,7 +216,7 @@ function BreadcrumbEllipsis({ ...props }: Omit<React.ComponentProps<'span'>, 'cl
|
|
|
215
216
|
className="size-5 [&>svg]:size-4 flex items-center justify-center"
|
|
216
217
|
{...props}
|
|
217
218
|
>
|
|
218
|
-
<
|
|
219
|
+
<OverflowMenuHorizontal />
|
|
219
220
|
<span className="sr-only">More</span>
|
|
220
221
|
</span>
|
|
221
222
|
);
|
|
@@ -229,7 +230,7 @@ function BreadcrumbEllipsisMenu({ collapsedItems }: { collapsedItems?: Breadcrum
|
|
|
229
230
|
return (
|
|
230
231
|
<DropdownMenu>
|
|
231
232
|
<DropdownMenuTrigger variant="ellipsis" aria-label="Show hidden breadcrumb items">
|
|
232
|
-
<
|
|
233
|
+
<OverflowMenuHorizontal />
|
|
233
234
|
</DropdownMenuTrigger>
|
|
234
235
|
<DropdownMenuContent align="start">
|
|
235
236
|
{collapsedItems.map((item, index) => (
|
|
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
4
|
const cardVariants = cva(
|
|
5
|
-
'
|
|
5
|
+
'bg-card text-card-foreground border border-border/40 shadow-[0_1px_3px_0_rgb(0_0_0/0.06)] overflow-hidden rounded-xl py-4 text-sm has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
|
|
6
6
|
{
|
|
7
7
|
variants: {
|
|
8
8
|
width: {
|
|
@@ -25,14 +25,18 @@ const cardVariants = cva(
|
|
|
25
25
|
full: 'max-w-full',
|
|
26
26
|
},
|
|
27
27
|
spacing: {
|
|
28
|
-
default: '',
|
|
29
|
-
tight: 'gap-3',
|
|
30
|
-
relaxed: 'gap-6',
|
|
28
|
+
default: 'gap-4 data-[size=sm]:gap-3',
|
|
29
|
+
tight: 'gap-3 data-[size=sm]:gap-2.5',
|
|
30
|
+
relaxed: 'gap-6 data-[size=sm]:gap-4',
|
|
31
|
+
},
|
|
32
|
+
disabled: {
|
|
33
|
+
true: 'opacity-60 pointer-events-none select-none',
|
|
31
34
|
},
|
|
32
35
|
},
|
|
33
36
|
defaultVariants: {
|
|
34
37
|
width: 'auto',
|
|
35
38
|
spacing: 'default',
|
|
39
|
+
disabled: undefined,
|
|
36
40
|
},
|
|
37
41
|
},
|
|
38
42
|
);
|
|
@@ -56,6 +60,8 @@ function Card({
|
|
|
56
60
|
size = 'default',
|
|
57
61
|
width,
|
|
58
62
|
maxWidth,
|
|
63
|
+
spacing,
|
|
64
|
+
disabled,
|
|
59
65
|
title,
|
|
60
66
|
description,
|
|
61
67
|
headerAction,
|
|
@@ -68,6 +74,13 @@ function Card({
|
|
|
68
74
|
// Check if children contain compound components (have data-slot)
|
|
69
75
|
const hasCompoundChildren = React.Children.toArray(children).some((child) => {
|
|
70
76
|
if (React.isValidElement(child)) {
|
|
77
|
+
// Prefer checking component identity. `data-slot` is applied inside the component render,
|
|
78
|
+
// so it won't exist on `child.props` unless manually passed in.
|
|
79
|
+
if (child.type === CardHeader || child.type === CardContent || child.type === CardFooter) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback for direct DOM usage.
|
|
71
84
|
const props = child.props as Record<string, unknown>;
|
|
72
85
|
return (
|
|
73
86
|
props['data-slot'] === 'card-header' ||
|
|
@@ -79,7 +92,13 @@ function Card({
|
|
|
79
92
|
});
|
|
80
93
|
|
|
81
94
|
return (
|
|
82
|
-
<div
|
|
95
|
+
<div
|
|
96
|
+
data-slot="card"
|
|
97
|
+
data-size={size}
|
|
98
|
+
data-disabled={disabled ? '' : undefined}
|
|
99
|
+
className={cardVariants({ width, maxWidth, spacing, disabled: disabled ? true : undefined })}
|
|
100
|
+
{...props}
|
|
101
|
+
>
|
|
83
102
|
{hasHeader && (
|
|
84
103
|
<CardHeader>
|
|
85
104
|
{title && <CardTitle>{title}</CardTitle>}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Search } from '@carbon/icons-react';
|
|
5
|
+
import {
|
|
6
|
+
Command,
|
|
7
|
+
CommandDialog,
|
|
8
|
+
CommandEmpty,
|
|
9
|
+
CommandGroup,
|
|
10
|
+
CommandInput,
|
|
11
|
+
CommandItem,
|
|
12
|
+
CommandList,
|
|
13
|
+
CommandShortcut,
|
|
14
|
+
} from '../organisms/command';
|
|
15
|
+
import { Kbd } from '../atoms/kbd';
|
|
16
|
+
|
|
17
|
+
// ============ TYPES ============
|
|
18
|
+
|
|
19
|
+
export interface CommandSearchItem {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
shortcut?: string;
|
|
24
|
+
onSelect?: () => void;
|
|
25
|
+
keywords?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CommandSearchGroup {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
items: CommandSearchItem[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CommandSearchProps {
|
|
35
|
+
/** Placeholder text for the search input */
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** Text shown when no results are found */
|
|
38
|
+
emptyText?: string;
|
|
39
|
+
/** Groups of searchable items */
|
|
40
|
+
groups?: CommandSearchGroup[];
|
|
41
|
+
/** Flat list of items (alternative to groups) */
|
|
42
|
+
items?: CommandSearchItem[];
|
|
43
|
+
/** Callback when an item is selected */
|
|
44
|
+
onSelect?: (item: CommandSearchItem) => void;
|
|
45
|
+
/** Controlled open state */
|
|
46
|
+
open?: boolean;
|
|
47
|
+
/** Callback when open state changes */
|
|
48
|
+
onOpenChange?: (open: boolean) => void;
|
|
49
|
+
/** Whether to show the trigger input */
|
|
50
|
+
showTrigger?: boolean;
|
|
51
|
+
/** Width of the trigger input */
|
|
52
|
+
triggerWidth?: 'sm' | 'md' | 'lg' | 'full';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============ COMPONENT ============
|
|
56
|
+
|
|
57
|
+
function CommandSearch({
|
|
58
|
+
placeholder = 'Search...',
|
|
59
|
+
emptyText = 'No results found.',
|
|
60
|
+
groups = [],
|
|
61
|
+
items = [],
|
|
62
|
+
onSelect,
|
|
63
|
+
open: openProp,
|
|
64
|
+
onOpenChange,
|
|
65
|
+
showTrigger = true,
|
|
66
|
+
triggerWidth = 'md',
|
|
67
|
+
}: CommandSearchProps) {
|
|
68
|
+
const [_open, _setOpen] = React.useState(false);
|
|
69
|
+
const open = openProp ?? _open;
|
|
70
|
+
const setOpen = onOpenChange ?? _setOpen;
|
|
71
|
+
|
|
72
|
+
// Listen for Cmd+K / Ctrl+K
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
75
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
setOpen(!open);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
82
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
83
|
+
}, [open, setOpen]);
|
|
84
|
+
|
|
85
|
+
const handleSelect = (item: CommandSearchItem) => {
|
|
86
|
+
setOpen(false);
|
|
87
|
+
item.onSelect?.();
|
|
88
|
+
onSelect?.(item);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const widthClasses = {
|
|
92
|
+
sm: 'w-48 md:w-56',
|
|
93
|
+
md: 'w-56 md:w-72',
|
|
94
|
+
lg: 'w-72 md:w-96',
|
|
95
|
+
full: 'w-full max-w-md',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Combine flat items into a default group if provided
|
|
99
|
+
const allGroups = items.length > 0
|
|
100
|
+
? [{ id: 'default', label: '', items }, ...groups]
|
|
101
|
+
: groups;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
{showTrigger && (
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => setOpen(true)}
|
|
109
|
+
className={`${widthClasses[triggerWidth]} inline-flex items-center gap-2 rounded-lg border border-input/50 bg-background/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-background hover:border-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`}
|
|
110
|
+
>
|
|
111
|
+
<Search className="size-4" />
|
|
112
|
+
<span className="flex-1 text-left">{placeholder}</span>
|
|
113
|
+
<Kbd>⌘K</Kbd>
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
118
|
+
<Command>
|
|
119
|
+
<CommandInput placeholder={placeholder} />
|
|
120
|
+
<CommandList>
|
|
121
|
+
<CommandEmpty>{emptyText}</CommandEmpty>
|
|
122
|
+
{allGroups.map((group) => (
|
|
123
|
+
<CommandGroup key={group.id} heading={group.label || undefined}>
|
|
124
|
+
{group.items.map((item) => (
|
|
125
|
+
<CommandItem
|
|
126
|
+
key={item.id}
|
|
127
|
+
value={item.label}
|
|
128
|
+
keywords={item.keywords}
|
|
129
|
+
onSelect={() => handleSelect(item)}
|
|
130
|
+
>
|
|
131
|
+
{item.icon}
|
|
132
|
+
<span>{item.label}</span>
|
|
133
|
+
{item.shortcut && (
|
|
134
|
+
<CommandShortcut>{item.shortcut}</CommandShortcut>
|
|
135
|
+
)}
|
|
136
|
+
</CommandItem>
|
|
137
|
+
))}
|
|
138
|
+
</CommandGroup>
|
|
139
|
+
))}
|
|
140
|
+
</CommandList>
|
|
141
|
+
</Command>
|
|
142
|
+
</CommandDialog>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { CommandSearch };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from './accordion';
|
|
2
|
+
export * from './ai-chat';
|
|
2
3
|
export * from './alert';
|
|
3
4
|
export * from './breadcrumb';
|
|
4
5
|
export * from './button-group';
|
|
5
6
|
export * from './card';
|
|
6
7
|
export * from './collapsible';
|
|
8
|
+
export * from './command-search';
|
|
7
9
|
export * from './empty';
|
|
8
10
|
export * from './field';
|
|
9
11
|
export * from './grid';
|
|
@@ -19,8 +21,9 @@ export * from './resizable';
|
|
|
19
21
|
export * from './scroll-area';
|
|
20
22
|
export * from './section';
|
|
21
23
|
export * from './select';
|
|
22
|
-
export * from './
|
|
24
|
+
export * from './settings';
|
|
23
25
|
export * from './table';
|
|
24
26
|
export * from './tabs';
|
|
27
|
+
export * from './theme-switcher';
|
|
25
28
|
export * from './toggle-group';
|
|
26
29
|
export * from './tooltip';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OTPInput, OTPInputContext } from 'input-otp';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { Subtract } from '@carbon/icons-react';
|
|
5
5
|
|
|
6
6
|
function InputOTP({
|
|
7
7
|
className: _className,
|
|
@@ -62,7 +62,7 @@ function InputOTPSeparator({ ...props }: Omit<React.ComponentProps<'div'>, 'clas
|
|
|
62
62
|
role="separator"
|
|
63
63
|
{...props}
|
|
64
64
|
>
|
|
65
|
-
<
|
|
65
|
+
<Subtract />
|
|
66
66
|
</div>
|
|
67
67
|
);
|
|
68
68
|
}
|
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { Heading } from '../atoms/heading';
|
|
4
4
|
import { Text } from '../atoms/text';
|
|
5
|
-
import { Stack } from '
|
|
5
|
+
import { Stack } from '../atoms/stack';
|
|
6
6
|
|
|
7
7
|
interface PageHeaderProps extends Omit<React.ComponentProps<'div'>, 'className'> {
|
|
8
8
|
title: string;
|
|
@@ -13,8 +13,29 @@ interface PageHeaderProps extends Omit<React.ComponentProps<'div'>, 'className'>
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function PageHeader({ title, description, meta, actions, children, ...props }: PageHeaderProps) {
|
|
16
|
+
const childArray = React.Children.toArray(children);
|
|
17
|
+
const extractedActionChildren: React.ReactNode[] = [];
|
|
18
|
+
const nonActionChildren: React.ReactNode[] = [];
|
|
19
|
+
|
|
20
|
+
childArray.forEach((child) => {
|
|
21
|
+
if (
|
|
22
|
+
React.isValidElement(child) &&
|
|
23
|
+
(child.type === PageHeaderActions ||
|
|
24
|
+
(typeof child.type === 'function' &&
|
|
25
|
+
(child.type as unknown as { __pageHeaderSlot?: string }).__pageHeaderSlot === 'actions'))
|
|
26
|
+
) {
|
|
27
|
+
extractedActionChildren.push((child.props as { children?: React.ReactNode }).children);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
nonActionChildren.push(child);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const resolvedActions =
|
|
34
|
+
actions ??
|
|
35
|
+
(extractedActionChildren.length > 0 ? extractedActionChildren : undefined);
|
|
36
|
+
|
|
16
37
|
return (
|
|
17
|
-
<div data-slot="page-header" className="flex items-
|
|
38
|
+
<div data-slot="page-header" className="flex items-start justify-between gap-4" {...props}>
|
|
18
39
|
<Stack gap="1">
|
|
19
40
|
<Heading level="1">{title}</Heading>
|
|
20
41
|
{description && (
|
|
@@ -27,9 +48,14 @@ function PageHeader({ title, description, meta, actions, children, ...props }: P
|
|
|
27
48
|
{meta}
|
|
28
49
|
</Text>
|
|
29
50
|
)}
|
|
30
|
-
{
|
|
51
|
+
{nonActionChildren}
|
|
31
52
|
</Stack>
|
|
32
|
-
{
|
|
53
|
+
{resolvedActions &&
|
|
54
|
+
(React.isValidElement(resolvedActions) && resolvedActions.type === PageHeaderActions ? (
|
|
55
|
+
resolvedActions
|
|
56
|
+
) : (
|
|
57
|
+
<PageHeaderActions>{resolvedActions}</PageHeaderActions>
|
|
58
|
+
))}
|
|
33
59
|
</div>
|
|
34
60
|
);
|
|
35
61
|
}
|
|
@@ -48,4 +74,7 @@ function PageHeaderActions({ ...props }: Omit<React.ComponentProps<'div'>, 'clas
|
|
|
48
74
|
);
|
|
49
75
|
}
|
|
50
76
|
|
|
77
|
+
// Mark compound slots so PageHeader can detect them even if module instances differ.
|
|
78
|
+
(PageHeaderActions as unknown as { __pageHeaderSlot?: string }).__pageHeaderSlot = 'actions';
|
|
79
|
+
|
|
51
80
|
export { PageHeader, PageHeaderActions, PageHeaderDescription, PageHeaderTitle };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Button as ButtonPrimitive } from '@base-ui/react/button';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ChevronLeft, ChevronRight, OverflowMenuHorizontal } from '@carbon/icons-react';
|
|
4
4
|
import { buttonVariants } from '../atoms/button';
|
|
5
5
|
|
|
6
6
|
function Pagination({ ...props }: Omit<React.ComponentProps<'nav'>, 'className'>) {
|
|
@@ -52,7 +52,7 @@ function PaginationPrevious({ ...props }: Omit<PaginationLinkProps, 'size'>) {
|
|
|
52
52
|
className={`${buttonVariants({ variant: 'ghost', size: 'default' })} pl-2`}
|
|
53
53
|
render={<a aria-label="Go to previous page" data-slot="pagination-link" {...props} />}
|
|
54
54
|
>
|
|
55
|
-
<
|
|
55
|
+
<ChevronLeft data-icon="inline-start" />
|
|
56
56
|
<span className="hidden sm:block">Previous</span>
|
|
57
57
|
</ButtonPrimitive>
|
|
58
58
|
);
|
|
@@ -65,7 +65,7 @@ function PaginationNext({ ...props }: Omit<PaginationLinkProps, 'size'>) {
|
|
|
65
65
|
render={<a aria-label="Go to next page" data-slot="pagination-link" {...props} />}
|
|
66
66
|
>
|
|
67
67
|
<span className="hidden sm:block">Next</span>
|
|
68
|
-
<
|
|
68
|
+
<ChevronRight data-icon="inline-end" />
|
|
69
69
|
</ButtonPrimitive>
|
|
70
70
|
);
|
|
71
71
|
}
|
|
@@ -78,7 +78,7 @@ function PaginationEllipsis({ ...props }: Omit<React.ComponentProps<'span'>, 'cl
|
|
|
78
78
|
className="size-9 items-center justify-center [&_svg:not([class*='size-'])]:size-4 flex items-center justify-center"
|
|
79
79
|
{...props}
|
|
80
80
|
>
|
|
81
|
-
<
|
|
81
|
+
<OverflowMenuHorizontal />
|
|
82
82
|
<span className="sr-only">More pages</span>
|
|
83
83
|
</span>
|
|
84
84
|
);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Popover as PopoverPrimitive } from '@base-ui/react/popover';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
|
+
import { cn } from '../../../lib/utils';
|
|
5
|
+
|
|
4
6
|
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
|
5
7
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
|
9
|
-
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
|
10
|
+
function PopoverTrigger({ className, ...props }: PopoverPrimitive.Trigger.Props) {
|
|
11
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" className={cn(className)} {...props} />;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
function PopoverContent({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Radio as RadioPrimitive } from '@base-ui/react/radio';
|
|
2
2
|
import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { CircleFilled } from '@carbon/icons-react';
|
|
5
5
|
|
|
6
6
|
function RadioGroup({ ...props }: Omit<RadioGroupPrimitive.Props, 'className'>) {
|
|
7
7
|
return (
|
|
@@ -24,7 +24,7 @@ function RadioGroupItem({ ...props }: Omit<RadioPrimitive.Root.Props, 'className
|
|
|
24
24
|
data-slot="radio-group-indicator"
|
|
25
25
|
className="group-aria-invalid/radio-group-item:text-destructive text-primary flex size-4 items-center justify-center"
|
|
26
26
|
>
|
|
27
|
-
<
|
|
27
|
+
<CircleFilled className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
|
28
28
|
</RadioPrimitive.Indicator>
|
|
29
29
|
</RadioPrimitive.Root>
|
|
30
30
|
);
|
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { Heading } from '../atoms/heading';
|
|
4
4
|
import { Text } from '../atoms/text';
|
|
5
|
-
import { Stack } from '
|
|
5
|
+
import { Stack } from '../atoms/stack';
|
|
6
6
|
|
|
7
7
|
interface SectionProps extends Omit<React.ComponentProps<'section'>, 'className'> {
|
|
8
8
|
title?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Select as SelectPrimitive } from '@base-ui/react/select';
|
|
4
|
-
import {
|
|
4
|
+
import { Checkmark, ChevronDown, ChevronUp } from '@carbon/icons-react';
|
|
5
5
|
import * as React from 'react';
|
|
6
6
|
|
|
7
7
|
const Select = SelectPrimitive.Root;
|
|
@@ -52,7 +52,7 @@ function SelectTrigger({
|
|
|
52
52
|
>
|
|
53
53
|
{children}
|
|
54
54
|
<SelectPrimitive.Icon
|
|
55
|
-
render={<
|
|
55
|
+
render={<ChevronDown className="text-muted-foreground size-4 pointer-events-none" />}
|
|
56
56
|
/>
|
|
57
57
|
</SelectPrimitive.Trigger>
|
|
58
58
|
);
|
|
@@ -120,7 +120,7 @@ function SelectItem({ children, ...props }: Omit<SelectPrimitive.Item.Props, 'cl
|
|
|
120
120
|
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
|
121
121
|
}
|
|
122
122
|
>
|
|
123
|
-
<
|
|
123
|
+
<Checkmark className="pointer-events-none" />
|
|
124
124
|
</SelectPrimitive.ItemIndicator>
|
|
125
125
|
</SelectPrimitive.Item>
|
|
126
126
|
);
|
|
@@ -145,7 +145,7 @@ function SelectScrollUpButton({
|
|
|
145
145
|
className="bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full"
|
|
146
146
|
{...props}
|
|
147
147
|
>
|
|
148
|
-
<
|
|
148
|
+
<ChevronUp />
|
|
149
149
|
</SelectPrimitive.ScrollUpArrow>
|
|
150
150
|
);
|
|
151
151
|
}
|
|
@@ -159,7 +159,7 @@ function SelectScrollDownButton({
|
|
|
159
159
|
className="bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full"
|
|
160
160
|
{...props}
|
|
161
161
|
>
|
|
162
|
-
<
|
|
162
|
+
<ChevronDown />
|
|
163
163
|
</SelectPrimitive.ScrollDownArrow>
|
|
164
164
|
);
|
|
165
165
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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 Omit<React.ComponentProps<'div'>, 'className'>,
|
|
26
|
+
VariantProps<typeof settingRowVariants> {
|
|
27
|
+
/** The setting label */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Optional description text */
|
|
30
|
+
description?: string;
|
|
31
|
+
/** Whether the setting is disabled */
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A horizontal row for a single setting with label/description on the left
|
|
37
|
+
* and a control (switch, button, select, etc.) on the right.
|
|
38
|
+
*/
|
|
39
|
+
function SettingRow({
|
|
40
|
+
label,
|
|
41
|
+
description,
|
|
42
|
+
disabled,
|
|
43
|
+
size,
|
|
44
|
+
children,
|
|
45
|
+
...props
|
|
46
|
+
}: SettingRowProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="setting-row"
|
|
50
|
+
data-disabled={disabled || undefined}
|
|
51
|
+
className={settingRowVariants({ size })}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
<div className="min-w-0 flex-1">
|
|
55
|
+
<Stack gap="1">
|
|
56
|
+
<Text weight="medium" variant={disabled ? 'muted' : undefined}>
|
|
57
|
+
{label}
|
|
58
|
+
</Text>
|
|
59
|
+
{description && (
|
|
60
|
+
<Text size="sm" variant="muted">
|
|
61
|
+
{description}
|
|
62
|
+
</Text>
|
|
63
|
+
)}
|
|
64
|
+
</Stack>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex shrink-0 items-center">{children}</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface SettingGroupProps extends Omit<React.ComponentProps<'div'>, 'className'> {
|
|
72
|
+
/** Whether to show dividers between rows */
|
|
73
|
+
divided?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Groups multiple SettingRow components together, optionally with dividers.
|
|
78
|
+
*/
|
|
79
|
+
function SettingGroup({ divided = true, children, ...props }: SettingGroupProps) {
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
data-slot="setting-group"
|
|
83
|
+
data-divided={divided || undefined}
|
|
84
|
+
className={
|
|
85
|
+
divided
|
|
86
|
+
? '[&>[data-slot=setting-row]:not(:last-child)]:border-border [&>[data-slot=setting-row]:not(:last-child)]:border-b'
|
|
87
|
+
: ''
|
|
88
|
+
}
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface SettingLabelProps extends Omit<React.ComponentProps<'div'>, 'className'> {}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Custom label area for complex setting rows. Use when you need more than just text.
|
|
100
|
+
*/
|
|
101
|
+
function SettingLabel({ children, ...props }: SettingLabelProps) {
|
|
102
|
+
return (
|
|
103
|
+
<div data-slot="setting-label" className="min-w-0 flex-1" {...props}>
|
|
104
|
+
{children}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SettingControlProps extends Omit<React.ComponentProps<'div'>, 'className'> {}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Control area for setting rows. Use when you need multiple controls or custom layout.
|
|
113
|
+
*/
|
|
114
|
+
function SettingControl({ children, ...props }: SettingControlProps) {
|
|
115
|
+
return (
|
|
116
|
+
<div data-slot="setting-control" className="flex shrink-0 items-center gap-2" {...props}>
|
|
117
|
+
{children}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface SettingsCardProps extends Omit<React.ComponentProps<'div'>, 'className'> {
|
|
123
|
+
/** Card title */
|
|
124
|
+
title: string;
|
|
125
|
+
/** Optional description */
|
|
126
|
+
description?: string;
|
|
127
|
+
/** Hint text shown in footer (left side) */
|
|
128
|
+
hint?: React.ReactNode;
|
|
129
|
+
/** Action button/content shown in footer (right side) */
|
|
130
|
+
action?: React.ReactNode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A self-contained settings card with title, description, content area, and footer.
|
|
135
|
+
* Footer shows hint text on left and action button on right.
|
|
136
|
+
*/
|
|
137
|
+
function SettingsCard({ title, description, hint, action, children, ...props }: SettingsCardProps) {
|
|
138
|
+
const hasFooter = hint || action;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
data-slot="settings-card"
|
|
143
|
+
className="bg-card text-card-foreground rounded-xl border shadow-sm"
|
|
144
|
+
{...props}
|
|
145
|
+
>
|
|
146
|
+
<div className="px-6 pt-6 pb-4">
|
|
147
|
+
<Stack gap="1">
|
|
148
|
+
<Text weight="semibold">{title}</Text>
|
|
149
|
+
{description && (
|
|
150
|
+
<Text size="sm" variant="muted">
|
|
151
|
+
{description}
|
|
152
|
+
</Text>
|
|
153
|
+
)}
|
|
154
|
+
</Stack>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="px-6 pb-6">{children}</div>
|
|
157
|
+
{hasFooter && (
|
|
158
|
+
<div className="border-t bg-muted/30 px-6 py-4">
|
|
159
|
+
<div className="flex items-center justify-between gap-4">
|
|
160
|
+
<div className="text-muted-foreground text-sm">{hint}</div>
|
|
161
|
+
<div className="flex shrink-0 items-center gap-2">{action}</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { SettingControl, SettingGroup, SettingLabel, SettingRow, SettingsCard };
|
|
@@ -12,7 +12,11 @@ function Table({
|
|
|
12
12
|
data-variant={variant}
|
|
13
13
|
className="relative w-full overflow-x-auto data-[variant=bordered]:border data-[variant=bordered]:rounded-lg"
|
|
14
14
|
>
|
|
15
|
-
<table
|
|
15
|
+
<table
|
|
16
|
+
data-slot="table"
|
|
17
|
+
className="w-full caption-bottom text-sm [&_[data-slot=text][data-default-size=true]]:text-sm"
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
16
20
|
</div>
|
|
17
21
|
);
|
|
18
22
|
}
|