@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.
- package/README.md +9 -0
- package/package.json +46 -0
- package/src/accordion.tsx +84 -0
- package/src/alert-dialog.tsx +101 -0
- package/src/attachment.tsx +138 -0
- package/src/autocomplete.tsx +281 -0
- package/src/avatar.tsx +56 -0
- package/src/badge.tsx +28 -0
- package/src/brand.tsx +70 -0
- package/src/breadcrumb.tsx +81 -0
- package/src/button.tsx +129 -0
- package/src/calendar.tsx +84 -0
- package/src/card.tsx +54 -0
- package/src/carousel.tsx +253 -0
- package/src/chart.tsx +185 -0
- package/src/checkbox-group.tsx +14 -0
- package/src/checkbox.tsx +31 -0
- package/src/code-block.tsx +124 -0
- package/src/collapsible.tsx +61 -0
- package/src/combobox.tsx +324 -0
- package/src/command.tsx +377 -0
- package/src/context-menu.tsx +250 -0
- package/src/dialog.tsx +78 -0
- package/src/drawer.tsx +156 -0
- package/src/empty.tsx +52 -0
- package/src/field.tsx +75 -0
- package/src/fieldset.tsx +25 -0
- package/src/form.tsx +14 -0
- package/src/icons.tsx +91 -0
- package/src/illustrations.tsx +167 -0
- package/src/image-modal.tsx +110 -0
- package/src/index.ts +833 -0
- package/src/input.tsx +68 -0
- package/src/lib/cn.ts +3 -0
- package/src/lib/motion.ts +23 -0
- package/src/markdown-editor.tsx +302 -0
- package/src/menu.tsx +205 -0
- package/src/menubar.tsx +22 -0
- package/src/meter.tsx +61 -0
- package/src/navigation-menu.tsx +217 -0
- package/src/number-field.tsx +119 -0
- package/src/orb.tsx +33 -0
- package/src/otp-field.tsx +45 -0
- package/src/pagination.tsx +91 -0
- package/src/popover.tsx +92 -0
- package/src/preview-card.tsx +103 -0
- package/src/progress.tsx +60 -0
- package/src/radio.tsx +43 -0
- package/src/scroll-area.tsx +67 -0
- package/src/select.tsx +161 -0
- package/src/separator.tsx +17 -0
- package/src/sidebar.tsx +82 -0
- package/src/skeleton.tsx +32 -0
- package/src/slider.tsx +56 -0
- package/src/sounds.tsx +270 -0
- package/src/spinner.tsx +45 -0
- package/src/switch.tsx +19 -0
- package/src/table.tsx +81 -0
- package/src/tabs.tsx +62 -0
- package/src/text-animate.tsx +155 -0
- package/src/textarea.tsx +58 -0
- package/src/ticker.tsx +74 -0
- package/src/toast.tsx +142 -0
- package/src/toggle.tsx +32 -0
- package/src/toolbar.tsx +75 -0
- package/src/tooltip.tsx +55 -0
- package/src/video-player.tsx +241 -0
package/src/carousel.tsx
ADDED
|
@@ -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
|
+
)
|
package/src/checkbox.tsx
ADDED
|
@@ -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
|
+
)
|