@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.
- package/dist/index.d.ts +1195 -0
- package/dist/index.js +3628 -0
- package/package.json +66 -0
- package/src/catalog.ts +654 -0
- package/src/components/accordion.tsx +91 -0
- package/src/components/alert.tsx +58 -0
- package/src/components/autocomplete.tsx +174 -0
- package/src/components/avatar.tsx +60 -0
- package/src/components/badge.tsx +37 -0
- package/src/components/breadcrumb.tsx +62 -0
- package/src/components/button-group.tsx +23 -0
- package/src/components/button.tsx +53 -0
- package/src/components/calendar.tsx +61 -0
- package/src/components/card.tsx +72 -0
- package/src/components/chart.tsx +130 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/chip.tsx +75 -0
- package/src/components/code-block.tsx +126 -0
- package/src/components/command.tsx +139 -0
- package/src/components/data-table.tsx +194 -0
- package/src/components/date-picker.tsx +77 -0
- package/src/components/dialog.tsx +57 -0
- package/src/components/dropdown-menu.tsx +186 -0
- package/src/components/form-field.tsx +97 -0
- package/src/components/input.tsx +29 -0
- package/src/components/label.tsx +18 -0
- package/src/components/layout.tsx +179 -0
- package/src/components/link.tsx +37 -0
- package/src/components/modal.tsx +67 -0
- package/src/components/multi-select.tsx +175 -0
- package/src/components/pagination.tsx +72 -0
- package/src/components/popover.tsx +25 -0
- package/src/components/progress.tsx +31 -0
- package/src/components/radio-group.tsx +34 -0
- package/src/components/select.tsx +134 -0
- package/src/components/separator.tsx +21 -0
- package/src/components/sheet.tsx +80 -0
- package/src/components/skeleton.tsx +11 -0
- package/src/components/slider.tsx +28 -0
- package/src/components/stepper.tsx +69 -0
- package/src/components/switch.tsx +33 -0
- package/src/components/table.tsx +121 -0
- package/src/components/tabs.tsx +90 -0
- package/src/components/text.tsx +109 -0
- package/src/components/textarea.tsx +27 -0
- package/src/components/toast.tsx +107 -0
- package/src/components/toggle-button.tsx +103 -0
- package/src/components/tooltip.tsx +26 -0
- package/src/icons/forge-icon.tsx +55 -0
- package/src/icons/icon-set.ts +60 -0
- package/src/icons/svg-icon.tsx +43 -0
- package/src/index.ts +80 -0
- package/src/layouts/app-bar.tsx +95 -0
- package/src/layouts/app-shell.tsx +80 -0
- package/src/layouts/side-nav.tsx +196 -0
- package/src/layouts/theme-provider.tsx +128 -0
- package/src/lib/recipes.ts +50 -0
- package/src/lib/types.ts +3 -0
- package/src/lib/use-media-query.ts +18 -0
- package/src/lib/utils.ts +10 -0
- 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
|