@boyernick/standard-ui-react 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 (67) hide show
  1. package/README.md +9 -0
  2. package/package.json +46 -0
  3. package/src/accordion.tsx +84 -0
  4. package/src/alert-dialog.tsx +101 -0
  5. package/src/attachment.tsx +138 -0
  6. package/src/autocomplete.tsx +281 -0
  7. package/src/avatar.tsx +56 -0
  8. package/src/badge.tsx +28 -0
  9. package/src/brand.tsx +70 -0
  10. package/src/breadcrumb.tsx +81 -0
  11. package/src/button.tsx +129 -0
  12. package/src/calendar.tsx +84 -0
  13. package/src/card.tsx +54 -0
  14. package/src/carousel.tsx +253 -0
  15. package/src/chart.tsx +185 -0
  16. package/src/checkbox-group.tsx +14 -0
  17. package/src/checkbox.tsx +31 -0
  18. package/src/code-block.tsx +124 -0
  19. package/src/collapsible.tsx +61 -0
  20. package/src/combobox.tsx +324 -0
  21. package/src/command.tsx +377 -0
  22. package/src/context-menu.tsx +250 -0
  23. package/src/dialog.tsx +78 -0
  24. package/src/drawer.tsx +156 -0
  25. package/src/empty.tsx +52 -0
  26. package/src/field.tsx +75 -0
  27. package/src/fieldset.tsx +25 -0
  28. package/src/form.tsx +14 -0
  29. package/src/icons.tsx +91 -0
  30. package/src/illustrations.tsx +167 -0
  31. package/src/image-modal.tsx +110 -0
  32. package/src/index.ts +833 -0
  33. package/src/input.tsx +68 -0
  34. package/src/lib/cn.ts +3 -0
  35. package/src/lib/motion.ts +23 -0
  36. package/src/markdown-editor.tsx +302 -0
  37. package/src/menu.tsx +205 -0
  38. package/src/menubar.tsx +22 -0
  39. package/src/meter.tsx +61 -0
  40. package/src/navigation-menu.tsx +217 -0
  41. package/src/number-field.tsx +119 -0
  42. package/src/orb.tsx +33 -0
  43. package/src/otp-field.tsx +45 -0
  44. package/src/pagination.tsx +91 -0
  45. package/src/popover.tsx +92 -0
  46. package/src/preview-card.tsx +103 -0
  47. package/src/progress.tsx +60 -0
  48. package/src/radio.tsx +43 -0
  49. package/src/scroll-area.tsx +67 -0
  50. package/src/select.tsx +161 -0
  51. package/src/separator.tsx +17 -0
  52. package/src/sidebar.tsx +82 -0
  53. package/src/skeleton.tsx +32 -0
  54. package/src/slider.tsx +56 -0
  55. package/src/sounds.tsx +270 -0
  56. package/src/spinner.tsx +45 -0
  57. package/src/switch.tsx +19 -0
  58. package/src/table.tsx +81 -0
  59. package/src/tabs.tsx +62 -0
  60. package/src/text-animate.tsx +155 -0
  61. package/src/textarea.tsx +58 -0
  62. package/src/ticker.tsx +74 -0
  63. package/src/toast.tsx +142 -0
  64. package/src/toggle.tsx +32 -0
  65. package/src/toolbar.tsx +75 -0
  66. package/src/tooltip.tsx +55 -0
  67. package/src/video-player.tsx +241 -0
@@ -0,0 +1,253 @@
1
+ "use client"
2
+
3
+ import useEmblaCarousel, {
4
+ type UseEmblaCarouselType,
5
+ } from "embla-carousel-react"
6
+ import {
7
+ createContext,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useState,
12
+ type ComponentProps,
13
+ type KeyboardEvent,
14
+ } from "react"
15
+ import { Button } from "./button"
16
+ import { IconChevronRightSmall } from "./icons"
17
+ import { cn } from "./lib/cn"
18
+
19
+ type CarouselApi = UseEmblaCarouselType[1]
20
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
21
+ type CarouselOptions = UseCarouselParameters[0]
22
+ type CarouselPlugin = UseCarouselParameters[1]
23
+
24
+ type CarouselContextValue = {
25
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0]
26
+ api: CarouselApi
27
+ scrollPrev: () => void
28
+ scrollNext: () => void
29
+ canScrollPrev: boolean
30
+ canScrollNext: boolean
31
+ orientation: "horizontal" | "vertical"
32
+ }
33
+
34
+ const CarouselContext = createContext<CarouselContextValue | null>(null)
35
+
36
+ const useCarousel = () => {
37
+ const context = useContext(CarouselContext)
38
+ if (!context) {
39
+ throw new Error("Carousel components must be used within <Carousel>")
40
+ }
41
+ return context
42
+ }
43
+
44
+ export type CarouselProps = ComponentProps<"div"> & {
45
+ opts?: CarouselOptions
46
+ plugins?: CarouselPlugin
47
+ orientation?: "horizontal" | "vertical"
48
+ setApi?: (api: CarouselApi) => void
49
+ }
50
+
51
+ export type CarouselContentProps = ComponentProps<"div">
52
+ export type CarouselItemProps = ComponentProps<"div">
53
+ export type CarouselPreviousProps = ComponentProps<typeof Button>
54
+ export type CarouselNextProps = ComponentProps<typeof Button>
55
+
56
+ export const Carousel = ({
57
+ orientation = "horizontal",
58
+ opts,
59
+ setApi,
60
+ plugins,
61
+ className,
62
+ children,
63
+ onKeyDownCapture,
64
+ ...props
65
+ }: CarouselProps) => {
66
+ const [carouselRef, api] = useEmblaCarousel(
67
+ {
68
+ ...opts,
69
+ axis: orientation === "horizontal" ? "x" : "y",
70
+ },
71
+ plugins,
72
+ )
73
+ const [canScrollPrev, setCanScrollPrev] = useState(false)
74
+ const [canScrollNext, setCanScrollNext] = useState(false)
75
+
76
+ const handleSelect = useCallback((instance: CarouselApi) => {
77
+ if (!instance) return
78
+ setCanScrollPrev(instance.canScrollPrev())
79
+ setCanScrollNext(instance.canScrollNext())
80
+ }, [])
81
+
82
+ const scrollPrev = useCallback(() => {
83
+ api?.scrollPrev()
84
+ }, [api])
85
+
86
+ const scrollNext = useCallback(() => {
87
+ api?.scrollNext()
88
+ }, [api])
89
+
90
+ const handleKeyDown = useCallback(
91
+ (event: KeyboardEvent<HTMLDivElement>) => {
92
+ onKeyDownCapture?.(event)
93
+ if (event.defaultPrevented) return
94
+ if (event.key === "ArrowLeft") {
95
+ event.preventDefault()
96
+ scrollPrev()
97
+ } else if (event.key === "ArrowRight") {
98
+ event.preventDefault()
99
+ scrollNext()
100
+ }
101
+ },
102
+ [onKeyDownCapture, scrollNext, scrollPrev],
103
+ )
104
+
105
+ useEffect(() => {
106
+ if (!api || !setApi) return
107
+ setApi(api)
108
+ }, [api, setApi])
109
+
110
+ useEffect(() => {
111
+ if (!api) return
112
+ handleSelect(api)
113
+ api.on("reInit", handleSelect)
114
+ api.on("select", handleSelect)
115
+ return () => {
116
+ api.off("reInit", handleSelect)
117
+ api.off("select", handleSelect)
118
+ }
119
+ }, [api, handleSelect])
120
+
121
+ return (
122
+ <CarouselContext.Provider
123
+ value={{
124
+ carouselRef,
125
+ api,
126
+ scrollPrev,
127
+ scrollNext,
128
+ canScrollPrev,
129
+ canScrollNext,
130
+ orientation,
131
+ }}
132
+ >
133
+ <div
134
+ role="region"
135
+ aria-roledescription="carousel"
136
+ data-slot="carousel"
137
+ className={cn("relative", className)}
138
+ onKeyDownCapture={handleKeyDown}
139
+ {...props}
140
+ >
141
+ {children}
142
+ </div>
143
+ </CarouselContext.Provider>
144
+ )
145
+ }
146
+
147
+ export const CarouselContent = ({
148
+ className,
149
+ ...props
150
+ }: CarouselContentProps) => {
151
+ const { carouselRef, orientation } = useCarousel()
152
+
153
+ return (
154
+ <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-viewport">
155
+ <div
156
+ data-slot="carousel-content"
157
+ className={cn(
158
+ "flex",
159
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
160
+ className,
161
+ )}
162
+ {...props}
163
+ />
164
+ </div>
165
+ )
166
+ }
167
+
168
+ export const CarouselItem = ({ className, ...props }: CarouselItemProps) => {
169
+ const { orientation } = useCarousel()
170
+
171
+ return (
172
+ <div
173
+ role="group"
174
+ aria-roledescription="slide"
175
+ data-slot="carousel-item"
176
+ className={cn(
177
+ "min-w-0 shrink-0 grow-0 basis-full",
178
+ orientation === "horizontal" ? "pl-4" : "pt-4",
179
+ className,
180
+ )}
181
+ {...props}
182
+ />
183
+ )
184
+ }
185
+
186
+ export const CarouselPrevious = ({
187
+ className,
188
+ variant = "outline",
189
+ size = "md",
190
+ ...props
191
+ }: CarouselPreviousProps) => {
192
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
193
+
194
+ return (
195
+ <Button
196
+ type="button"
197
+ variant={variant}
198
+ size={size}
199
+ iconOnly
200
+ rounded
201
+ disabled={!canScrollPrev}
202
+ aria-label="Previous slide"
203
+ data-slot="carousel-previous"
204
+ className={cn(
205
+ "absolute size-9",
206
+ orientation === "horizontal"
207
+ ? "top-1/2 -left-12 -translate-y-1/2 active:!-translate-y-1/2"
208
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90 active:!-translate-x-1/2 active:!translate-y-0",
209
+ className,
210
+ )}
211
+ onClick={scrollPrev}
212
+ {...props}
213
+ >
214
+ <IconChevronRightSmall className="rotate-180" aria-hidden />
215
+ </Button>
216
+ )
217
+ }
218
+
219
+ export const CarouselNext = ({
220
+ className,
221
+ variant = "outline",
222
+ size = "md",
223
+ ...props
224
+ }: CarouselNextProps) => {
225
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
226
+
227
+ return (
228
+ <Button
229
+ type="button"
230
+ variant={variant}
231
+ size={size}
232
+ iconOnly
233
+ rounded
234
+ disabled={!canScrollNext}
235
+ aria-label="Next slide"
236
+ data-slot="carousel-next"
237
+ className={cn(
238
+ "absolute size-9",
239
+ orientation === "horizontal"
240
+ ? "top-1/2 -right-12 -translate-y-1/2 active:!-translate-y-1/2"
241
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 active:!-translate-x-1/2 active:!translate-y-0",
242
+ className,
243
+ )}
244
+ onClick={scrollNext}
245
+ {...props}
246
+ >
247
+ <IconChevronRightSmall aria-hidden />
248
+ </Button>
249
+ )
250
+ }
251
+
252
+ export type { CarouselApi }
253
+ export { useCarousel }
package/src/chart.tsx ADDED
@@ -0,0 +1,185 @@
1
+ "use client"
2
+
3
+ import type { ComponentProps, CSSProperties, ReactElement } from "react"
4
+ import {
5
+ Area,
6
+ AreaChart,
7
+ Bar,
8
+ BarChart,
9
+ CartesianGrid,
10
+ Legend as RechartsLegend,
11
+ Line,
12
+ LineChart,
13
+ ResponsiveContainer,
14
+ Tooltip as RechartsTooltip,
15
+ XAxis,
16
+ YAxis,
17
+ } from "recharts"
18
+ import { cn } from "./lib/cn"
19
+
20
+ const chartColorVars = {
21
+ "--color-chart-1": "rgb(var(--chart-1))",
22
+ "--color-chart-2": "rgb(var(--chart-2))",
23
+ "--color-chart-3": "rgb(var(--chart-3))",
24
+ "--color-chart-4": "rgb(var(--chart-4))",
25
+ "--color-chart-5": "rgb(var(--chart-5))",
26
+ "--color-chart-neutral": "rgb(var(--chart-neutral))",
27
+ } as CSSProperties
28
+
29
+ export type ChartContainerProps = ComponentProps<"div"> & {
30
+ children: ReactElement
31
+ }
32
+
33
+ export const ChartContainer = ({
34
+ className,
35
+ children,
36
+ style,
37
+ ...props
38
+ }: ChartContainerProps) => (
39
+ <div
40
+ className={cn(
41
+ "relative flex w-full min-h-[200px] justify-center text-xs text-fg-secondary [&_.recharts-cartesian-axis-tick_text]:fill-fg-tertiary [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border-primary/40 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border-primary [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-background-tertiary/50 [&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none",
42
+ className,
43
+ )}
44
+ style={{ ...chartColorVars, ...style }}
45
+ {...props}
46
+ >
47
+ <ResponsiveContainer width="100%" height="100%" minHeight={200}>
48
+ {children}
49
+ </ResponsiveContainer>
50
+ </div>
51
+ )
52
+
53
+ type ChartTooltipPayloadItem = {
54
+ name?: string | number
55
+ value?: string | number
56
+ color?: string
57
+ dataKey?: string | number
58
+ }
59
+
60
+ export type ChartTooltipContentProps = {
61
+ active?: boolean
62
+ label?: string | number
63
+ payload?: ChartTooltipPayloadItem[]
64
+ className?: string
65
+ indicator?: "dot" | "line"
66
+ }
67
+
68
+ export const ChartTooltipContent = ({
69
+ active,
70
+ label,
71
+ payload,
72
+ className,
73
+ indicator = "dot",
74
+ }: ChartTooltipContentProps) => {
75
+ if (!active || !payload?.length) return null
76
+
77
+ return (
78
+ <div
79
+ className={cn(
80
+ "rounded-md border border-border-primary bg-surface px-2.5 py-1.5 shadow-md",
81
+ className,
82
+ )}
83
+ >
84
+ {label != null ? (
85
+ <p className="text-xs mb-1 text-fg-tertiary">{label}</p>
86
+ ) : null}
87
+ <div className="flex flex-col gap-0.5">
88
+ {payload.map((item, index) => (
89
+ <div
90
+ key={`${item.dataKey ?? item.name ?? index}`}
91
+ className="flex items-center gap-2 text-xs text-fg-secondary"
92
+ >
93
+ <span
94
+ className={cn(
95
+ "shrink-0 rounded-full",
96
+ indicator === "line" ? "h-0.5 w-3" : "size-2",
97
+ )}
98
+ style={{ backgroundColor: item.color }}
99
+ />
100
+ <span className="truncate">{item.name}</span>
101
+ <span className="ml-auto tabular-nums text-fg-primary">
102
+ {item.value}
103
+ </span>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ )
109
+ }
110
+
111
+ export type ChartTooltipProps = ComponentProps<typeof RechartsTooltip>
112
+
113
+ export const ChartTooltip = ({
114
+ content,
115
+ cursor = { stroke: "var(--border-primary)", strokeWidth: 1 },
116
+ animationDuration = 150,
117
+ ...props
118
+ }: ChartTooltipProps) => (
119
+ <RechartsTooltip
120
+ cursor={cursor}
121
+ animationDuration={animationDuration}
122
+ content={content ?? <ChartTooltipContent />}
123
+ {...props}
124
+ />
125
+ )
126
+
127
+ export type ChartLegendContentProps = {
128
+ payload?: Array<{
129
+ value?: string
130
+ color?: string
131
+ dataKey?: string | number
132
+ }>
133
+ className?: string
134
+ }
135
+
136
+ export const ChartLegendContent = ({
137
+ payload,
138
+ className,
139
+ }: ChartLegendContentProps) => {
140
+ if (!payload?.length) return null
141
+
142
+ return (
143
+ <div
144
+ className={cn(
145
+ "flex flex-wrap items-center justify-center gap-3 pt-3",
146
+ className,
147
+ )}
148
+ >
149
+ {payload.map((item, index) => (
150
+ <div
151
+ key={`${item.dataKey ?? item.value ?? index}`}
152
+ className="flex items-center gap-1.5 text-xs text-fg-secondary"
153
+ >
154
+ <span
155
+ className="size-2 shrink-0 rounded-full"
156
+ style={{ backgroundColor: item.color }}
157
+ />
158
+ <span>{item.value}</span>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ export type ChartLegendProps = ComponentProps<typeof RechartsLegend>
166
+
167
+ export const ChartLegend = ({
168
+ content,
169
+ ...props
170
+ }: ChartLegendProps) => (
171
+ <RechartsLegend content={content ?? <ChartLegendContent />} {...props} />
172
+ )
173
+
174
+ export {
175
+ Area,
176
+ AreaChart,
177
+ Bar,
178
+ BarChart,
179
+ CartesianGrid,
180
+ Line,
181
+ LineChart,
182
+ ResponsiveContainer,
183
+ XAxis,
184
+ YAxis,
185
+ }
@@ -0,0 +1,14 @@
1
+ "use client"
2
+
3
+ import { CheckboxGroup as BaseCheckboxGroup } from "@base-ui/react/checkbox-group"
4
+ import type { ComponentProps } from "react"
5
+ import { cn } from "./lib/cn"
6
+
7
+ export type CheckboxGroupProps = ComponentProps<typeof BaseCheckboxGroup>
8
+
9
+ export const CheckboxGroup = ({ className, ...props }: CheckboxGroupProps) => (
10
+ <BaseCheckboxGroup
11
+ className={cn("flex flex-col gap-2", className)}
12
+ {...props}
13
+ />
14
+ )
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox"
4
+ import type { ComponentProps } from "react"
5
+ import { IconCheckmark1, IconMinus } from "./icons"
6
+ import { cn } from "./lib/cn"
7
+
8
+ export type CheckboxProps = ComponentProps<typeof BaseCheckbox.Root>
9
+
10
+ export const Checkbox = ({ className, ...props }: CheckboxProps) => (
11
+ <BaseCheckbox.Root
12
+ className={cn(
13
+ "group flex size-4 shrink-0 items-center justify-center rounded-sm border border-border-secondary bg-surface transition-colors duration-150 ease-out motion-reduce:transition-none outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-checked:border-brand-primary data-checked:bg-brand-primary data-indeterminate:border-brand-primary data-indeterminate:bg-brand-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
14
+ className,
15
+ )}
16
+ {...props}
17
+ >
18
+ <BaseCheckbox.Indicator className="flex text-brand-foreground data-unchecked:hidden">
19
+ <IconCheckmark1
20
+ size={12}
21
+ className="size-3 group-data-indeterminate:hidden"
22
+ aria-hidden
23
+ />
24
+ <IconMinus
25
+ size={12}
26
+ className="hidden size-3 group-data-indeterminate:block"
27
+ aria-hidden
28
+ />
29
+ </BaseCheckbox.Indicator>
30
+ </BaseCheckbox.Root>
31
+ )
@@ -0,0 +1,124 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { highlight, type LanguageName } from "sugar-high"
5
+ import { Button } from "./button"
6
+ import { IconCheckmark1, IconSquareBehindSquare6 } from "./icons"
7
+
8
+ export type CodeBlockProps = {
9
+ code: string
10
+ /** Label shown in the header (for example, tsx). TSX uses TypeScript highlighting. */
11
+ lang?: string
12
+ size?: "sm" | "md"
13
+ showHeader?: boolean
14
+ /** Removes border and radius chrome for nesting inside another frame. */
15
+ bare?: boolean
16
+ className?: string
17
+ }
18
+
19
+ const sizeClass = {
20
+ sm: "text-2xs p-3",
21
+ md: "text-xs p-4",
22
+ } as const
23
+
24
+ const languages = new Set<LanguageName>([
25
+ "javascript",
26
+ "typescript",
27
+ "css",
28
+ "python",
29
+ "c",
30
+ "go",
31
+ "java",
32
+ "rust",
33
+ "json",
34
+ "diff",
35
+ "shell",
36
+ "cpp",
37
+ "csharp",
38
+ "sql",
39
+ "html",
40
+ "yaml",
41
+ "markdown",
42
+ "kotlin",
43
+ "swift",
44
+ "php",
45
+ "toml",
46
+ "powershell",
47
+ "dockerfile",
48
+ "graphql",
49
+ "hcl",
50
+ ])
51
+
52
+ const getHighlightLanguage = (lang: string): LanguageName => {
53
+ const key = lang.toLowerCase()
54
+
55
+ if (key === "tsx" || key === "ts") return "typescript"
56
+ if (key === "jsx" || key === "js") return "javascript"
57
+ if (languages.has(key as LanguageName)) return key as LanguageName
58
+
59
+ return "typescript"
60
+ }
61
+
62
+ export const CodeBlock = ({
63
+ code: rawCode,
64
+ lang = "tsx",
65
+ size = "md",
66
+ showHeader = true,
67
+ bare = false,
68
+ className = "",
69
+ }: CodeBlockProps) => {
70
+ const [copied, setCopied] = useState(false)
71
+ const code = rawCode.replace(/^\n/, "").replace(/\n$/, "")
72
+ const html = highlight(code, { lang: getHighlightLanguage(lang) })
73
+ const frameClass =
74
+ !showHeader && !bare
75
+ ? "rounded-lg border border-border-primary bg-surface"
76
+ : ""
77
+
78
+ const handleCopy = async () => {
79
+ try {
80
+ await navigator.clipboard.writeText(code)
81
+ setCopied(true)
82
+ window.setTimeout(() => setCopied(false), 1200)
83
+ } catch {
84
+ setCopied(false)
85
+ }
86
+ }
87
+
88
+ const codeContent = (
89
+ <pre
90
+ className={`sh-code overflow-x-auto font-mono ${sizeClass[size]} ${frameClass}`}
91
+ dangerouslySetInnerHTML={{ __html: html }}
92
+ />
93
+ )
94
+
95
+ if (!showHeader) {
96
+ return <div className={className}>{codeContent}</div>
97
+ }
98
+
99
+ return (
100
+ <div
101
+ className={`overflow-hidden rounded-xl border border-border-primary bg-surface ${className}`}
102
+ >
103
+ <div className="flex items-center justify-between border-b border-border-primary bg-background-secondary px-3 py-1.5 pl-4">
104
+ <p className="text-xs font-mono text-fg-quaternary">{lang}</p>
105
+ <Button
106
+ type="button"
107
+ variant="ghost"
108
+ iconOnly
109
+ size="sm"
110
+ className="size-6 cursor-copy text-fg-quaternary hover:text-fg-primary"
111
+ aria-label={copied ? "Copied" : "Copy code"}
112
+ onClick={handleCopy}
113
+ >
114
+ {copied ? (
115
+ <IconCheckmark1 size={14} aria-hidden />
116
+ ) : (
117
+ <IconSquareBehindSquare6 size={14} aria-hidden />
118
+ )}
119
+ </Button>
120
+ </div>
121
+ {codeContent}
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,61 @@
1
+ "use client"
2
+
3
+ import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible"
4
+ import type { ComponentProps } from "react"
5
+ import { IconChevronDownSmall } from "./icons"
6
+ import { cn } from "./lib/cn"
7
+ import { motion } from "./lib/motion"
8
+
9
+ export type CollapsibleProps = ComponentProps<typeof BaseCollapsible.Root>
10
+ export type CollapsibleTriggerProps = ComponentProps<
11
+ typeof BaseCollapsible.Trigger
12
+ >
13
+ export type CollapsiblePanelProps = ComponentProps<typeof BaseCollapsible.Panel>
14
+
15
+ export const Collapsible = (props: CollapsibleProps) => (
16
+ <BaseCollapsible.Root {...props} />
17
+ )
18
+
19
+ export const CollapsibleTrigger = ({
20
+ className,
21
+ children,
22
+ ...props
23
+ }: CollapsibleTriggerProps) => (
24
+ <BaseCollapsible.Trigger
25
+ className={cn(
26
+ "group flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-border-secondary bg-surface px-3 text-sm text-fg-primary inset-shadow-outline-top outline-none",
27
+ motion.colors,
28
+ "hover:bg-background-tertiary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-disabled:cursor-not-allowed data-disabled:opacity-50",
29
+ className,
30
+ )}
31
+ {...props}
32
+ >
33
+ <span className="min-w-0 flex-1 text-left">{children}</span>
34
+ <IconChevronDownSmall
35
+ size={16}
36
+ className={cn(
37
+ "size-4 shrink-0 text-fg-tertiary",
38
+ motion.transform,
39
+ "group-data-panel-open:rotate-180",
40
+ )}
41
+ aria-hidden
42
+ />
43
+ </BaseCollapsible.Trigger>
44
+ )
45
+
46
+ export const CollapsiblePanel = ({
47
+ className,
48
+ children,
49
+ ...props
50
+ }: CollapsiblePanelProps) => (
51
+ <BaseCollapsible.Panel
52
+ className={cn(
53
+ "h-[var(--collapsible-panel-height)] overflow-hidden text-sm text-fg-secondary",
54
+ motion.accordionPanel,
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ <div className="pt-2 leading-relaxed">{children}</div>
60
+ </BaseCollapsible.Panel>
61
+ )