@payglocal_ui/flux-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 (72) hide show
  1. package/package.json +72 -0
  2. package/src/accordion.tsx +68 -0
  3. package/src/alert.tsx +107 -0
  4. package/src/avatar-group.tsx +96 -0
  5. package/src/avatar-tag.tsx +136 -0
  6. package/src/avatar.tsx +39 -0
  7. package/src/badge.tsx +98 -0
  8. package/src/blanket.tsx +61 -0
  9. package/src/breadcrumb.tsx +119 -0
  10. package/src/button-group.tsx +218 -0
  11. package/src/button.tsx +83 -0
  12. package/src/calendar.tsx +227 -0
  13. package/src/callout.tsx +68 -0
  14. package/src/card.tsx +103 -0
  15. package/src/chart-templates.tsx +587 -0
  16. package/src/chart.tsx +379 -0
  17. package/src/checkbox-select.tsx +239 -0
  18. package/src/checkbox.tsx +54 -0
  19. package/src/code.tsx +154 -0
  20. package/src/command.tsx +77 -0
  21. package/src/country-select.tsx +242 -0
  22. package/src/currency-amount-input.tsx +72 -0
  23. package/src/data-table.tsx +378 -0
  24. package/src/date-picker.tsx +317 -0
  25. package/src/dialog.tsx +81 -0
  26. package/src/drawer.tsx +91 -0
  27. package/src/dropdown-menu.tsx +174 -0
  28. package/src/empty-state.tsx +32 -0
  29. package/src/field.tsx +243 -0
  30. package/src/flag.tsx +265 -0
  31. package/src/form.tsx +168 -0
  32. package/src/grid-flex.tsx +241 -0
  33. package/src/heading.tsx +202 -0
  34. package/src/icon-button.tsx +93 -0
  35. package/src/index.ts +332 -0
  36. package/src/inline-dialog.tsx +153 -0
  37. package/src/inline-edit.tsx +212 -0
  38. package/src/input-group.tsx +151 -0
  39. package/src/input.tsx +28 -0
  40. package/src/label.tsx +21 -0
  41. package/src/layout.tsx +119 -0
  42. package/src/link.tsx +80 -0
  43. package/src/lozenge.tsx +61 -0
  44. package/src/menu.tsx +146 -0
  45. package/src/otp-input.tsx +117 -0
  46. package/src/page-header.tsx +28 -0
  47. package/src/pagination.tsx +185 -0
  48. package/src/password-input.tsx +34 -0
  49. package/src/popover.tsx +31 -0
  50. package/src/progress-indicator.tsx +94 -0
  51. package/src/progress.tsx +95 -0
  52. package/src/radio-group.tsx +46 -0
  53. package/src/responsive.tsx +276 -0
  54. package/src/scroll-area.tsx +39 -0
  55. package/src/section-message.tsx +119 -0
  56. package/src/select.tsx +144 -0
  57. package/src/separator.tsx +26 -0
  58. package/src/side-nav.tsx +264 -0
  59. package/src/skeleton.tsx +74 -0
  60. package/src/slider.tsx +25 -0
  61. package/src/sonner.tsx +32 -0
  62. package/src/spinner.tsx +54 -0
  63. package/src/spotlight.tsx +141 -0
  64. package/src/status-badge.tsx +86 -0
  65. package/src/switch.tsx +70 -0
  66. package/src/tabs.tsx +57 -0
  67. package/src/tag.tsx +52 -0
  68. package/src/textarea.tsx +25 -0
  69. package/src/time-picker.tsx +443 -0
  70. package/src/tooltip.tsx +29 -0
  71. package/src/utils.ts +6 -0
  72. package/src/visually-hidden.tsx +25 -0
package/src/chart.tsx ADDED
@@ -0,0 +1,379 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Chart primitives from shadcn/ui (Recharts composition layer).
5
+ * @see https://ui.shadcn.com/docs/components/radix/chart
6
+ */
7
+
8
+ import * as React from "react"
9
+ import * as RechartsPrimitive from "recharts"
10
+ import type { TooltipValueType } from "recharts"
11
+
12
+ import { cn } from "./utils"
13
+
14
+ // Format: { THEME_NAME: CSS_SELECTOR }
15
+ const THEMES = { light: "", dark: ".dark" } as const
16
+
17
+ const INITIAL_DIMENSION = { width: 320, height: 200 } as const
18
+ type TooltipNameType = number | string
19
+
20
+ export type ChartConfig = Record<
21
+ string,
22
+ {
23
+ label?: React.ReactNode
24
+ icon?: React.ComponentType
25
+ } & (
26
+ | { color?: string; theme?: never }
27
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
28
+ )
29
+ >
30
+
31
+ type ChartContextProps = {
32
+ config: ChartConfig
33
+ }
34
+
35
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
36
+
37
+ function useChart() {
38
+ const context = React.useContext(ChartContext)
39
+
40
+ if (!context) {
41
+ throw new Error("useChart must be used within a <ChartContainer />")
42
+ }
43
+
44
+ return context
45
+ }
46
+
47
+ function ChartContainer({
48
+ id,
49
+ className,
50
+ children,
51
+ config,
52
+ initialDimension = INITIAL_DIMENSION,
53
+ ...props
54
+ }: React.ComponentProps<"div"> & {
55
+ config: ChartConfig
56
+ children: React.ComponentProps<
57
+ typeof RechartsPrimitive.ResponsiveContainer
58
+ >["children"]
59
+ initialDimension?: {
60
+ width: number
61
+ height: number
62
+ }
63
+ }) {
64
+ const uniqueId = React.useId()
65
+ const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`
66
+
67
+ return (
68
+ <ChartContext.Provider value={{ config }}>
69
+ <div
70
+ data-slot="chart"
71
+ data-chart={chartId}
72
+ className={cn(
73
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
74
+ className
75
+ )}
76
+ {...props}
77
+ >
78
+ <ChartStyle id={chartId} config={config} />
79
+ <RechartsPrimitive.ResponsiveContainer
80
+ initialDimension={initialDimension}
81
+ >
82
+ {children}
83
+ </RechartsPrimitive.ResponsiveContainer>
84
+ </div>
85
+ </ChartContext.Provider>
86
+ )
87
+ }
88
+
89
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
90
+ const colorConfig = Object.entries(config).filter(
91
+ ([, config]) => config.theme ?? config.color
92
+ )
93
+
94
+ if (!colorConfig.length) {
95
+ return null
96
+ }
97
+
98
+ return (
99
+ <style
100
+ dangerouslySetInnerHTML={{
101
+ __html: Object.entries(THEMES)
102
+ .map(
103
+ ([theme, prefix]) => `
104
+ ${prefix} [data-chart=${id}] {
105
+ ${colorConfig
106
+ .map(([key, itemConfig]) => {
107
+ const color =
108
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
109
+ itemConfig.color
110
+ return color ? ` --color-${key}: ${color};` : null
111
+ })
112
+ .join("\n")}
113
+ }
114
+ `
115
+ )
116
+ .join("\n"),
117
+ }}
118
+ />
119
+ )
120
+ }
121
+
122
+ const ChartTooltip = RechartsPrimitive.Tooltip
123
+
124
+ function ChartTooltipContent({
125
+ active,
126
+ payload,
127
+ className,
128
+ indicator = "dot",
129
+ hideLabel = false,
130
+ hideIndicator = false,
131
+ label,
132
+ labelFormatter,
133
+ labelClassName,
134
+ formatter,
135
+ color,
136
+ nameKey,
137
+ labelKey,
138
+ }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
139
+ React.ComponentProps<"div"> & {
140
+ hideLabel?: boolean
141
+ hideIndicator?: boolean
142
+ indicator?: "line" | "dot" | "dashed"
143
+ nameKey?: string
144
+ labelKey?: string
145
+ } & Omit<
146
+ RechartsPrimitive.DefaultTooltipContentProps<
147
+ TooltipValueType,
148
+ TooltipNameType
149
+ >,
150
+ "accessibilityLayer"
151
+ >) {
152
+ const { config } = useChart()
153
+
154
+ const tooltipLabel = React.useMemo(() => {
155
+ if (hideLabel || !payload?.length) {
156
+ return null
157
+ }
158
+
159
+ const [item] = payload
160
+ const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`
161
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
162
+ const value =
163
+ !labelKey && typeof label === "string"
164
+ ? (config[label]?.label ?? label)
165
+ : itemConfig?.label
166
+
167
+ if (labelFormatter) {
168
+ return (
169
+ <div className={cn("font-medium", labelClassName)}>
170
+ {labelFormatter(value, payload)}
171
+ </div>
172
+ )
173
+ }
174
+
175
+ if (!value) {
176
+ return null
177
+ }
178
+
179
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
180
+ }, [
181
+ label,
182
+ labelFormatter,
183
+ payload,
184
+ hideLabel,
185
+ labelClassName,
186
+ config,
187
+ labelKey,
188
+ ])
189
+
190
+ if (!active || !payload?.length) {
191
+ return null
192
+ }
193
+
194
+ const nestLabel = payload.length === 1 && indicator !== "dot"
195
+
196
+ return (
197
+ <div
198
+ className={cn(
199
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
200
+ className
201
+ )}
202
+ >
203
+ {!nestLabel ? tooltipLabel : null}
204
+ <div className="grid gap-1.5">
205
+ {payload
206
+ .filter((item) => item.type !== "none")
207
+ .map((item, index) => {
208
+ const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`
209
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
210
+ const indicatorColor = color ?? item.payload?.fill ?? item.color
211
+
212
+ return (
213
+ <div
214
+ key={index}
215
+ className={cn(
216
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
217
+ indicator === "dot" && "items-center"
218
+ )}
219
+ >
220
+ {formatter && item?.value !== undefined && item.name ? (
221
+ formatter(item.value, item.name, item, index, item.payload)
222
+ ) : (
223
+ <>
224
+ {itemConfig?.icon ? (
225
+ <itemConfig.icon />
226
+ ) : (
227
+ !hideIndicator && (
228
+ <div
229
+ className={cn(
230
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
231
+ {
232
+ "h-2.5 w-2.5": indicator === "dot",
233
+ "w-1": indicator === "line",
234
+ "w-0 border-[1.5px] border-dashed bg-transparent":
235
+ indicator === "dashed",
236
+ "my-0.5": nestLabel && indicator === "dashed",
237
+ }
238
+ )}
239
+ style={
240
+ {
241
+ "--color-bg": indicatorColor,
242
+ "--color-border": indicatorColor,
243
+ } as React.CSSProperties
244
+ }
245
+ />
246
+ )
247
+ )}
248
+ <div
249
+ className={cn(
250
+ "flex flex-1 justify-between leading-none",
251
+ nestLabel ? "items-end" : "items-center"
252
+ )}
253
+ >
254
+ <div className="grid gap-1.5">
255
+ {nestLabel ? tooltipLabel : null}
256
+ <span className="text-muted-foreground">
257
+ {itemConfig?.label ?? item.name}
258
+ </span>
259
+ </div>
260
+ {item.value != null && (
261
+ <span className="font-mono font-medium text-foreground tabular-nums">
262
+ {typeof item.value === "number"
263
+ ? item.value.toLocaleString()
264
+ : String(item.value)}
265
+ </span>
266
+ )}
267
+ </div>
268
+ </>
269
+ )}
270
+ </div>
271
+ )
272
+ })}
273
+ </div>
274
+ </div>
275
+ )
276
+ }
277
+
278
+ const ChartLegend = RechartsPrimitive.Legend
279
+
280
+ function ChartLegendContent({
281
+ className,
282
+ hideIcon = false,
283
+ payload,
284
+ verticalAlign = "bottom",
285
+ nameKey,
286
+ }: React.ComponentProps<"div"> & {
287
+ hideIcon?: boolean
288
+ nameKey?: string
289
+ } & RechartsPrimitive.DefaultLegendContentProps) {
290
+ const { config } = useChart()
291
+
292
+ if (!payload?.length) {
293
+ return null
294
+ }
295
+
296
+ return (
297
+ <div
298
+ className={cn(
299
+ "flex items-center justify-center gap-4",
300
+ verticalAlign === "top" ? "pb-3" : "pt-3",
301
+ className
302
+ )}
303
+ >
304
+ {payload
305
+ .filter((item) => item.type !== "none")
306
+ .map((item, index) => {
307
+ const key = `${nameKey ?? item.dataKey ?? "value"}`
308
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
309
+
310
+ return (
311
+ <div
312
+ key={index}
313
+ className={cn(
314
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
315
+ )}
316
+ >
317
+ {itemConfig?.icon && !hideIcon ? (
318
+ <itemConfig.icon />
319
+ ) : (
320
+ <div
321
+ className="h-2 w-2 shrink-0 rounded-[2px]"
322
+ style={{
323
+ backgroundColor: item.color,
324
+ }}
325
+ />
326
+ )}
327
+ {itemConfig?.label}
328
+ </div>
329
+ )
330
+ })}
331
+ </div>
332
+ )
333
+ }
334
+
335
+ // Helper to extract item config from a payload.
336
+ function getPayloadConfigFromPayload(
337
+ config: ChartConfig,
338
+ payload: unknown,
339
+ key: string
340
+ ) {
341
+ if (typeof payload !== "object" || payload === null) {
342
+ return undefined
343
+ }
344
+
345
+ const payloadPayload =
346
+ "payload" in payload &&
347
+ typeof payload.payload === "object" &&
348
+ payload.payload !== null
349
+ ? payload.payload
350
+ : undefined
351
+
352
+ let configLabelKey: string = key
353
+
354
+ if (
355
+ key in payload &&
356
+ typeof payload[key as keyof typeof payload] === "string"
357
+ ) {
358
+ configLabelKey = payload[key as keyof typeof payload] as string
359
+ } else if (
360
+ payloadPayload &&
361
+ key in payloadPayload &&
362
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
363
+ ) {
364
+ configLabelKey = payloadPayload[
365
+ key as keyof typeof payloadPayload
366
+ ] as string
367
+ }
368
+
369
+ return configLabelKey in config ? config[configLabelKey] : config[key]
370
+ }
371
+
372
+ export {
373
+ ChartContainer,
374
+ ChartTooltip,
375
+ ChartTooltipContent,
376
+ ChartLegend,
377
+ ChartLegendContent,
378
+ ChartStyle,
379
+ }
@@ -0,0 +1,239 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
6
+ import { Check, ChevronDown, Search } from "lucide-react";
7
+ import { cn } from "./utils";
8
+
9
+ export interface CheckboxSelectOption {
10
+ value: string;
11
+ label: string;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export interface CheckboxSelectProps {
16
+ options: CheckboxSelectOption[];
17
+ value: string[];
18
+ onChange: (values: string[]) => void;
19
+ placeholder?: string;
20
+ showSearch?: boolean;
21
+ disabled?: boolean;
22
+ maxDisplay?: number;
23
+ className?: string;
24
+ }
25
+
26
+ function getTriggerLabel(
27
+ value: string[],
28
+ options: CheckboxSelectOption[],
29
+ placeholder: string,
30
+ maxDisplay: number
31
+ ): string {
32
+ if (value.length === 0) return placeholder;
33
+ if (value.length === options.length) return "All";
34
+ if (value.length <= maxDisplay) {
35
+ const labels = value
36
+ .map((v) => options.find((o) => o.value === v)?.label ?? v)
37
+ .filter(Boolean);
38
+ return labels.join(", ");
39
+ }
40
+ return `${value.length} selected`;
41
+ }
42
+
43
+ const CheckboxSelect = React.forwardRef<HTMLButtonElement, CheckboxSelectProps>(
44
+ (
45
+ {
46
+ options,
47
+ value,
48
+ onChange,
49
+ placeholder = "Select options",
50
+ showSearch = false,
51
+ disabled = false,
52
+ maxDisplay = 2,
53
+ className,
54
+ },
55
+ ref
56
+ ) => {
57
+ const [open, setOpen] = React.useState(false);
58
+ const [search, setSearch] = React.useState("");
59
+
60
+ const filtered = React.useMemo(() => {
61
+ if (!search.trim()) return options;
62
+ const lower = search.toLowerCase();
63
+ return options.filter((o) => o.label.toLowerCase().includes(lower));
64
+ }, [options, search]);
65
+
66
+ const allFilteredValues = filtered.filter((o) => !o.disabled).map((o) => o.value);
67
+ const allFilteredSelected =
68
+ allFilteredValues.length > 0 && allFilteredValues.every((v) => value.includes(v));
69
+ const someFilteredSelected = allFilteredValues.some((v) => value.includes(v));
70
+
71
+ function handleSelectAll() {
72
+ if (allFilteredSelected) {
73
+ onChange(value.filter((v) => !allFilteredValues.includes(v)));
74
+ } else {
75
+ const merged = Array.from(new Set([...value, ...allFilteredValues]));
76
+ onChange(merged);
77
+ }
78
+ }
79
+
80
+ function handleClearAll() {
81
+ onChange(value.filter((v) => !allFilteredValues.includes(v)));
82
+ }
83
+
84
+ function handleToggle(optValue: string) {
85
+ if (value.includes(optValue)) {
86
+ onChange(value.filter((v) => v !== optValue));
87
+ } else {
88
+ onChange([...value, optValue]);
89
+ }
90
+ }
91
+
92
+ const triggerLabel = getTriggerLabel(value, options, placeholder, maxDisplay);
93
+ const hasSelection = value.length > 0;
94
+
95
+ return (
96
+ <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
97
+ <PopoverPrimitive.Trigger asChild>
98
+ <button
99
+ ref={ref}
100
+ type="button"
101
+ disabled={disabled}
102
+ aria-expanded={open}
103
+ aria-haspopup="listbox"
104
+ className={cn(
105
+ "flex h-11 w-full items-center justify-between gap-2.5 rounded-lg border border-border bg-card px-4 py-2 text-[15px] shadow-sm outline-none",
106
+ "transition-colors duration-pg-fast ease-pg-standard",
107
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
108
+ "disabled:cursor-not-allowed disabled:opacity-50",
109
+ hasSelection ? "text-foreground" : "text-muted-foreground",
110
+ className
111
+ )}
112
+ >
113
+ <span className="truncate">{triggerLabel}</span>
114
+ <ChevronDown
115
+ className={cn(
116
+ "h-4 w-4 shrink-0 text-muted-foreground opacity-70 transition-transform duration-pg-fast ease-pg-standard",
117
+ open && "rotate-180"
118
+ )}
119
+ />
120
+ </button>
121
+ </PopoverPrimitive.Trigger>
122
+
123
+ <PopoverPrimitive.Portal>
124
+ <PopoverPrimitive.Content
125
+ align="start"
126
+ sideOffset={6}
127
+ className={cn(
128
+ "z-[120] min-w-[var(--radix-popover-trigger-width)] w-full rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none p-1",
129
+ "data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150"
130
+ )}
131
+ >
132
+ {showSearch && (
133
+ <div className="relative mb-1 px-1 pt-1">
134
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
135
+ <input
136
+ type="text"
137
+ value={search}
138
+ onChange={(e) => setSearch(e.target.value)}
139
+ placeholder="Search..."
140
+ className={cn(
141
+ "flex h-9 w-full rounded-md border border-border bg-card pl-8 pr-3 text-sm shadow-sm placeholder:text-muted-foreground",
142
+ "transition-colors duration-pg-fast ease-pg-standard",
143
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35"
144
+ )}
145
+ />
146
+ </div>
147
+ )}
148
+
149
+ <div className="flex items-center justify-between px-2 py-1.5">
150
+ <button
151
+ type="button"
152
+ onClick={handleSelectAll}
153
+ className={cn(
154
+ "text-xs font-medium transition-colors duration-pg-fast ease-pg-standard",
155
+ allFilteredSelected
156
+ ? "text-primary hover:text-primary/80"
157
+ : someFilteredSelected
158
+ ? "text-primary hover:text-primary/80"
159
+ : "text-muted-foreground hover:text-foreground"
160
+ )}
161
+ >
162
+ {allFilteredSelected ? "Deselect all" : "Select all"}
163
+ </button>
164
+ {value.length > 0 && (
165
+ <button
166
+ type="button"
167
+ onClick={handleClearAll}
168
+ className="text-xs font-medium text-muted-foreground transition-colors duration-pg-fast ease-pg-standard hover:text-foreground"
169
+ >
170
+ Clear
171
+ </button>
172
+ )}
173
+ </div>
174
+
175
+ <div className="my-0.5 h-px bg-border mx-1" />
176
+
177
+ <div className="max-h-60 overflow-y-auto py-0.5">
178
+ {filtered.length === 0 ? (
179
+ <div className="px-3 py-6 text-center text-sm text-muted-foreground">
180
+ No options found.
181
+ </div>
182
+ ) : (
183
+ filtered.map((option) => {
184
+ const checked = value.includes(option.value);
185
+ return (
186
+ <div
187
+ key={option.value}
188
+ role="option"
189
+ aria-selected={checked}
190
+ aria-disabled={option.disabled}
191
+ onClick={() => !option.disabled && handleToggle(option.value)}
192
+ className={cn(
193
+ "flex items-center gap-2.5 px-3 py-2 rounded-md text-sm select-none",
194
+ "transition-colors duration-pg-fast ease-pg-standard",
195
+ option.disabled
196
+ ? "cursor-not-allowed opacity-50"
197
+ : "cursor-pointer hover:bg-muted"
198
+ )}
199
+ >
200
+ <CheckboxPrimitive.Root
201
+ checked={checked}
202
+ disabled={option.disabled}
203
+ onCheckedChange={() => !option.disabled && handleToggle(option.value)}
204
+ onClick={(e) => e.stopPropagation()}
205
+ className={cn(
206
+ "peer h-4 w-4 shrink-0 rounded-md border border-border bg-card shadow-sm",
207
+ "transition-colors duration-pg-fast ease-pg-standard",
208
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
209
+ "disabled:cursor-not-allowed disabled:opacity-50",
210
+ "data-[state=checked]:bg-primary data-[state=checked]:border-primary"
211
+ )}
212
+ >
213
+ <CheckboxPrimitive.Indicator className="flex items-center justify-center text-primary-foreground">
214
+ <Check className="size-3" strokeWidth={3} />
215
+ </CheckboxPrimitive.Indicator>
216
+ </CheckboxPrimitive.Root>
217
+ <span
218
+ className={cn(
219
+ "leading-none",
220
+ checked ? "text-foreground" : "text-foreground"
221
+ )}
222
+ >
223
+ {option.label}
224
+ </span>
225
+ </div>
226
+ );
227
+ })
228
+ )}
229
+ </div>
230
+ </PopoverPrimitive.Content>
231
+ </PopoverPrimitive.Portal>
232
+ </PopoverPrimitive.Root>
233
+ );
234
+ }
235
+ );
236
+
237
+ CheckboxSelect.displayName = "CheckboxSelect";
238
+
239
+ export { CheckboxSelect };
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
+ import { Check, Minus } from "lucide-react";
6
+ import { cn } from "./utils";
7
+
8
+ export type CheckboxSize = "sm" | "md" | "lg";
9
+
10
+ export interface CheckboxProps extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
11
+ size?: CheckboxSize;
12
+ }
13
+
14
+ const sizeClasses: Record<CheckboxSize, string> = {
15
+ sm: "h-3.5 w-3.5 rounded",
16
+ md: "h-4 w-4 rounded-md",
17
+ lg: "h-[1.125rem] w-[1.125rem] rounded-md",
18
+ };
19
+
20
+ const iconSizes: Record<CheckboxSize, string> = {
21
+ sm: "size-2.5",
22
+ md: "size-3",
23
+ lg: "size-3.5",
24
+ };
25
+
26
+ const Checkbox = React.forwardRef<
27
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
28
+ CheckboxProps
29
+ >(({ className, size = "md", ...props }, ref) => (
30
+ <CheckboxPrimitive.Root
31
+ ref={ref}
32
+ className={cn(
33
+ "peer shrink-0 border border-border bg-card shadow-sm transition-colors duration-pg-fast ease-pg-standard",
34
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
35
+ "disabled:cursor-not-allowed disabled:opacity-50",
36
+ "data-[state=checked]:bg-primary data-[state=checked]:border-primary",
37
+ "data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary",
38
+ sizeClasses[size],
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ <CheckboxPrimitive.Indicator className="flex items-center justify-center text-primary-foreground">
44
+ {props.checked === "indeterminate" ? (
45
+ <Minus className={iconSizes[size]} strokeWidth={3} />
46
+ ) : (
47
+ <Check className={iconSizes[size]} strokeWidth={3} />
48
+ )}
49
+ </CheckboxPrimitive.Indicator>
50
+ </CheckboxPrimitive.Root>
51
+ ));
52
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
53
+
54
+ export { Checkbox };