@windforge/ui 0.1.0

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 (61) hide show
  1. package/dist/index.d.ts +1195 -0
  2. package/dist/index.js +3628 -0
  3. package/package.json +66 -0
  4. package/src/catalog.ts +654 -0
  5. package/src/components/accordion.tsx +91 -0
  6. package/src/components/alert.tsx +58 -0
  7. package/src/components/autocomplete.tsx +174 -0
  8. package/src/components/avatar.tsx +60 -0
  9. package/src/components/badge.tsx +37 -0
  10. package/src/components/breadcrumb.tsx +62 -0
  11. package/src/components/button-group.tsx +23 -0
  12. package/src/components/button.tsx +53 -0
  13. package/src/components/calendar.tsx +61 -0
  14. package/src/components/card.tsx +72 -0
  15. package/src/components/chart.tsx +130 -0
  16. package/src/components/checkbox.tsx +27 -0
  17. package/src/components/chip.tsx +75 -0
  18. package/src/components/code-block.tsx +126 -0
  19. package/src/components/command.tsx +139 -0
  20. package/src/components/data-table.tsx +194 -0
  21. package/src/components/date-picker.tsx +77 -0
  22. package/src/components/dialog.tsx +57 -0
  23. package/src/components/dropdown-menu.tsx +186 -0
  24. package/src/components/form-field.tsx +97 -0
  25. package/src/components/input.tsx +29 -0
  26. package/src/components/label.tsx +18 -0
  27. package/src/components/layout.tsx +179 -0
  28. package/src/components/link.tsx +37 -0
  29. package/src/components/modal.tsx +67 -0
  30. package/src/components/multi-select.tsx +175 -0
  31. package/src/components/pagination.tsx +72 -0
  32. package/src/components/popover.tsx +25 -0
  33. package/src/components/progress.tsx +31 -0
  34. package/src/components/radio-group.tsx +34 -0
  35. package/src/components/select.tsx +134 -0
  36. package/src/components/separator.tsx +21 -0
  37. package/src/components/sheet.tsx +80 -0
  38. package/src/components/skeleton.tsx +11 -0
  39. package/src/components/slider.tsx +28 -0
  40. package/src/components/stepper.tsx +69 -0
  41. package/src/components/switch.tsx +33 -0
  42. package/src/components/table.tsx +121 -0
  43. package/src/components/tabs.tsx +90 -0
  44. package/src/components/text.tsx +109 -0
  45. package/src/components/textarea.tsx +27 -0
  46. package/src/components/toast.tsx +107 -0
  47. package/src/components/toggle-button.tsx +103 -0
  48. package/src/components/tooltip.tsx +26 -0
  49. package/src/icons/forge-icon.tsx +55 -0
  50. package/src/icons/icon-set.ts +60 -0
  51. package/src/icons/svg-icon.tsx +43 -0
  52. package/src/index.ts +80 -0
  53. package/src/layouts/app-bar.tsx +95 -0
  54. package/src/layouts/app-shell.tsx +80 -0
  55. package/src/layouts/side-nav.tsx +196 -0
  56. package/src/layouts/theme-provider.tsx +128 -0
  57. package/src/lib/recipes.ts +50 -0
  58. package/src/lib/types.ts +3 -0
  59. package/src/lib/use-media-query.ts +18 -0
  60. package/src/lib/utils.ts +10 -0
  61. package/tailwind-preset.cjs +77 -0
@@ -0,0 +1,134 @@
1
+ import * as React from 'react'
2
+ import * as SelectPrimitive from '@radix-ui/react-select'
3
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
4
+ import { cn } from '../lib/utils'
5
+ import type { NoStyle } from '../lib/types'
6
+ import { floatingPanel, menuItem } from '../lib/recipes'
7
+
8
+ export const SelectGroup = SelectPrimitive.Group
9
+ export const SelectValue = SelectPrimitive.Value
10
+
11
+ /**
12
+ * Select — pass an `options` array for the common case (the component renders
13
+ * the trigger, value, and items), or compose `SelectTrigger`/`SelectContent`/
14
+ * `SelectItem` by hand for groups, labels, or custom triggers. Value props
15
+ * (`value`/`defaultValue`/`onValueChange`) pass through to the Radix root.
16
+ *
17
+ * <Select placeholder="Pick one" options={[
18
+ * { value: 'a', label: 'Apple' },
19
+ * { value: 'b', label: 'Banana' },
20
+ * ]} />
21
+ */
22
+ export interface SelectOption {
23
+ value: string
24
+ label: React.ReactNode
25
+ disabled?: boolean
26
+ }
27
+
28
+ export interface SelectProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> {
29
+ /** Declarative options. Omit to compose the primitives as children instead. */
30
+ options?: SelectOption[]
31
+ placeholder?: string
32
+ /** Error state — red trigger outline + aria-invalid. Usually set by FormField. */
33
+ invalid?: boolean
34
+ /** Forwarded to the trigger so FormField can wire label/description/error. */
35
+ id?: string
36
+ 'aria-describedby'?: string
37
+ }
38
+
39
+ export function Select({ options, placeholder, invalid, id, children, ...props }: SelectProps) {
40
+ if (!options) return <SelectPrimitive.Root {...props}>{children}</SelectPrimitive.Root>
41
+ return (
42
+ <SelectPrimitive.Root {...props}>
43
+ <SelectTrigger invalid={invalid} id={id} aria-describedby={props['aria-describedby']}>
44
+ <SelectValue placeholder={placeholder} />
45
+ </SelectTrigger>
46
+ <SelectContent>
47
+ {options.map((o) => (
48
+ <SelectItem key={o.value} value={o.value} disabled={o.disabled}>{o.label}</SelectItem>
49
+ ))}
50
+ </SelectContent>
51
+ </SelectPrimitive.Root>
52
+ )
53
+ }
54
+
55
+ export const SelectTrigger = React.forwardRef<
56
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
57
+ NoStyle<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>> & { invalid?: boolean }
58
+ >(({ children, invalid, ...props }, ref) => (
59
+ <SelectPrimitive.Trigger
60
+ ref={ref}
61
+ aria-invalid={invalid || undefined}
62
+ className={cn(
63
+ 'flex h-10 w-full items-center justify-between gap-2 rounded-lg border border-strong bg-surface px-3 py-2 text-sm text-primary',
64
+ 'placeholder:text-tertiary focus:outline-none focus:border-focus focus:ring-2 focus:ring-ring',
65
+ 'disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 data-[placeholder]:text-tertiary',
66
+ invalid && 'border-error focus:border-error',
67
+ )}
68
+ {...props}
69
+ >
70
+ {children}
71
+ <SelectPrimitive.Icon asChild>
72
+ <ChevronDown className="h-4 w-4 opacity-60" />
73
+ </SelectPrimitive.Icon>
74
+ </SelectPrimitive.Trigger>
75
+ ))
76
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
77
+
78
+ export const SelectContent = React.forwardRef<
79
+ React.ElementRef<typeof SelectPrimitive.Content>,
80
+ NoStyle<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>>
81
+ >(({ children, position = 'popper', ...props }, ref) => (
82
+ <SelectPrimitive.Portal>
83
+ <SelectPrimitive.Content
84
+ ref={ref}
85
+ position={position}
86
+ className={cn(
87
+ floatingPanel,
88
+ 'relative max-h-96 min-w-32 overflow-hidden animate-scale-in',
89
+ position === 'popper' && 'data-[side=bottom]:translate-y-1',
90
+ )}
91
+ {...props}
92
+ >
93
+ <SelectPrimitive.ScrollUpButton className="flex items-center justify-center py-1">
94
+ <ChevronUp className="h-4 w-4" />
95
+ </SelectPrimitive.ScrollUpButton>
96
+ <SelectPrimitive.Viewport
97
+ className={cn('p-1', position === 'popper' && 'w-full min-w-[var(--radix-select-trigger-width)]')}
98
+ >
99
+ {children}
100
+ </SelectPrimitive.Viewport>
101
+ <SelectPrimitive.ScrollDownButton className="flex items-center justify-center py-1">
102
+ <ChevronDown className="h-4 w-4" />
103
+ </SelectPrimitive.ScrollDownButton>
104
+ </SelectPrimitive.Content>
105
+ </SelectPrimitive.Portal>
106
+ ))
107
+ SelectContent.displayName = SelectPrimitive.Content.displayName
108
+
109
+ export const SelectLabel = React.forwardRef<
110
+ React.ElementRef<typeof SelectPrimitive.Label>,
111
+ NoStyle<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>
112
+ >(({ ...props }, ref) => (
113
+ <SelectPrimitive.Label ref={ref} className={cn('px-2 py-1.5 text-sm font-semibold text-tertiary')} {...props} />
114
+ ))
115
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
116
+
117
+ export const SelectItem = React.forwardRef<
118
+ React.ElementRef<typeof SelectPrimitive.Item>,
119
+ NoStyle<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>
120
+ >(({ children, ...props }, ref) => (
121
+ <SelectPrimitive.Item
122
+ ref={ref}
123
+ className={cn(menuItem, 'w-full py-1.5 pl-8 pr-2 focus:bg-surface-inset focus:text-primary')}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <SelectPrimitive.ItemIndicator>
128
+ <Check className="h-4 w-4" />
129
+ </SelectPrimitive.ItemIndicator>
130
+ </span>
131
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
132
+ </SelectPrimitive.Item>
133
+ ))
134
+ SelectItem.displayName = SelectPrimitive.Item.displayName
@@ -0,0 +1,21 @@
1
+ import * as React from 'react'
2
+ import * as SeparatorPrimitive from '@radix-ui/react-separator'
3
+ import { cn } from '../lib/utils'
4
+ import type { NoStyle } from '../lib/types'
5
+
6
+ export const Separator = React.forwardRef<
7
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
8
+ NoStyle<React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>
9
+ >(({ orientation = 'horizontal', decorative = true, ...props }, ref) => (
10
+ <SeparatorPrimitive.Root
11
+ ref={ref}
12
+ decorative={decorative}
13
+ orientation={orientation}
14
+ className={cn(
15
+ 'shrink-0 bg-border',
16
+ orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ Separator.displayName = SeparatorPrimitive.Root.displayName
@@ -0,0 +1,80 @@
1
+ import * as React from 'react'
2
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { X } from 'lucide-react'
5
+ import { cn } from '../lib/utils'
6
+ import type { NoStyle } from '../lib/types'
7
+ import { overlayBackdrop, dismissButton, focusRingInset } from '../lib/recipes'
8
+
9
+ export const Sheet = DialogPrimitive.Root
10
+ export const SheetTrigger = DialogPrimitive.Trigger
11
+ export const SheetClose = DialogPrimitive.Close
12
+ export const SheetPortal = DialogPrimitive.Portal
13
+
14
+ const SheetOverlay = React.forwardRef<
15
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
16
+ NoStyle<React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>>
17
+ >(({ ...props }, ref) => (
18
+ <DialogPrimitive.Overlay ref={ref} className={cn(overlayBackdrop)} {...props} />
19
+ ))
20
+ SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
21
+
22
+ const sheetVariants = cva(
23
+ 'fixed z-50 gap-4 bg-surface shadow-xl transition ease-in-out',
24
+ {
25
+ variants: {
26
+ side: {
27
+ top: 'inset-x-0 top-0 border-b border-border',
28
+ bottom: 'inset-x-0 bottom-0 border-t border-border',
29
+ left: 'inset-y-0 left-0 h-full w-3/4 max-w-sm border-r border-border',
30
+ right: 'inset-y-0 right-0 h-full w-3/4 max-w-sm border-l border-border',
31
+ },
32
+ },
33
+ defaultVariants: { side: 'right' },
34
+ },
35
+ )
36
+
37
+ /**
38
+ * SheetContent — the sliding panel. Pass `title`, `description`, and `footer`
39
+ * actions as props (no header/title/footer sub-components to assemble); the
40
+ * body goes in `children`. `title`/`description` render as accessible Radix
41
+ * Dialog nodes.
42
+ */
43
+ export interface SheetContentProps
44
+ extends Omit<NoStyle<React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>>, 'title'>,
45
+ VariantProps<typeof sheetVariants> {
46
+ title?: React.ReactNode
47
+ description?: React.ReactNode
48
+ /** Footer content pinned to the bottom — typically buttons. */
49
+ footer?: React.ReactNode
50
+ }
51
+
52
+ export const SheetContent = React.forwardRef<
53
+ React.ElementRef<typeof DialogPrimitive.Content>,
54
+ SheetContentProps
55
+ >(({ side = 'right', title, description, footer, children, ...props }, ref) => (
56
+ <SheetPortal>
57
+ <SheetOverlay />
58
+ <DialogPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), 'flex flex-col p-6 animate-fade-in')} {...props}>
59
+ {(title != null || description != null) && (
60
+ <div className={cn('flex flex-col gap-1.5')}>
61
+ {title != null && (
62
+ <DialogPrimitive.Title className={cn('text-lg font-semibold text-primary')}>{title}</DialogPrimitive.Title>
63
+ )}
64
+ {description != null && (
65
+ <DialogPrimitive.Description className={cn('text-sm text-primary')}>{description}</DialogPrimitive.Description>
66
+ )}
67
+ </div>
68
+ )}
69
+ {children}
70
+ {footer != null && <div className={cn('mt-auto flex flex-col gap-2')}>{footer}</div>}
71
+ <DialogPrimitive.Close
72
+ className={cn(dismissButton, 'focus:outline-none', focusRingInset)}
73
+ aria-label="Close"
74
+ >
75
+ <X className="h-4 w-4" />
76
+ </DialogPrimitive.Close>
77
+ </DialogPrimitive.Content>
78
+ </SheetPortal>
79
+ ))
80
+ SheetContent.displayName = DialogPrimitive.Content.displayName
@@ -0,0 +1,11 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../lib/utils'
3
+ import type { NoStyle } from '../lib/types'
4
+
5
+ /**
6
+ * Skeleton — a fluid loading placeholder. It fills its parent (`h-full w-full`),
7
+ * so you size it by wrapping it in a Box: `<Box className="h-4 w-2/3"><Skeleton/></Box>`.
8
+ */
9
+ export function Skeleton({ ...props }: NoStyle<React.HTMLAttributes<HTMLDivElement>>) {
10
+ return <div className={cn('h-full w-full animate-pulse rounded-md bg-surface-inset')} {...props} />
11
+ }
@@ -0,0 +1,28 @@
1
+ import * as React from 'react'
2
+ import * as SliderPrimitive from '@radix-ui/react-slider'
3
+ import { cn } from '../lib/utils'
4
+ import type { NoStyle } from '../lib/types'
5
+ import { focusRing } from '../lib/recipes'
6
+
7
+ export const Slider = React.forwardRef<
8
+ React.ElementRef<typeof SliderPrimitive.Root>,
9
+ NoStyle<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>>
10
+ >(({ ...props }, ref) => (
11
+ <SliderPrimitive.Root
12
+ ref={ref}
13
+ className={cn('relative flex w-full touch-none select-none items-center')}
14
+ {...props}
15
+ >
16
+ <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-surface-track">
17
+ <SliderPrimitive.Range className="absolute h-full bg-surface-inverse" />
18
+ </SliderPrimitive.Track>
19
+ <SliderPrimitive.Thumb
20
+ className={cn(
21
+ 'block h-5 w-5 rounded-full border-2 border-strong bg-surface shadow-sm transition-colors',
22
+ focusRing,
23
+ 'disabled:pointer-events-none disabled:opacity-50',
24
+ )}
25
+ />
26
+ </SliderPrimitive.Root>
27
+ ))
28
+ Slider.displayName = SliderPrimitive.Root.displayName
@@ -0,0 +1,69 @@
1
+ import * as React from 'react'
2
+ import { Check } from 'lucide-react'
3
+ import { cn } from '../lib/utils'
4
+ import type { NoStyle } from '../lib/types'
5
+
6
+ /**
7
+ * Stepper — progress through a sequence of steps. `activeStep` is the 0-based
8
+ * index of the current step; earlier steps render as completed (✓). Horizontal
9
+ * by default; pass `orientation="vertical"` for a stacked flow.
10
+ */
11
+ export interface Step {
12
+ label: string
13
+ description?: string
14
+ }
15
+ export interface StepperProps extends NoStyle<React.HTMLAttributes<HTMLOListElement>> {
16
+ steps: Step[]
17
+ activeStep: number
18
+ orientation?: 'horizontal' | 'vertical'
19
+ }
20
+
21
+ function StepDot({ state, stepNumber }: { state: 'complete' | 'active' | 'upcoming'; stepNumber: number }) {
22
+ return (
23
+ <span
24
+ className={cn(
25
+ 'grid size-8 shrink-0 place-items-center rounded-full border text-sm font-semibold transition-colors [&_svg]:size-4',
26
+ state === 'complete' && 'border-transparent bg-surface-inverse text-inverse',
27
+ state === 'active' && 'border-strong bg-surface-inset text-primary',
28
+ state === 'upcoming' && 'border-strong bg-surface text-tertiary',
29
+ )}
30
+ >
31
+ {state === 'complete' ? <Check /> : stepNumber}
32
+ </span>
33
+ )
34
+ }
35
+
36
+ export function Stepper({ steps, activeStep, orientation = 'horizontal', ...props }: StepperProps) {
37
+ const vertical = orientation === 'vertical'
38
+ const stateOf = (index: number) => (index < activeStep ? 'complete' : index === activeStep ? 'active' : 'upcoming')
39
+
40
+ return (
41
+ <ol className={cn(vertical ? 'flex flex-col' : 'flex items-start')} {...props}>
42
+ {steps.map((step, index) => {
43
+ const state = stateOf(index)
44
+ const isLast = index === steps.length - 1
45
+ return (
46
+ <li
47
+ key={step.label}
48
+ aria-current={state === 'active' ? 'step' : undefined}
49
+ className={cn('flex', vertical ? 'gap-3' : 'flex-1 items-start', !isLast && !vertical && 'pr-2')}
50
+ >
51
+ <div className={cn('flex', vertical ? 'flex-col items-center' : 'flex-col items-center', vertical && 'self-stretch')}>
52
+ <StepDot state={state} stepNumber={index + 1} />
53
+ {!isLast && vertical && (
54
+ <span className={cn('my-1 w-px flex-1', index < activeStep ? 'bg-surface-inverse' : 'bg-border')} />
55
+ )}
56
+ </div>
57
+ <div className={cn(vertical ? 'pb-6 pt-1' : 'mt-2 text-center', vertical ? 'text-left' : 'flex-1')}>
58
+ <div className={cn('text-sm font-medium', state === 'upcoming' ? 'text-secondary' : 'text-primary')}>{step.label}</div>
59
+ {step.description && <div className="text-sm text-tertiary">{step.description}</div>}
60
+ </div>
61
+ {!isLast && !vertical && (
62
+ <span className={cn('mt-4 h-px flex-1', index < activeStep ? 'bg-surface-inverse' : 'bg-border')} />
63
+ )}
64
+ </li>
65
+ )
66
+ })}
67
+ </ol>
68
+ )
69
+ }
@@ -0,0 +1,33 @@
1
+ import * as React from 'react'
2
+ import * as SwitchPrimitives from '@radix-ui/react-switch'
3
+ import { cn } from '../lib/utils'
4
+ import type { NoStyle } from '../lib/types'
5
+ import { focusRing } from '../lib/recipes'
6
+
7
+ export const Switch = React.forwardRef<
8
+ React.ElementRef<typeof SwitchPrimitives.Root>,
9
+ NoStyle<React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>
10
+ >(({ ...props }, ref) => (
11
+ <SwitchPrimitives.Root
12
+ ref={ref}
13
+ className={cn(
14
+ 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors',
15
+ focusRing,
16
+ 'disabled:cursor-not-allowed disabled:opacity-50',
17
+ // Selected state matches every other control (neutral inverse fill), not a hue.
18
+ 'data-[state=checked]:bg-surface-inverse data-[state=unchecked]:bg-surface-track',
19
+ )}
20
+ {...props}
21
+ >
22
+ {/* Knob is white on the translucent OFF track (stays visible in dark mode); on
23
+ the inverse ON track it flips to the contrasting tone so it reads in both modes. */}
24
+ <SwitchPrimitives.Thumb
25
+ className={cn(
26
+ 'pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm ring-0 transition-transform',
27
+ 'data-[state=checked]:bg-inverse',
28
+ 'data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
29
+ )}
30
+ />
31
+ </SwitchPrimitives.Root>
32
+ ))
33
+ Switch.displayName = SwitchPrimitives.Root.displayName
@@ -0,0 +1,121 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../lib/utils'
3
+ import type { NoStyle } from '../lib/types'
4
+
5
+ const alignClass = { left: 'text-left', center: 'text-center', right: 'text-right' } as const
6
+ type Align = keyof typeof alignClass
7
+
8
+ /**
9
+ * Table — pass `columns` + `data` for the common case (the component renders the
10
+ * header, rows, and cells), or compose `TableHeader`/`TableRow`/`TableCell` by
11
+ * hand for full control. Each column's `accessor` is a row key or a render
12
+ * function for custom cells (badges, buttons, …).
13
+ *
14
+ * <Table
15
+ * columns={[
16
+ * { header: 'Name', accessor: 'name' },
17
+ * { header: 'Status', accessor: (r) => <Badge>{r.status}</Badge> },
18
+ * { header: 'Amount', accessor: 'amount', align: 'right' },
19
+ * ]}
20
+ * data={rows}
21
+ * />
22
+ */
23
+ export interface TableColumn<Row = Record<string, React.ReactNode>> {
24
+ header: React.ReactNode
25
+ /** A key into the row, or a render function returning the cell content. */
26
+ accessor: keyof Row | ((row: Row) => React.ReactNode)
27
+ align?: Align
28
+ }
29
+
30
+ export interface TableProps<Row = Record<string, React.ReactNode>>
31
+ extends NoStyle<React.HTMLAttributes<HTMLTableElement>> {
32
+ columns?: TableColumn<Row>[]
33
+ data?: readonly Row[]
34
+ caption?: React.ReactNode
35
+ }
36
+
37
+ // Generic forwardRef so `data` infers the row type — column accessors stay typed.
38
+ function TableInner<Row extends Record<string, React.ReactNode>>(
39
+ { columns, data, caption, children, ...props }: TableProps<Row>,
40
+ ref: React.ForwardedRef<HTMLTableElement>,
41
+ ) {
42
+ return (
43
+ <div className="relative w-full overflow-auto rounded-xl border border-border">
44
+ <table ref={ref} className={cn('w-full caption-bottom text-sm')} {...props}>
45
+ {caption != null && <TableCaption>{caption}</TableCaption>}
46
+ {columns && data ? (
47
+ <>
48
+ <TableHeader>
49
+ <TableRow>
50
+ {columns.map((col, i) => (
51
+ <TableHead key={i} align={col.align}>{col.header}</TableHead>
52
+ ))}
53
+ </TableRow>
54
+ </TableHeader>
55
+ <TableBody>
56
+ {data.map((row, r) => (
57
+ <TableRow key={r}>
58
+ {columns.map((col, c) => (
59
+ <TableCell key={c} align={col.align}>
60
+ {typeof col.accessor === 'function' ? col.accessor(row) : (row[col.accessor] as React.ReactNode)}
61
+ </TableCell>
62
+ ))}
63
+ </TableRow>
64
+ ))}
65
+ </TableBody>
66
+ </>
67
+ ) : (
68
+ children
69
+ )}
70
+ </table>
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export const Table = React.forwardRef(TableInner) as (<Row extends Record<string, React.ReactNode>>(
76
+ props: TableProps<Row> & { ref?: React.ForwardedRef<HTMLTableElement> },
77
+ ) => React.ReactElement) & { displayName?: string }
78
+ Table.displayName = 'Table'
79
+
80
+ export const TableHeader = React.forwardRef<HTMLTableSectionElement, NoStyle<React.HTMLAttributes<HTMLTableSectionElement>>>(
81
+ ({ ...props }, ref) => <thead ref={ref} className={cn('[&_tr]:border-b [&_tr]:border-border')} {...props} />,
82
+ )
83
+ TableHeader.displayName = 'TableHeader'
84
+
85
+ export const TableBody = React.forwardRef<HTMLTableSectionElement, NoStyle<React.HTMLAttributes<HTMLTableSectionElement>>>(
86
+ ({ ...props }, ref) => <tbody ref={ref} className={cn('[&_tr:last-child]:border-0')} {...props} />,
87
+ )
88
+ TableBody.displayName = 'TableBody'
89
+
90
+ export const TableFooter = React.forwardRef<HTMLTableSectionElement, NoStyle<React.HTMLAttributes<HTMLTableSectionElement>>>(
91
+ ({ ...props }, ref) => <tfoot ref={ref} className={cn('border-t border-border bg-surface-subtle font-medium')} {...props} />,
92
+ )
93
+ TableFooter.displayName = 'TableFooter'
94
+
95
+ export const TableRow = React.forwardRef<HTMLTableRowElement, NoStyle<React.HTMLAttributes<HTMLTableRowElement>>>(
96
+ ({ ...props }, ref) => (
97
+ <tr ref={ref} className={cn('border-b border-border transition-colors hover:bg-surface-subtle data-[state=selected]:bg-surface-inset')} {...props} />
98
+ ),
99
+ )
100
+ TableRow.displayName = 'TableRow'
101
+
102
+ export const TableHead = React.forwardRef<
103
+ HTMLTableCellElement,
104
+ Omit<NoStyle<React.ThHTMLAttributes<HTMLTableCellElement>>, 'align'> & { align?: Align }
105
+ >(({ align = 'left', ...props }, ref) => (
106
+ <th ref={ref} className={cn('h-11 px-4 align-middle text-sm font-semibold uppercase tracking-wide text-tertiary', alignClass[align])} {...props} />
107
+ ))
108
+ TableHead.displayName = 'TableHead'
109
+
110
+ export const TableCell = React.forwardRef<
111
+ HTMLTableCellElement,
112
+ Omit<NoStyle<React.TdHTMLAttributes<HTMLTableCellElement>>, 'align'> & { align?: Align }
113
+ >(({ align = 'left', ...props }, ref) => (
114
+ <td ref={ref} className={cn('px-4 py-3 align-middle', alignClass[align])} {...props} />
115
+ ))
116
+ TableCell.displayName = 'TableCell'
117
+
118
+ export const TableCaption = React.forwardRef<HTMLTableCaptionElement, NoStyle<React.HTMLAttributes<HTMLTableCaptionElement>>>(
119
+ ({ ...props }, ref) => <caption ref={ref} className={cn('mt-4 text-sm text-secondary')} {...props} />,
120
+ )
121
+ TableCaption.displayName = 'TableCaption'
@@ -0,0 +1,90 @@
1
+ import * as React from 'react'
2
+ import * as TabsPrimitive from '@radix-ui/react-tabs'
3
+ import { cn } from '../lib/utils'
4
+ import type { NoStyle } from '../lib/types'
5
+ import { focusRing } from '../lib/recipes'
6
+
7
+ /**
8
+ * Tabs — pass an `items` array for the common case (the component renders the
9
+ * list, triggers, and panels for you), or compose `TabsList`/`TabsTrigger`/
10
+ * `TabsContent` by hand for full control.
11
+ *
12
+ * <Tabs defaultValue="overview" items={[
13
+ * { value: 'overview', label: 'Overview', content: <Overview /> },
14
+ * { value: 'usage', label: 'Usage', content: <Usage /> },
15
+ * ]} />
16
+ */
17
+ export interface TabItem {
18
+ value: string
19
+ label: React.ReactNode
20
+ content: React.ReactNode
21
+ disabled?: boolean
22
+ }
23
+
24
+ export interface TabsProps extends NoStyle<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>> {
25
+ /** Declarative tabs. Omit to compose the primitives as children instead. */
26
+ items?: TabItem[]
27
+ }
28
+
29
+ export const Tabs = React.forwardRef<
30
+ React.ElementRef<typeof TabsPrimitive.Root>,
31
+ TabsProps
32
+ >(({ items, children, ...props }, ref) => {
33
+ if (!items) return <TabsPrimitive.Root ref={ref} {...props}>{children}</TabsPrimitive.Root>
34
+ return (
35
+ <TabsPrimitive.Root ref={ref} {...props}>
36
+ <TabsList>
37
+ {items.map((t) => (
38
+ <TabsTrigger key={t.value} value={t.value} disabled={t.disabled}>{t.label}</TabsTrigger>
39
+ ))}
40
+ </TabsList>
41
+ {items.map((t) => (
42
+ <TabsContent key={t.value} value={t.value}>{t.content}</TabsContent>
43
+ ))}
44
+ </TabsPrimitive.Root>
45
+ )
46
+ })
47
+ Tabs.displayName = 'Tabs'
48
+
49
+ export const TabsList = React.forwardRef<
50
+ React.ElementRef<typeof TabsPrimitive.List>,
51
+ NoStyle<React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>>
52
+ >(({ ...props }, ref) => (
53
+ <TabsPrimitive.List
54
+ ref={ref}
55
+ className={cn(
56
+ 'inline-flex h-10 items-center justify-center gap-1 rounded-lg bg-surface-inset p-1 text-secondary',
57
+ )}
58
+ {...props}
59
+ />
60
+ ))
61
+ TabsList.displayName = TabsPrimitive.List.displayName
62
+
63
+ export const TabsTrigger = React.forwardRef<
64
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
65
+ NoStyle<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>>
66
+ >(({ ...props }, ref) => (
67
+ <TabsPrimitive.Trigger
68
+ ref={ref}
69
+ className={cn(
70
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-all',
71
+ focusRing,
72
+ 'disabled:pointer-events-none disabled:opacity-50',
73
+ 'data-[state=active]:bg-surface data-[state=active]:text-primary data-[state=active]:shadow-sm',
74
+ )}
75
+ {...props}
76
+ />
77
+ ))
78
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
79
+
80
+ export const TabsContent = React.forwardRef<
81
+ React.ElementRef<typeof TabsPrimitive.Content>,
82
+ NoStyle<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>>
83
+ >(({ ...props }, ref) => (
84
+ <TabsPrimitive.Content
85
+ ref={ref}
86
+ className={cn('mt-lg focus-visible:outline-none')}
87
+ {...props}
88
+ />
89
+ ))
90
+ TabsContent.displayName = TabsPrimitive.Content.displayName