@cntyclub/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/dist/chunk-HDGMSYQS.js +26461 -0
- package/dist/chunk-HDGMSYQS.js.map +1 -0
- package/dist/chunk-PR4QN5HX.js +39 -0
- package/dist/chunk-PR4QN5HX.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +5207 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +1462 -0
- package/dist/index.js +81862 -0
- package/dist/index.js.map +1 -0
- package/dist/input-CZvh825j.d.ts +24 -0
- package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
- package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
- package/package.json +79 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +95 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +744 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +143 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +229 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +125 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +300 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
|
|
8
|
+
function Progress({
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}: ProgressPrimitive.Root.Props) {
|
|
13
|
+
return (
|
|
14
|
+
<ProgressPrimitive.Root
|
|
15
|
+
className={cn("flex w-full flex-col gap-2", className)}
|
|
16
|
+
data-slot="progress"
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
{children ? (
|
|
20
|
+
children
|
|
21
|
+
) : (
|
|
22
|
+
<ProgressTrack>
|
|
23
|
+
<ProgressIndicator />
|
|
24
|
+
</ProgressTrack>
|
|
25
|
+
)}
|
|
26
|
+
</ProgressPrimitive.Root>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
|
|
31
|
+
return (
|
|
32
|
+
<ProgressPrimitive.Label
|
|
33
|
+
className={cn("font-medium text-sm", className)}
|
|
34
|
+
data-slot="progress-label"
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
|
|
41
|
+
return (
|
|
42
|
+
<ProgressPrimitive.Track
|
|
43
|
+
className={cn(
|
|
44
|
+
"block h-1.5 w-full overflow-hidden rounded-full bg-input",
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
data-slot="progress-track"
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ProgressIndicator({
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: ProgressPrimitive.Indicator.Props) {
|
|
57
|
+
return (
|
|
58
|
+
<ProgressPrimitive.Indicator
|
|
59
|
+
className={cn("bg-primary transition-all duration-500", className)}
|
|
60
|
+
data-slot="progress-indicator"
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
|
|
67
|
+
return (
|
|
68
|
+
<ProgressPrimitive.Value
|
|
69
|
+
className={cn("text-sm tabular-nums", className)}
|
|
70
|
+
data-slot="progress-value"
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A tooltip-like value bubble that floats above or below the track,
|
|
78
|
+
* following the progress percentage. Render it next to a ProgressTrack
|
|
79
|
+
* inside a `relative` wrapper.
|
|
80
|
+
*/
|
|
81
|
+
function ProgressFloatingValue({
|
|
82
|
+
className,
|
|
83
|
+
side = "top",
|
|
84
|
+
value,
|
|
85
|
+
max = 100,
|
|
86
|
+
children,
|
|
87
|
+
style,
|
|
88
|
+
...props
|
|
89
|
+
}: React.ComponentProps<"div"> & {
|
|
90
|
+
side?: "top" | "bottom";
|
|
91
|
+
value: number;
|
|
92
|
+
max?: number;
|
|
93
|
+
}) {
|
|
94
|
+
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className={cn(
|
|
98
|
+
"absolute -translate-x-1/2 transition-[left] duration-500",
|
|
99
|
+
side === "top" ? "bottom-full mb-2" : "top-full mt-2",
|
|
100
|
+
className,
|
|
101
|
+
)}
|
|
102
|
+
data-side={side}
|
|
103
|
+
data-slot="progress-floating-value"
|
|
104
|
+
style={{
|
|
105
|
+
left: `clamp(1.25rem, ${percent}%, calc(100% - 1.25rem))`,
|
|
106
|
+
...style,
|
|
107
|
+
}}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
<div className="rounded-md border bg-popover px-2 py-1 font-semibold text-popover-foreground text-xs tabular-nums shadow-md/5">
|
|
111
|
+
{children ?? `${Math.round(percent)}%`}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const progressCircleSizes = {
|
|
118
|
+
sm: { label: "text-[0.625rem]", px: 64, stroke: 6, value: "text-sm" },
|
|
119
|
+
md: { label: "text-xs", px: 120, stroke: 10, value: "text-2xl" },
|
|
120
|
+
lg: { label: "text-sm", px: 160, stroke: 12, value: "text-3xl" },
|
|
121
|
+
} as const;
|
|
122
|
+
|
|
123
|
+
type ProgressCircleSize = keyof typeof progressCircleSizes;
|
|
124
|
+
|
|
125
|
+
interface ProgressCircleProps extends ProgressPrimitive.Root.Props {
|
|
126
|
+
size?: ProgressCircleSize;
|
|
127
|
+
/** A short caption rendered above the value, inside the circle. */
|
|
128
|
+
label?: React.ReactNode;
|
|
129
|
+
/** Hide the centered percentage. */
|
|
130
|
+
showValue?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A circular progress ring with an optional centered value and label. */
|
|
134
|
+
function ProgressCircle({
|
|
135
|
+
className,
|
|
136
|
+
size = "md",
|
|
137
|
+
label,
|
|
138
|
+
showValue = true,
|
|
139
|
+
value,
|
|
140
|
+
max = 100,
|
|
141
|
+
...props
|
|
142
|
+
}: ProgressCircleProps) {
|
|
143
|
+
const { px, stroke, value: valueClass, label: labelClass } =
|
|
144
|
+
progressCircleSizes[size];
|
|
145
|
+
const radius = (px - stroke) / 2;
|
|
146
|
+
const percent = Math.min(100, Math.max(0, ((value ?? 0) / max) * 100));
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<ProgressPrimitive.Root
|
|
150
|
+
className={cn(
|
|
151
|
+
"relative inline-flex items-center justify-center",
|
|
152
|
+
className,
|
|
153
|
+
)}
|
|
154
|
+
data-slot="progress-circle"
|
|
155
|
+
max={max}
|
|
156
|
+
value={value}
|
|
157
|
+
{...props}
|
|
158
|
+
>
|
|
159
|
+
<svg
|
|
160
|
+
className="-rotate-90"
|
|
161
|
+
fill="none"
|
|
162
|
+
height={px}
|
|
163
|
+
viewBox={`0 0 ${px} ${px}`}
|
|
164
|
+
width={px}
|
|
165
|
+
>
|
|
166
|
+
<circle
|
|
167
|
+
className="text-input"
|
|
168
|
+
cx={px / 2}
|
|
169
|
+
cy={px / 2}
|
|
170
|
+
r={radius}
|
|
171
|
+
stroke="currentColor"
|
|
172
|
+
strokeWidth={stroke}
|
|
173
|
+
/>
|
|
174
|
+
<circle
|
|
175
|
+
className="text-primary transition-[stroke-dashoffset] duration-500"
|
|
176
|
+
cx={px / 2}
|
|
177
|
+
cy={px / 2}
|
|
178
|
+
pathLength={100}
|
|
179
|
+
r={radius}
|
|
180
|
+
stroke="currentColor"
|
|
181
|
+
strokeDasharray={100}
|
|
182
|
+
strokeDashoffset={100 - percent}
|
|
183
|
+
strokeLinecap="round"
|
|
184
|
+
strokeWidth={stroke}
|
|
185
|
+
/>
|
|
186
|
+
</svg>
|
|
187
|
+
{showValue || label ? (
|
|
188
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
189
|
+
{label ? (
|
|
190
|
+
<span className={cn("text-muted-foreground", labelClass)}>
|
|
191
|
+
{label}
|
|
192
|
+
</span>
|
|
193
|
+
) : null}
|
|
194
|
+
{showValue ? (
|
|
195
|
+
<ProgressPrimitive.Value
|
|
196
|
+
className={cn(
|
|
197
|
+
"font-semibold text-foreground tabular-nums",
|
|
198
|
+
valueClass,
|
|
199
|
+
)}
|
|
200
|
+
data-slot="progress-circle-value"
|
|
201
|
+
/>
|
|
202
|
+
) : null}
|
|
203
|
+
</div>
|
|
204
|
+
) : null}
|
|
205
|
+
</ProgressPrimitive.Root>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** A half-circle (gauge) progress arc with an optional value and label. */
|
|
210
|
+
function ProgressHalfCircle({
|
|
211
|
+
className,
|
|
212
|
+
size = "md",
|
|
213
|
+
label,
|
|
214
|
+
showValue = true,
|
|
215
|
+
value,
|
|
216
|
+
max = 100,
|
|
217
|
+
...props
|
|
218
|
+
}: ProgressCircleProps) {
|
|
219
|
+
const { px, stroke, value: valueClass, label: labelClass } =
|
|
220
|
+
progressCircleSizes[size];
|
|
221
|
+
const radius = (px - stroke) / 2;
|
|
222
|
+
const height = px / 2 + stroke / 2;
|
|
223
|
+
const arc = `M ${stroke / 2} ${px / 2} A ${radius} ${radius} 0 0 1 ${px - stroke / 2} ${px / 2}`;
|
|
224
|
+
const percent = Math.min(100, Math.max(0, ((value ?? 0) / max) * 100));
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<ProgressPrimitive.Root
|
|
228
|
+
className={cn("relative inline-flex justify-center", className)}
|
|
229
|
+
data-slot="progress-half-circle"
|
|
230
|
+
max={max}
|
|
231
|
+
value={value}
|
|
232
|
+
{...props}
|
|
233
|
+
>
|
|
234
|
+
<svg
|
|
235
|
+
fill="none"
|
|
236
|
+
height={height}
|
|
237
|
+
viewBox={`0 0 ${px} ${height}`}
|
|
238
|
+
width={px}
|
|
239
|
+
>
|
|
240
|
+
<path
|
|
241
|
+
className="text-input"
|
|
242
|
+
d={arc}
|
|
243
|
+
stroke="currentColor"
|
|
244
|
+
strokeLinecap="round"
|
|
245
|
+
strokeWidth={stroke}
|
|
246
|
+
/>
|
|
247
|
+
<path
|
|
248
|
+
className="text-primary transition-[stroke-dashoffset] duration-500"
|
|
249
|
+
d={arc}
|
|
250
|
+
pathLength={100}
|
|
251
|
+
stroke="currentColor"
|
|
252
|
+
strokeDasharray={100}
|
|
253
|
+
strokeDashoffset={100 - percent}
|
|
254
|
+
strokeLinecap="round"
|
|
255
|
+
strokeWidth={stroke}
|
|
256
|
+
/>
|
|
257
|
+
</svg>
|
|
258
|
+
{showValue || label ? (
|
|
259
|
+
<div className="absolute inset-x-0 bottom-0 flex flex-col items-center">
|
|
260
|
+
{label ? (
|
|
261
|
+
<span className={cn("text-muted-foreground", labelClass)}>
|
|
262
|
+
{label}
|
|
263
|
+
</span>
|
|
264
|
+
) : null}
|
|
265
|
+
{showValue ? (
|
|
266
|
+
<ProgressPrimitive.Value
|
|
267
|
+
className={cn(
|
|
268
|
+
"font-semibold text-foreground tabular-nums",
|
|
269
|
+
valueClass,
|
|
270
|
+
)}
|
|
271
|
+
data-slot="progress-half-circle-value"
|
|
272
|
+
/>
|
|
273
|
+
) : null}
|
|
274
|
+
</div>
|
|
275
|
+
) : null}
|
|
276
|
+
</ProgressPrimitive.Root>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export {
|
|
281
|
+
Progress,
|
|
282
|
+
ProgressLabel,
|
|
283
|
+
ProgressTrack,
|
|
284
|
+
ProgressIndicator,
|
|
285
|
+
ProgressValue,
|
|
286
|
+
ProgressFloatingValue,
|
|
287
|
+
ProgressCircle,
|
|
288
|
+
ProgressHalfCircle,
|
|
289
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import type { Options as QRCodeStylingOptions } from "qr-code-styling";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils/css";
|
|
8
|
+
|
|
9
|
+
const qrCodeVariants = cva(
|
|
10
|
+
"relative inline-flex shrink-0 items-center justify-center text-foreground [&_canvas]:size-full [&_svg]:size-full",
|
|
11
|
+
{
|
|
12
|
+
defaultVariants: { size: "md" },
|
|
13
|
+
variants: {
|
|
14
|
+
size: {
|
|
15
|
+
sm: "size-24",
|
|
16
|
+
md: "size-40",
|
|
17
|
+
lg: "size-60",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
interface QRCodeProps
|
|
24
|
+
extends Omit<React.ComponentProps<"div">, "children">,
|
|
25
|
+
VariantProps<typeof qrCodeVariants> {
|
|
26
|
+
/** The text or URL encoded in the QR code. */
|
|
27
|
+
value: string;
|
|
28
|
+
/**
|
|
29
|
+
* Extra qr-code-styling options merged over the themed defaults — e.g. a
|
|
30
|
+
* center image, custom dot colors or shapes. See the qr-code-styling docs.
|
|
31
|
+
*/
|
|
32
|
+
options?: Partial<QRCodeStylingOptions>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A themeable QR code rendered with qr-code-styling. Dots follow the
|
|
37
|
+
* foreground color (and re-tint when the theme flips) unless overridden
|
|
38
|
+
* via `options`.
|
|
39
|
+
*/
|
|
40
|
+
function QRCode({ className, size, value, options, ...props }: QRCodeProps) {
|
|
41
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
42
|
+
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
const container = containerRef.current;
|
|
45
|
+
if (!container) return;
|
|
46
|
+
|
|
47
|
+
let disposed = false;
|
|
48
|
+
let observer: MutationObserver | undefined;
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
dotsOptions,
|
|
52
|
+
cornersSquareOptions,
|
|
53
|
+
cornersDotOptions,
|
|
54
|
+
imageOptions,
|
|
55
|
+
...rest
|
|
56
|
+
} = options ?? {};
|
|
57
|
+
|
|
58
|
+
// The container's computed color is the resolved foreground token, so the
|
|
59
|
+
// code stays on-theme without hard-coding hex values.
|
|
60
|
+
const themedShapes = () => {
|
|
61
|
+
const color = getComputedStyle(container).color;
|
|
62
|
+
return {
|
|
63
|
+
cornersDotOptions: { color, ...cornersDotOptions },
|
|
64
|
+
cornersSquareOptions: {
|
|
65
|
+
color,
|
|
66
|
+
type: "extra-rounded" as const,
|
|
67
|
+
...cornersSquareOptions,
|
|
68
|
+
},
|
|
69
|
+
dotsOptions: { color, type: "rounded" as const, ...dotsOptions },
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// qr-code-styling touches the DOM at import time, so load it lazily to
|
|
74
|
+
// keep the component SSR-safe.
|
|
75
|
+
import("qr-code-styling").then(({ default: QRCodeStyling }) => {
|
|
76
|
+
if (disposed) return;
|
|
77
|
+
const edge = container.clientWidth || 160;
|
|
78
|
+
const qr = new QRCodeStyling({
|
|
79
|
+
backgroundOptions: { color: "transparent" },
|
|
80
|
+
data: value,
|
|
81
|
+
height: edge,
|
|
82
|
+
imageOptions: {
|
|
83
|
+
crossOrigin: "anonymous",
|
|
84
|
+
imageSize: 0.4,
|
|
85
|
+
margin: 4,
|
|
86
|
+
...imageOptions,
|
|
87
|
+
},
|
|
88
|
+
margin: 0,
|
|
89
|
+
qrOptions: { errorCorrectionLevel: "Q" },
|
|
90
|
+
type: "svg",
|
|
91
|
+
width: edge,
|
|
92
|
+
...themedShapes(),
|
|
93
|
+
...rest,
|
|
94
|
+
});
|
|
95
|
+
container.replaceChildren();
|
|
96
|
+
qr.append(container);
|
|
97
|
+
|
|
98
|
+
observer = new MutationObserver(() => qr.update(themedShapes()));
|
|
99
|
+
observer.observe(document.documentElement, {
|
|
100
|
+
attributeFilter: ["data-theme", "class"],
|
|
101
|
+
attributes: true,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
disposed = true;
|
|
107
|
+
observer?.disconnect();
|
|
108
|
+
container.replaceChildren();
|
|
109
|
+
};
|
|
110
|
+
}, [value, size, options]);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
aria-label={`QR code for ${value}`}
|
|
115
|
+
className={cn(qrCodeVariants({ size }), className)}
|
|
116
|
+
data-slot="qr-code"
|
|
117
|
+
ref={containerRef}
|
|
118
|
+
role="img"
|
|
119
|
+
{...props}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* An animated scan-line overlay for a QR code. Render it inside a `relative`
|
|
126
|
+
* wrapper around the `<QRCode />`.
|
|
127
|
+
*/
|
|
128
|
+
function QRCodeGradientScan({
|
|
129
|
+
className,
|
|
130
|
+
...props
|
|
131
|
+
}: React.ComponentProps<"div">) {
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
className={cn(
|
|
136
|
+
"pointer-events-none absolute inset-0 overflow-hidden rounded-lg",
|
|
137
|
+
className,
|
|
138
|
+
)}
|
|
139
|
+
data-slot="qr-code-gradient-scan"
|
|
140
|
+
{...props}
|
|
141
|
+
>
|
|
142
|
+
<div className="absolute inset-x-0 top-0 h-1/2 animate-qr-scan will-change-transform">
|
|
143
|
+
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-brand/20 to-transparent" />
|
|
144
|
+
<div className="absolute inset-x-0 top-1/2 h-px bg-brand/80 shadow-[0_0_10px_1px_var(--brand)]" />
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { QRCode, QRCodeGradientScan, qrCodeVariants };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
|
|
4
|
+
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
|
|
8
|
+
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
|
|
9
|
+
return (
|
|
10
|
+
<RadioGroupPrimitive
|
|
11
|
+
className={cn("flex flex-col gap-3", className)}
|
|
12
|
+
data-slot="radio-group"
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const radioSizes = {
|
|
19
|
+
sm: {
|
|
20
|
+
box: "size-4.5 sm:size-4",
|
|
21
|
+
indicator: "size-4.5 sm:size-4",
|
|
22
|
+
dot: "before:size-2 sm:before:size-1.5",
|
|
23
|
+
check: "size-3.5 sm:size-3",
|
|
24
|
+
},
|
|
25
|
+
md: {
|
|
26
|
+
box: "size-5",
|
|
27
|
+
indicator: "size-5",
|
|
28
|
+
dot: "before:size-2",
|
|
29
|
+
check: "size-4",
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
interface RadioProps extends RadioPrimitive.Root.Props {
|
|
34
|
+
/** Visual size of the radio. Defaults to "sm". */
|
|
35
|
+
size?: keyof typeof radioSizes;
|
|
36
|
+
/** Indicator style: the classic dot or a checkbox-style tick. Defaults to "dot". */
|
|
37
|
+
variant?: "dot" | "check";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Radio({
|
|
41
|
+
className,
|
|
42
|
+
size = "sm",
|
|
43
|
+
variant = "dot",
|
|
44
|
+
...props
|
|
45
|
+
}: RadioProps) {
|
|
46
|
+
const sizes = radioSizes[size];
|
|
47
|
+
return (
|
|
48
|
+
<RadioPrimitive.Root
|
|
49
|
+
className={cn(
|
|
50
|
+
"relative inline-flex shrink-0 items-center justify-center border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/6%)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/48 data-disabled:opacity-64 dark:not-data-checked:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],[data-checked],[aria-invalid]]:shadow-none",
|
|
51
|
+
variant === "check"
|
|
52
|
+
? "rounded-[4px] before:rounded-[3px]"
|
|
53
|
+
: "rounded-full before:rounded-full",
|
|
54
|
+
sizes.box,
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
data-slot="radio"
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
{variant === "check" ? (
|
|
61
|
+
<RadioPrimitive.Indicator
|
|
62
|
+
className="-inset-px absolute flex items-center justify-center rounded-[4px] text-primary-foreground transition-opacity duration-150 ease-out data-checked:bg-primary data-checked:opacity-100 data-unchecked:opacity-0"
|
|
63
|
+
data-slot="radio-indicator"
|
|
64
|
+
keepMounted
|
|
65
|
+
>
|
|
66
|
+
{/* biome-ignore lint/a11y/noSvgWithoutTitle: decorative indicator */}
|
|
67
|
+
<svg
|
|
68
|
+
className={sizes.check}
|
|
69
|
+
fill="none"
|
|
70
|
+
height="24"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
strokeLinecap="round"
|
|
73
|
+
strokeLinejoin="round"
|
|
74
|
+
strokeWidth="3"
|
|
75
|
+
viewBox="0 0 24 24"
|
|
76
|
+
width="24"
|
|
77
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
78
|
+
>
|
|
79
|
+
{/* Same draw-in motion as the Checkbox: the tick strokes itself in
|
|
80
|
+
a beat after the fill fades in. */}
|
|
81
|
+
<path
|
|
82
|
+
className="[stroke-dasharray:1] [stroke-dashoffset:1] [transition:stroke-dashoffset_250ms_cubic-bezier(0.65,0,0.35,1)_110ms] in-[[data-checked]]:[stroke-dashoffset:0]"
|
|
83
|
+
d="M5.252 12.7 10.2 18.63 18.748 5.37"
|
|
84
|
+
pathLength={1}
|
|
85
|
+
/>
|
|
86
|
+
</svg>
|
|
87
|
+
</RadioPrimitive.Indicator>
|
|
88
|
+
) : (
|
|
89
|
+
<RadioPrimitive.Indicator
|
|
90
|
+
className={cn(
|
|
91
|
+
"-inset-px absolute flex origin-center items-center justify-center rounded-full transition-[background-color,box-shadow] duration-150 ease-out before:rounded-full before:bg-primary-foreground before:transition-[transform,opacity] before:duration-200 before:ease-[cubic-bezier(0.34,1.56,0.64,1)] data-checked:bg-primary data-checked:before:scale-100 data-checked:before:opacity-100 data-unchecked:before:scale-0 data-unchecked:before:opacity-0",
|
|
92
|
+
sizes.indicator,
|
|
93
|
+
sizes.dot,
|
|
94
|
+
)}
|
|
95
|
+
data-slot="radio-indicator"
|
|
96
|
+
keepMounted
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
</RadioPrimitive.Root>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { RadioGroup, Radio, Radio as RadioGroupItem };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { GripVerticalIcon } from "lucide-react";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
import * as ResizablePrimitive from "react-resizable-panels";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils/css";
|
|
8
|
+
|
|
9
|
+
function ResizablePanelGroup({
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
|
13
|
+
return (
|
|
14
|
+
<ResizablePrimitive.PanelGroup
|
|
15
|
+
className={cn(
|
|
16
|
+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
data-slot="resizable-panel-group"
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ResizablePanel({
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
|
28
|
+
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ResizableHandle({
|
|
32
|
+
withHandle,
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
|
36
|
+
withHandle?: boolean;
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<ResizablePrimitive.PanelResizeHandle
|
|
40
|
+
className={cn(
|
|
41
|
+
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
data-slot="resizable-handle"
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{withHandle && (
|
|
48
|
+
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-xs border bg-border">
|
|
49
|
+
<GripVerticalIcon className="size-2.5" />
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
</ResizablePrimitive.PanelResizeHandle>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
import { useSmoothWheel } from "./smooth-scroll";
|
|
8
|
+
|
|
9
|
+
function ScrollArea({
|
|
10
|
+
className,
|
|
11
|
+
children,
|
|
12
|
+
scrollFade = false,
|
|
13
|
+
scrollbarGutter = false,
|
|
14
|
+
wheelToHorizontal = false,
|
|
15
|
+
smooth = false,
|
|
16
|
+
viewportClassName,
|
|
17
|
+
...props
|
|
18
|
+
}: ScrollAreaPrimitive.Root.Props & {
|
|
19
|
+
scrollFade?: boolean;
|
|
20
|
+
scrollbarGutter?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Translate vertical mouse-wheel scrolling into (eased) horizontal scrolling
|
|
23
|
+
* while the pointer is over the viewport. The page only scrolls once this
|
|
24
|
+
* element reaches its horizontal start/end.
|
|
25
|
+
*/
|
|
26
|
+
wheelToHorizontal?: boolean;
|
|
27
|
+
/** Eased, momentum-style vertical wheel scrolling. */
|
|
28
|
+
smooth?: boolean;
|
|
29
|
+
viewportClassName?: string;
|
|
30
|
+
}) {
|
|
31
|
+
const viewportRef = React.useRef<HTMLDivElement>(null);
|
|
32
|
+
|
|
33
|
+
// Eased horizontal scrolling (smooth + slower than a raw scrollLeft jump).
|
|
34
|
+
useSmoothWheel(viewportRef, { enabled: wheelToHorizontal, horizontal: true });
|
|
35
|
+
// Optional eased vertical scrolling.
|
|
36
|
+
useSmoothWheel(viewportRef, {
|
|
37
|
+
enabled: smooth && !wheelToHorizontal,
|
|
38
|
+
horizontal: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ScrollAreaPrimitive.Root
|
|
43
|
+
className={cn("flex flex-col min-h-0", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<ScrollAreaPrimitive.Viewport
|
|
47
|
+
ref={viewportRef}
|
|
48
|
+
className={cn(
|
|
49
|
+
"flex grow flex-col min-h-0 rounded-[inherit] outline-none transition-shadows focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-has-overflow-x:overscroll-x-contain data-has-overflow-y:overscroll-y-contain",
|
|
50
|
+
scrollFade &&
|
|
51
|
+
"mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] [--fade-size:1.5rem]",
|
|
52
|
+
scrollbarGutter &&
|
|
53
|
+
"data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
|
|
54
|
+
viewportClassName,
|
|
55
|
+
)}
|
|
56
|
+
data-slot="scroll-area-viewport"
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</ScrollAreaPrimitive.Viewport>
|
|
60
|
+
<ScrollBar orientation="vertical" />
|
|
61
|
+
<ScrollBar orientation="horizontal" />
|
|
62
|
+
<ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
|
|
63
|
+
</ScrollAreaPrimitive.Root>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ScrollBar({
|
|
68
|
+
className,
|
|
69
|
+
orientation = "vertical",
|
|
70
|
+
...props
|
|
71
|
+
}: ScrollAreaPrimitive.Scrollbar.Props) {
|
|
72
|
+
return (
|
|
73
|
+
<ScrollAreaPrimitive.Scrollbar
|
|
74
|
+
className={cn(
|
|
75
|
+
"m-1 flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
data-slot="scroll-area-scrollbar"
|
|
79
|
+
orientation={orientation}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
<ScrollAreaPrimitive.Thumb
|
|
83
|
+
className="relative flex-1 rounded-full bg-foreground/20"
|
|
84
|
+
data-slot="scroll-area-thumb"
|
|
85
|
+
/>
|
|
86
|
+
</ScrollAreaPrimitive.Scrollbar>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { ScrollArea, ScrollBar };
|