@trycompai/design-system 1.0.1 → 1.0.3

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 (65) hide show
  1. package/package.json +11 -3
  2. package/src/components/atoms/badge.tsx +8 -7
  3. package/src/components/atoms/button.tsx +6 -1
  4. package/src/components/atoms/checkbox.tsx +3 -3
  5. package/src/components/atoms/heading.tsx +6 -6
  6. package/src/components/atoms/index.ts +2 -0
  7. package/src/components/atoms/logo.tsx +52 -0
  8. package/src/components/atoms/spinner.tsx +3 -3
  9. package/src/components/atoms/stack.tsx +97 -0
  10. package/src/components/atoms/switch.tsx +1 -1
  11. package/src/components/atoms/text.tsx +5 -1
  12. package/src/components/atoms/toggle.tsx +1 -1
  13. package/src/components/molecules/accordion.tsx +3 -3
  14. package/src/components/molecules/ai-chat.tsx +217 -0
  15. package/src/components/molecules/alert.tsx +5 -5
  16. package/src/components/molecules/breadcrumb.tsx +8 -7
  17. package/src/components/molecules/card.tsx +24 -5
  18. package/src/components/molecules/command-search.tsx +147 -0
  19. package/src/components/molecules/index.ts +4 -1
  20. package/src/components/molecules/input-otp.tsx +2 -2
  21. package/src/components/molecules/page-header.tsx +33 -4
  22. package/src/components/molecules/pagination.tsx +4 -4
  23. package/src/components/molecules/popover.tsx +4 -2
  24. package/src/components/molecules/radio-group.tsx +2 -2
  25. package/src/components/molecules/section.tsx +1 -1
  26. package/src/components/molecules/select.tsx +5 -5
  27. package/src/components/molecules/settings.tsx +169 -0
  28. package/src/components/molecules/table.tsx +5 -1
  29. package/src/components/molecules/tabs.tsx +5 -4
  30. package/src/components/molecules/theme-switcher.tsx +176 -0
  31. package/src/components/organisms/app-shell.tsx +822 -0
  32. package/src/components/organisms/calendar.tsx +4 -4
  33. package/src/components/organisms/carousel.tsx +3 -3
  34. package/src/components/organisms/combobox.tsx +5 -5
  35. package/src/components/organisms/command.tsx +3 -3
  36. package/src/components/organisms/context-menu.tsx +4 -4
  37. package/src/components/organisms/dialog.tsx +2 -2
  38. package/src/components/organisms/dropdown-menu.tsx +8 -6
  39. package/src/components/organisms/index.ts +1 -0
  40. package/src/components/organisms/menubar.tsx +3 -3
  41. package/src/components/organisms/navigation-menu.tsx +2 -2
  42. package/src/components/organisms/page-layout.tsx +50 -20
  43. package/src/components/organisms/sheet.tsx +2 -2
  44. package/src/components/organisms/sidebar.tsx +22 -6
  45. package/src/components/organisms/sonner.tsx +11 -11
  46. package/src/fonts/TWKLausanne/TWKLausanne-300-Italic.woff +0 -0
  47. package/src/fonts/TWKLausanne/TWKLausanne-300-Italic.woff2 +0 -0
  48. package/src/fonts/TWKLausanne/TWKLausanne-300.woff +0 -0
  49. package/src/fonts/TWKLausanne/TWKLausanne-300.woff2 +0 -0
  50. package/src/fonts/TWKLausanne/TWKLausanne-350-Italic.woff +0 -0
  51. package/src/fonts/TWKLausanne/TWKLausanne-350-Italic.woff2 +0 -0
  52. package/src/fonts/TWKLausanne/TWKLausanne-350.woff +0 -0
  53. package/src/fonts/TWKLausanne/TWKLausanne-350.woff2 +0 -0
  54. package/src/fonts/TWKLausanne/TWKLausanne-400-Italic.woff +0 -0
  55. package/src/fonts/TWKLausanne/TWKLausanne-400-Italic.woff2 +0 -0
  56. package/src/fonts/TWKLausanne/TWKLausanne-400.woff +0 -0
  57. package/src/fonts/TWKLausanne/TWKLausanne-400.woff2 +0 -0
  58. package/src/fonts/TWKLausanne/TWKLausanne-700-Italic.woff +0 -0
  59. package/src/fonts/TWKLausanne/TWKLausanne-700-Italic.woff2 +0 -0
  60. package/src/fonts/TWKLausanne/TWKLausanne-700.woff +0 -0
  61. package/src/fonts/TWKLausanne/TWKLausanne-700.woff2 +0 -0
  62. package/src/icons.ts +2 -0
  63. package/src/index.ts +2 -2
  64. package/src/styles/globals.css +155 -23
  65. 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 { ArrowRightIcon, ChevronRightIcon, MoreHorizontalIcon, SlashIcon } from 'lucide-react';
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: <ChevronRightIcon />,
30
- slash: <SlashIcon />,
31
- arrow: <ArrowRightIcon />,
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 ?? <ChevronRightIcon />}
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
- <MoreHorizontalIcon />
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
- <MoreHorizontalIcon />
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
- 'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 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',
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 data-slot="card" data-size={size} className={cardVariants({ width, maxWidth })} {...props}>
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 './stack';
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 { MinusIcon } from 'lucide-react';
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
- <MinusIcon />
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 './stack';
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-center justify-between gap-4" {...props}>
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
- {children}
51
+ {nonActionChildren}
31
52
  </Stack>
32
- {actions && <div className="flex shrink-0 items-center gap-3">{actions}</div>}
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 { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
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
- <ChevronLeftIcon data-icon="inline-start" />
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
- <ChevronRightIcon data-icon="inline-end" />
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
- <MoreHorizontalIcon />
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 { CircleIcon } from 'lucide-react';
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
- <CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-current" />
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 './stack';
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 { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
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={<ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />}
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
- <CheckIcon className="pointer-events-none" />
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
- <ChevronUpIcon />
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
- <ChevronDownIcon />
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 data-slot="table" className="w-full caption-bottom text-sm" {...props} />
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
  }