@exxatdesignux/ui 0.0.5
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 +72 -0
- package/src/components/ui/avatar.tsx +384 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/banner.tsx +364 -0
- package/src/components/ui/breadcrumb.tsx +120 -0
- package/src/components/ui/button.tsx +66 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +136 -0
- package/src/components/ui/chart.tsx +378 -0
- package/src/components/ui/checkbox.tsx +160 -0
- package/src/components/ui/coach-mark.tsx +361 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +232 -0
- package/src/components/ui/date-picker-field.tsx +186 -0
- package/src/components/ui/dialog.tsx +171 -0
- package/src/components/ui/drag-handle-grip.tsx +10 -0
- package/src/components/ui/drawer.tsx +134 -0
- package/src/components/ui/dropdown-menu.tsx +422 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/form.tsx +137 -0
- package/src/components/ui/input-group.tsx +156 -0
- package/src/components/ui/input-mask.tsx +135 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +55 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/payment-card-fields.tsx +65 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/radio-group.tsx +217 -0
- package/src/components/ui/select.tsx +191 -0
- package/src/components/ui/selection-tile-grid.tsx +246 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +716 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +39 -0
- package/src/components/ui/status-badge.tsx +109 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tip.tsx +21 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle-switch.tsx +31 -0
- package/src/components/ui/toggle.tsx +48 -0
- package/src/components/ui/tooltip.tsx +59 -0
- package/src/components/ui/view-segmented-control.tsx +160 -0
- package/src/globals.css +1795 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-app-theme.ts +172 -0
- package/src/hooks/use-coach-mark.ts +342 -0
- package/src/hooks/use-mobile.ts +31 -0
- package/src/hooks/use-mod-key-label.ts +29 -0
- package/src/index.ts +55 -0
- package/src/lib/compose-refs.ts +15 -0
- package/src/lib/date-filter.ts +67 -0
- package/src/lib/utils.ts +6 -0
- package/src/theme/apply-windows-contrast-theme.ts +29 -0
- package/src/theme/windows-contrast-theme.json +147 -0
- package/src/theme.css +1130 -0
- package/src/types/react-payment-inputs.d.ts +20 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Radio group — Radix `RadioGroup` + `RadioGroupItem` with Exxat styling.
|
|
5
|
+
*
|
|
6
|
+
* Aligned with Shadcn Studio radio-group guidance (see shadcnstudio.com/docs/components/radio-group):
|
|
7
|
+
* • Visual variants: default, outline, secondary, success, destructive, warning, muted
|
|
8
|
+
* • Sizes: sm, default, lg (outer ring + inner dot scale together)
|
|
9
|
+
* • Motion: none | pop | glow | pop-glow — `motion-safe` / `motion-reduce` (WCAG 2.3.3)
|
|
10
|
+
* • Optional group defaults: `<RadioGroup itemVariant="outline" itemSize="sm">` so items inherit
|
|
11
|
+
* • Per-item overrides: `<RadioGroupItem variant="success" />`
|
|
12
|
+
* • Forms: `aria-invalid`, `disabled`, hit slop (`after:`); pair with `RadioGroupLabel` + `htmlFor` / `id`
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as React from "react"
|
|
16
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
17
|
+
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
|
|
18
|
+
|
|
19
|
+
import { cn } from "../../lib/utils"
|
|
20
|
+
import { Label } from "./label"
|
|
21
|
+
|
|
22
|
+
// ── Item chrome (variants / sizes / motion) ─────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const radioGroupItemVariants = cva(
|
|
25
|
+
[
|
|
26
|
+
"group/radio-group-item peer relative flex shrink-0 aspect-square rounded-full border border-input",
|
|
27
|
+
"outline-none transition-[color,box-shadow,transform,background-color,border-color] duration-150",
|
|
28
|
+
"motion-reduce:transition-none",
|
|
29
|
+
"group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2",
|
|
30
|
+
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
|
|
31
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
32
|
+
"aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:data-[state=checked]:border-primary",
|
|
33
|
+
"dark:bg-input/15 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
34
|
+
].join(" "),
|
|
35
|
+
{
|
|
36
|
+
variants: {
|
|
37
|
+
variant: {
|
|
38
|
+
default: [
|
|
39
|
+
"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary",
|
|
40
|
+
].join(" "),
|
|
41
|
+
outline: [
|
|
42
|
+
"bg-background",
|
|
43
|
+
"data-[state=checked]:border-primary data-[state=checked]:bg-background data-[state=checked]:text-primary data-[state=checked]:ring-2 data-[state=checked]:ring-primary/25",
|
|
44
|
+
].join(" "),
|
|
45
|
+
secondary: [
|
|
46
|
+
"data-[state=checked]:border-secondary data-[state=checked]:bg-secondary data-[state=checked]:text-secondary-foreground",
|
|
47
|
+
].join(" "),
|
|
48
|
+
success: [
|
|
49
|
+
"data-[state=checked]:border-chart-2 data-[state=checked]:bg-chart-2 data-[state=checked]:text-primary-foreground",
|
|
50
|
+
].join(" "),
|
|
51
|
+
destructive: [
|
|
52
|
+
"data-[state=checked]:border-destructive data-[state=checked]:bg-destructive data-[state=checked]:text-destructive-foreground",
|
|
53
|
+
].join(" "),
|
|
54
|
+
warning: [
|
|
55
|
+
"data-[state=checked]:border-amber-500 data-[state=checked]:bg-amber-500 data-[state=checked]:text-amber-950",
|
|
56
|
+
].join(" "),
|
|
57
|
+
muted: [
|
|
58
|
+
"data-[state=checked]:border-muted-foreground/50 data-[state=checked]:bg-muted data-[state=checked]:text-foreground",
|
|
59
|
+
].join(" "),
|
|
60
|
+
},
|
|
61
|
+
size: {
|
|
62
|
+
sm: "size-3.5 min-h-3.5 min-w-3.5 max-h-3.5 max-w-3.5",
|
|
63
|
+
default: "size-4 min-h-4 min-w-4 max-h-4 max-w-4",
|
|
64
|
+
lg: "size-5 min-h-5 min-w-5 max-h-5 max-w-5",
|
|
65
|
+
},
|
|
66
|
+
motion: {
|
|
67
|
+
none: "",
|
|
68
|
+
pop: [
|
|
69
|
+
"motion-safe:active:scale-95",
|
|
70
|
+
"data-[state=checked]:motion-safe:scale-[1.04]",
|
|
71
|
+
].join(" "),
|
|
72
|
+
glow: "data-[state=checked]:shadow-[0_0_0_3px] data-[state=checked]:shadow-primary/35",
|
|
73
|
+
"pop-glow": [
|
|
74
|
+
"motion-safe:active:scale-95",
|
|
75
|
+
"data-[state=checked]:motion-safe:scale-[1.04]",
|
|
76
|
+
"data-[state=checked]:shadow-[0_0_0_3px] data-[state=checked]:shadow-primary/35",
|
|
77
|
+
].join(" "),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
defaultVariants: {
|
|
81
|
+
variant: "default",
|
|
82
|
+
size: "default",
|
|
83
|
+
motion: "none",
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const radioIndicatorDotVariants = cva(
|
|
89
|
+
"pointer-events-none absolute top-1/2 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 rounded-full",
|
|
90
|
+
{
|
|
91
|
+
variants: {
|
|
92
|
+
variant: {
|
|
93
|
+
default: "bg-primary-foreground",
|
|
94
|
+
outline: "bg-primary",
|
|
95
|
+
secondary: "bg-secondary-foreground",
|
|
96
|
+
success: "bg-primary-foreground",
|
|
97
|
+
destructive: "bg-destructive-foreground",
|
|
98
|
+
warning: "bg-amber-950",
|
|
99
|
+
muted: "bg-foreground",
|
|
100
|
+
},
|
|
101
|
+
size: {
|
|
102
|
+
sm: "size-1.5",
|
|
103
|
+
default: "size-2",
|
|
104
|
+
lg: "size-2.5",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
defaultVariants: {
|
|
108
|
+
variant: "default",
|
|
109
|
+
size: "default",
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const radioIndicatorWrapperVariants = cva("relative flex size-full items-center justify-center", {
|
|
115
|
+
variants: {
|
|
116
|
+
motion: {
|
|
117
|
+
none: "",
|
|
118
|
+
pop: "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:duration-150",
|
|
119
|
+
glow: "",
|
|
120
|
+
"pop-glow": "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:duration-150",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
defaultVariants: { motion: "none" },
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
type ItemChrome = {
|
|
127
|
+
itemVariant?: VariantProps<typeof radioGroupItemVariants>["variant"]
|
|
128
|
+
itemSize?: VariantProps<typeof radioGroupItemVariants>["size"]
|
|
129
|
+
itemMotion?: VariantProps<typeof radioGroupItemVariants>["motion"]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const RadioGroupItemChromeContext = React.createContext<ItemChrome>({})
|
|
133
|
+
|
|
134
|
+
// ── RadioGroup root ─────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
export type RadioGroupProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> &
|
|
137
|
+
ItemChrome
|
|
138
|
+
|
|
139
|
+
function RadioGroup({
|
|
140
|
+
className,
|
|
141
|
+
itemVariant,
|
|
142
|
+
itemSize,
|
|
143
|
+
itemMotion,
|
|
144
|
+
...props
|
|
145
|
+
}: RadioGroupProps) {
|
|
146
|
+
const ctx = React.useMemo(
|
|
147
|
+
() => ({ itemVariant, itemSize, itemMotion }),
|
|
148
|
+
[itemVariant, itemSize, itemMotion],
|
|
149
|
+
)
|
|
150
|
+
return (
|
|
151
|
+
<RadioGroupItemChromeContext.Provider value={ctx}>
|
|
152
|
+
<RadioGroupPrimitive.Root
|
|
153
|
+
data-slot="radio-group"
|
|
154
|
+
className={cn("grid w-full gap-2", className)}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
</RadioGroupItemChromeContext.Provider>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── RadioGroupItem ──────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
export type RadioGroupItemProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> &
|
|
164
|
+
VariantProps<typeof radioGroupItemVariants>
|
|
165
|
+
|
|
166
|
+
const RadioGroupItem = React.forwardRef<
|
|
167
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
168
|
+
RadioGroupItemProps
|
|
169
|
+
>(function RadioGroupItem(
|
|
170
|
+
{ className, variant: variantProp, size: sizeProp, motion: motionProp, ...props },
|
|
171
|
+
ref,
|
|
172
|
+
) {
|
|
173
|
+
const ctx = React.useContext(RadioGroupItemChromeContext)
|
|
174
|
+
const variant = variantProp ?? ctx.itemVariant
|
|
175
|
+
const size = sizeProp ?? ctx.itemSize
|
|
176
|
+
const motion = motionProp ?? ctx.itemMotion ?? "none"
|
|
177
|
+
const vResolved = variant ?? "default"
|
|
178
|
+
const sResolved = size ?? "default"
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<RadioGroupPrimitive.Item
|
|
182
|
+
ref={ref}
|
|
183
|
+
data-slot="radio-group-item"
|
|
184
|
+
data-variant={vResolved}
|
|
185
|
+
data-motion={motion}
|
|
186
|
+
className={cn(radioGroupItemVariants({ variant, size, motion }), className)}
|
|
187
|
+
{...props}
|
|
188
|
+
>
|
|
189
|
+
<RadioGroupPrimitive.Indicator
|
|
190
|
+
data-slot="radio-group-indicator"
|
|
191
|
+
className={radioIndicatorWrapperVariants({ motion })}
|
|
192
|
+
>
|
|
193
|
+
<span className={radioIndicatorDotVariants({ variant: vResolved, size: sResolved })} />
|
|
194
|
+
</RadioGroupPrimitive.Indicator>
|
|
195
|
+
</RadioGroupPrimitive.Item>
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// ── Label helper (touch-friendly row with peer-disabled) ────────────────────
|
|
200
|
+
|
|
201
|
+
export type RadioGroupLabelProps = React.ComponentPropsWithRef<typeof Label>
|
|
202
|
+
|
|
203
|
+
function RadioGroupLabel({ className, ...props }: RadioGroupLabelProps) {
|
|
204
|
+
return (
|
|
205
|
+
<Label
|
|
206
|
+
data-slot="radio-group-label"
|
|
207
|
+
className={cn(
|
|
208
|
+
"inline-flex min-h-11 cursor-pointer select-none items-center gap-2 py-1 -my-1 text-sm font-medium leading-snug",
|
|
209
|
+
"peer-disabled:cursor-not-allowed peer-disabled:opacity-60",
|
|
210
|
+
className
|
|
211
|
+
)}
|
|
212
|
+
{...props}
|
|
213
|
+
/>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { RadioGroup, RadioGroupItem, RadioGroupLabel, radioGroupItemVariants }
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Select as SelectPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
function Select({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
11
|
+
return <SelectPrimitive.Root data-slot="select" {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function SelectGroup({
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
18
|
+
return (
|
|
19
|
+
<SelectPrimitive.Group
|
|
20
|
+
data-slot="select-group"
|
|
21
|
+
className={cn("scroll-my-1 p-1", className)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SelectValue({
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
30
|
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function SelectTrigger({
|
|
34
|
+
className,
|
|
35
|
+
size = "default",
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
39
|
+
size?: "sm" | "default"
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<SelectPrimitive.Trigger
|
|
43
|
+
data-slot="select-trigger"
|
|
44
|
+
data-size={size}
|
|
45
|
+
className={cn(
|
|
46
|
+
"flex w-fit items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-2 pe-2 ps-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/15 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
<SelectPrimitive.Icon asChild>
|
|
53
|
+
<i className="fa-light fa-chevron-down pointer-events-none size-4 text-muted-foreground" aria-hidden="true" />
|
|
54
|
+
</SelectPrimitive.Icon>
|
|
55
|
+
</SelectPrimitive.Trigger>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function SelectContent({
|
|
60
|
+
className,
|
|
61
|
+
children,
|
|
62
|
+
position = "item-aligned",
|
|
63
|
+
align = "center",
|
|
64
|
+
...props
|
|
65
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
66
|
+
return (
|
|
67
|
+
<SelectPrimitive.Portal>
|
|
68
|
+
<SelectPrimitive.Content
|
|
69
|
+
data-slot="select-content"
|
|
70
|
+
data-align-trigger={position === "item-aligned"}
|
|
71
|
+
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 rtl:data-[side=left]:translate-x-1 data-[side=right]:translate-x-1 rtl:data-[side=right]:-translate-x-1 data-[side=top]:-translate-y-1", className )}
|
|
72
|
+
position={position}
|
|
73
|
+
align={align}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
<SelectScrollUpButton />
|
|
77
|
+
<SelectPrimitive.Viewport
|
|
78
|
+
data-position={position}
|
|
79
|
+
className={cn(
|
|
80
|
+
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
|
81
|
+
position === "popper" && ""
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</SelectPrimitive.Viewport>
|
|
86
|
+
<SelectScrollDownButton />
|
|
87
|
+
</SelectPrimitive.Content>
|
|
88
|
+
</SelectPrimitive.Portal>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function SelectLabel({
|
|
93
|
+
className,
|
|
94
|
+
...props
|
|
95
|
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
96
|
+
return (
|
|
97
|
+
<SelectPrimitive.Label
|
|
98
|
+
data-slot="select-label"
|
|
99
|
+
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function SelectItem({
|
|
106
|
+
className,
|
|
107
|
+
children,
|
|
108
|
+
...props
|
|
109
|
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
110
|
+
return (
|
|
111
|
+
<SelectPrimitive.Item
|
|
112
|
+
data-slot="select-item"
|
|
113
|
+
className={cn(
|
|
114
|
+
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
115
|
+
className
|
|
116
|
+
)}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
<span className="pointer-events-none absolute end-2 flex size-4 items-center justify-center">
|
|
120
|
+
<SelectPrimitive.ItemIndicator>
|
|
121
|
+
<i className="fa-light fa-check pointer-events-none" aria-hidden="true" />
|
|
122
|
+
</SelectPrimitive.ItemIndicator>
|
|
123
|
+
</span>
|
|
124
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
125
|
+
</SelectPrimitive.Item>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function SelectSeparator({
|
|
130
|
+
className,
|
|
131
|
+
...props
|
|
132
|
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
133
|
+
return (
|
|
134
|
+
<SelectPrimitive.Separator
|
|
135
|
+
data-slot="select-separator"
|
|
136
|
+
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function SelectScrollUpButton({
|
|
143
|
+
className,
|
|
144
|
+
...props
|
|
145
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
146
|
+
return (
|
|
147
|
+
<SelectPrimitive.ScrollUpButton
|
|
148
|
+
data-slot="select-scroll-up-button"
|
|
149
|
+
className={cn(
|
|
150
|
+
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
<i className="fa-light fa-chevron-up" aria-hidden="true"
|
|
156
|
+
/>
|
|
157
|
+
</SelectPrimitive.ScrollUpButton>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function SelectScrollDownButton({
|
|
162
|
+
className,
|
|
163
|
+
...props
|
|
164
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
165
|
+
return (
|
|
166
|
+
<SelectPrimitive.ScrollDownButton
|
|
167
|
+
data-slot="select-scroll-down-button"
|
|
168
|
+
className={cn(
|
|
169
|
+
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
170
|
+
className
|
|
171
|
+
)}
|
|
172
|
+
{...props}
|
|
173
|
+
>
|
|
174
|
+
<i className="fa-light fa-chevron-down" aria-hidden="true"
|
|
175
|
+
/>
|
|
176
|
+
</SelectPrimitive.ScrollDownButton>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
Select,
|
|
182
|
+
SelectContent,
|
|
183
|
+
SelectGroup,
|
|
184
|
+
SelectItem,
|
|
185
|
+
SelectLabel,
|
|
186
|
+
SelectScrollDownButton,
|
|
187
|
+
SelectScrollUpButton,
|
|
188
|
+
SelectSeparator,
|
|
189
|
+
SelectTrigger,
|
|
190
|
+
SelectValue,
|
|
191
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
import { Label } from "./label"
|
|
7
|
+
import { RadioGroup, RadioGroupItem, type RadioGroupProps } from "./radio-group"
|
|
8
|
+
|
|
9
|
+
export interface SelectionTileOption<T extends string = string> {
|
|
10
|
+
value: T
|
|
11
|
+
label: string
|
|
12
|
+
/** Font Awesome icon class without prefix (e.g. `fa-table`); rendered with `fa-light`. Ignored when `leading` is set. */
|
|
13
|
+
icon?: string
|
|
14
|
+
/** Custom graphic (SVG, swatch, etc.) instead of `icon`. */
|
|
15
|
+
leading?: React.ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Shared surface classes for icon+label tiles (Properties view type, Export format, etc.). */
|
|
19
|
+
export function selectionTileClassNames(selected: boolean) {
|
|
20
|
+
return cn(
|
|
21
|
+
"flex flex-col items-center justify-center gap-1.5 rounded-lg border py-3 text-xs transition-colors",
|
|
22
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
23
|
+
selected
|
|
24
|
+
? "border-brand bg-brand/5 text-foreground font-medium shadow-sm"
|
|
25
|
+
: "border-border bg-background text-muted-foreground hover:border-border/80 hover:text-interactive-hover-foreground",
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Preview box only — label rendered below (Settings appearance pattern). */
|
|
30
|
+
export function selectionTilePreviewClassNames(selected: boolean) {
|
|
31
|
+
return cn(
|
|
32
|
+
"relative box-border flex aspect-square w-full max-w-full flex-col rounded-lg border p-1 transition-colors",
|
|
33
|
+
selected
|
|
34
|
+
? "border-brand bg-brand/5 shadow-sm"
|
|
35
|
+
: "border-border bg-background hover:border-border/80",
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function SelectionTileGraphic<T extends string>({
|
|
40
|
+
option,
|
|
41
|
+
selected,
|
|
42
|
+
}: {
|
|
43
|
+
option: SelectionTileOption<T>
|
|
44
|
+
selected: boolean
|
|
45
|
+
}) {
|
|
46
|
+
if (option.leading != null) {
|
|
47
|
+
return (
|
|
48
|
+
<span className="flex h-full min-h-0 w-full min-w-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:block [&_svg]:h-full [&_svg]:w-auto [&_svg]:max-h-full [&_svg]:max-w-full [&_svg]:object-contain [&_svg]:object-center">
|
|
49
|
+
{option.leading}
|
|
50
|
+
</span>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
if (option.icon) {
|
|
54
|
+
return (
|
|
55
|
+
<i
|
|
56
|
+
className={cn(
|
|
57
|
+
"fa-light shrink-0 text-[18px] leading-none",
|
|
58
|
+
option.icon,
|
|
59
|
+
selected && "text-brand",
|
|
60
|
+
)}
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function SelectionTileLabelText<T extends string>({
|
|
69
|
+
option,
|
|
70
|
+
}: {
|
|
71
|
+
option: SelectionTileOption<T>
|
|
72
|
+
}) {
|
|
73
|
+
return <span className="text-center leading-tight">{option.label}</span>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SelectionTileGridProps<T extends string> {
|
|
77
|
+
/** Section caption above the grid (e.g. “View type”). */
|
|
78
|
+
sectionLabel?: string
|
|
79
|
+
options: readonly SelectionTileOption<T>[]
|
|
80
|
+
columns?: 2 | 3 | 4
|
|
81
|
+
value: T
|
|
82
|
+
onValueChange: (value: T) => void
|
|
83
|
+
/** `radio` — Form / RadioGroup; `button` — toggle buttons with aria-pressed. */
|
|
84
|
+
interaction: "radio" | "button"
|
|
85
|
+
/** Prefix for radio ids (`${idPrefix}-${value}`). */
|
|
86
|
+
idPrefix?: string
|
|
87
|
+
/** Forwarded to `RadioGroup` when `interaction="radio"` (sr-only inputs; affects focus ring / state tokens). */
|
|
88
|
+
itemVariant?: RadioGroupProps["itemVariant"]
|
|
89
|
+
itemSize?: RadioGroupProps["itemSize"]
|
|
90
|
+
itemMotion?: RadioGroupProps["itemMotion"]
|
|
91
|
+
className?: string
|
|
92
|
+
/**
|
|
93
|
+
* `inside` — label in the bordered tile (default). `below` — label under the preview
|
|
94
|
+
* (matches system settings: preview box + caption outside).
|
|
95
|
+
*/
|
|
96
|
+
labelPlacement?: "inside" | "below"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Icon tile grid for single selection — matches Properties “View type” and Export “File format” patterns.
|
|
101
|
+
*/
|
|
102
|
+
function SelectionTileCaptionBelow<T extends string>({
|
|
103
|
+
option,
|
|
104
|
+
selected,
|
|
105
|
+
ariaHidden = false,
|
|
106
|
+
}: {
|
|
107
|
+
option: SelectionTileOption<T>
|
|
108
|
+
selected: boolean
|
|
109
|
+
/** When the parent `button` already has `aria-label` (avoids duplicate SR output). */
|
|
110
|
+
ariaHidden?: boolean
|
|
111
|
+
}) {
|
|
112
|
+
return (
|
|
113
|
+
<span
|
|
114
|
+
className={cn(
|
|
115
|
+
"max-w-full px-0.5 text-center text-xs leading-tight",
|
|
116
|
+
selected ? "font-medium text-foreground" : "text-muted-foreground",
|
|
117
|
+
)}
|
|
118
|
+
{...(ariaHidden ? { "aria-hidden": true as const } : {})}
|
|
119
|
+
>
|
|
120
|
+
{option.label}
|
|
121
|
+
</span>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function SelectionTileGrid<T extends string>({
|
|
126
|
+
sectionLabel,
|
|
127
|
+
options,
|
|
128
|
+
columns = 4,
|
|
129
|
+
value,
|
|
130
|
+
onValueChange,
|
|
131
|
+
interaction,
|
|
132
|
+
idPrefix = "tile",
|
|
133
|
+
className,
|
|
134
|
+
labelPlacement = "inside",
|
|
135
|
+
itemVariant,
|
|
136
|
+
itemSize,
|
|
137
|
+
itemMotion,
|
|
138
|
+
}: SelectionTileGridProps<T>) {
|
|
139
|
+
const gridClass = cn(
|
|
140
|
+
"gap-2",
|
|
141
|
+
columns === 2 && "grid grid-cols-2",
|
|
142
|
+
columns === 3 && "grid grid-cols-3",
|
|
143
|
+
columns === 4 && "grid grid-cols-4",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if (interaction === "radio") {
|
|
147
|
+
return (
|
|
148
|
+
<div className={className}>
|
|
149
|
+
{sectionLabel ? (
|
|
150
|
+
<p className="mb-2 text-xs font-medium text-muted-foreground">{sectionLabel}</p>
|
|
151
|
+
) : null}
|
|
152
|
+
<RadioGroup
|
|
153
|
+
value={value}
|
|
154
|
+
onValueChange={v => onValueChange(v as T)}
|
|
155
|
+
className={gridClass}
|
|
156
|
+
itemVariant={itemVariant}
|
|
157
|
+
itemSize={itemSize}
|
|
158
|
+
itemMotion={itemMotion}
|
|
159
|
+
>
|
|
160
|
+
{options.map(opt => {
|
|
161
|
+
const selected = value === opt.value
|
|
162
|
+
const id = `${idPrefix}-${opt.value}`
|
|
163
|
+
if (labelPlacement === "below") {
|
|
164
|
+
return (
|
|
165
|
+
<Label
|
|
166
|
+
key={opt.value}
|
|
167
|
+
htmlFor={id}
|
|
168
|
+
className={cn(
|
|
169
|
+
"flex min-w-0 cursor-pointer flex-col items-center gap-1.5 rounded-lg focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
<span className={cn("flex w-full justify-center", selectionTilePreviewClassNames(selected))}>
|
|
173
|
+
<RadioGroupItem value={opt.value} id={id} className="sr-only" />
|
|
174
|
+
<span className="flex min-h-0 w-full min-w-0 flex-1 items-center justify-center">
|
|
175
|
+
<SelectionTileGraphic option={opt} selected={selected} />
|
|
176
|
+
</span>
|
|
177
|
+
</span>
|
|
178
|
+
<SelectionTileCaptionBelow option={opt} selected={selected} />
|
|
179
|
+
</Label>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
return (
|
|
183
|
+
<Label
|
|
184
|
+
key={opt.value}
|
|
185
|
+
htmlFor={id}
|
|
186
|
+
className={cn(
|
|
187
|
+
"cursor-pointer rounded-lg focus-within:rounded-lg focus-within:ring-2 focus-within:ring-ring focus-within:outline-none",
|
|
188
|
+
selectionTileClassNames(selected),
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
<RadioGroupItem value={opt.value} id={id} className="sr-only" />
|
|
192
|
+
<SelectionTileGraphic option={opt} selected={selected} />
|
|
193
|
+
<SelectionTileLabelText option={opt} />
|
|
194
|
+
</Label>
|
|
195
|
+
)
|
|
196
|
+
})}
|
|
197
|
+
</RadioGroup>
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div className={className}>
|
|
204
|
+
{sectionLabel ? (
|
|
205
|
+
<p className="mb-2 text-xs font-medium text-muted-foreground">{sectionLabel}</p>
|
|
206
|
+
) : null}
|
|
207
|
+
<div className={gridClass}>
|
|
208
|
+
{options.map(opt => {
|
|
209
|
+
const selected = value === opt.value
|
|
210
|
+
if (labelPlacement === "below") {
|
|
211
|
+
return (
|
|
212
|
+
<button
|
|
213
|
+
key={opt.value}
|
|
214
|
+
type="button"
|
|
215
|
+
aria-label={opt.label}
|
|
216
|
+
aria-pressed={selected}
|
|
217
|
+
onClick={() => onValueChange(opt.value)}
|
|
218
|
+
className="flex min-w-0 flex-col items-center gap-1.5 rounded-lg border-0 bg-transparent p-0 text-inherit focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
219
|
+
>
|
|
220
|
+
<span className={cn("flex w-full justify-center", selectionTilePreviewClassNames(selected))}>
|
|
221
|
+
<span className="flex min-h-0 w-full min-w-0 flex-1 items-center justify-center">
|
|
222
|
+
<SelectionTileGraphic option={opt} selected={selected} />
|
|
223
|
+
</span>
|
|
224
|
+
</span>
|
|
225
|
+
<SelectionTileCaptionBelow option={opt} selected={selected} ariaHidden />
|
|
226
|
+
</button>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return (
|
|
230
|
+
<button
|
|
231
|
+
key={opt.value}
|
|
232
|
+
type="button"
|
|
233
|
+
aria-label={opt.label}
|
|
234
|
+
aria-pressed={selected}
|
|
235
|
+
onClick={() => onValueChange(opt.value)}
|
|
236
|
+
className={selectionTileClassNames(selected)}
|
|
237
|
+
>
|
|
238
|
+
<SelectionTileGraphic option={opt} selected={selected} />
|
|
239
|
+
<SelectionTileLabelText option={opt} />
|
|
240
|
+
</button>
|
|
241
|
+
)
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Separator as SeparatorPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
function Separator({
|
|
9
|
+
className,
|
|
10
|
+
orientation = "horizontal",
|
|
11
|
+
decorative = true,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
14
|
+
return (
|
|
15
|
+
<SeparatorPrimitive.Root
|
|
16
|
+
data-slot="separator"
|
|
17
|
+
decorative={decorative}
|
|
18
|
+
orientation={orientation}
|
|
19
|
+
className={cn(
|
|
20
|
+
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Separator }
|