banhaten 0.1.1 → 0.1.2

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 (37) hide show
  1. package/README.md +20 -8
  2. package/package.json +8 -2
  3. package/registry/components/autocomplete.tsx +637 -0
  4. package/registry/components/avatar.tsx +258 -22
  5. package/registry/components/badge.tsx +97 -35
  6. package/registry/components/date-picker-state.ts +253 -0
  7. package/registry/components/date-picker.tsx +115 -158
  8. package/registry/components/expanded/EmptyState.tsx +155 -0
  9. package/registry/components/expanded/emptyState.css +111 -0
  10. package/registry/components/expanded/slideout.css +1 -0
  11. package/registry/components/expanded/table.css +1 -0
  12. package/registry/components/input-otp.tsx +574 -0
  13. package/registry/components/input.tsx +21 -11
  14. package/registry/components/menu.tsx +371 -8
  15. package/registry/components/popover.tsx +840 -0
  16. package/registry/components/select.tsx +4 -0
  17. package/registry/components/skeleton.css +57 -0
  18. package/registry/components/skeleton.tsx +482 -0
  19. package/registry/components/spinner.tsx +79 -11
  20. package/registry/components/textarea.tsx +1 -1
  21. package/registry/components/tooltip.tsx +4 -0
  22. package/registry/examples/autocomplete-demo.tsx +109 -0
  23. package/registry/examples/avatar-demo.tsx +102 -47
  24. package/registry/examples/badge-demo.tsx +16 -0
  25. package/registry/examples/expanded/command-bar-demo.tsx +236 -0
  26. package/registry/examples/expanded/empty-state-demo.tsx +39 -0
  27. package/registry/examples/input-demo.tsx +1 -1
  28. package/registry/examples/input-otp-demo.tsx +72 -0
  29. package/registry/examples/menu-demo.tsx +101 -88
  30. package/registry/examples/popover-demo.tsx +546 -0
  31. package/registry/examples/select-demo.tsx +1 -1
  32. package/registry/examples/skeleton-demo.tsx +56 -0
  33. package/registry/examples/spinner-demo.tsx +23 -1
  34. package/registry/examples/textarea-demo.tsx +1 -1
  35. package/registry/index.json +240 -8
  36. package/registry/styles/globals.css +88 -0
  37. package/src/cli/index.js +997 -62
@@ -293,10 +293,13 @@ const Select = React.forwardRef<HTMLDivElement, SelectProps>(function Select({
293
293
  type RadixSelectMenuContentProps = React.ComponentProps<typeof SelectPrimitive.Content>
294
294
  // Radix sideOffset requires a number; this mirrors --bh-select-menu-offset.
295
295
  const SELECT_MENU_SIDE_OFFSET_PX = 4
296
+ // Radix collisionPadding requires a number; this mirrors --bh-space-md-8.
297
+ const SELECT_MENU_COLLISION_PADDING_PX = 8
296
298
 
297
299
  function RadixSelectMenuContent({
298
300
  children,
299
301
  className,
302
+ collisionPadding = SELECT_MENU_COLLISION_PADDING_PX,
300
303
  position = "popper",
301
304
  sideOffset = SELECT_MENU_SIDE_OFFSET_PX,
302
305
  ...props
@@ -304,6 +307,7 @@ function RadixSelectMenuContent({
304
307
  return (
305
308
  <SelectPrimitive.Content
306
309
  asChild
310
+ collisionPadding={collisionPadding}
307
311
  position={position}
308
312
  sideOffset={sideOffset}
309
313
  {...props}
@@ -0,0 +1,57 @@
1
+ [data-slot="skeleton"] {
2
+ --bh-skeleton-bg: var(--bh-bg-muted);
3
+ --bh-skeleton-highlight: var(--bh-bg-raised-subtle);
4
+ --bh-skeleton-duration: 1.4s;
5
+ --bh-skeleton-radius: var(--bh-radius-md-6);
6
+ --bh-skeleton-shimmer-width: 62%;
7
+ --bh-skeleton-width: 100%;
8
+ --bh-skeleton-height: var(--bh-space-4xl-20);
9
+ }
10
+
11
+ [data-slot="skeleton"][data-animated="true"]::after {
12
+ animation: bh-skeleton-shimmer var(--bh-skeleton-duration) ease-in-out infinite;
13
+ background: linear-gradient(
14
+ 90deg,
15
+ var(--bh-alpha-alpha-transparent),
16
+ var(--bh-skeleton-highlight),
17
+ var(--bh-alpha-alpha-transparent)
18
+ );
19
+ content: "";
20
+ inset-block: 0;
21
+ inset-inline-start: 0;
22
+ pointer-events: none;
23
+ position: absolute;
24
+ transform: translateX(-100%);
25
+ width: var(--bh-skeleton-shimmer-width);
26
+ }
27
+
28
+ [dir="rtl"] [data-slot="skeleton"][data-animated="true"]::after,
29
+ [data-slot="skeleton"][dir="rtl"][data-animated="true"]::after {
30
+ animation-name: bh-skeleton-shimmer-rtl;
31
+ }
32
+
33
+ @keyframes bh-skeleton-shimmer {
34
+ from {
35
+ transform: translateX(-100%);
36
+ }
37
+
38
+ to {
39
+ transform: translateX(200%);
40
+ }
41
+ }
42
+
43
+ @keyframes bh-skeleton-shimmer-rtl {
44
+ from {
45
+ transform: translateX(100%);
46
+ }
47
+
48
+ to {
49
+ transform: translateX(-200%);
50
+ }
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ [data-slot="skeleton"][data-animated="true"]::after {
55
+ animation: none;
56
+ }
57
+ }
@@ -0,0 +1,482 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ import "./skeleton.css"
7
+
8
+ const skeletonVariants = cva(
9
+ "relative block min-w-0 shrink-0 overflow-hidden bg-[var(--bh-skeleton-bg)] h-[var(--bh-skeleton-height)] w-[var(--bh-skeleton-width)]",
10
+ {
11
+ variants: {
12
+ shape: {
13
+ block:
14
+ "rounded-[var(--bh-skeleton-radius)] [--bh-skeleton-radius:var(--bh-radius-md-6)]",
15
+ text:
16
+ "rounded-[var(--bh-radius-full)] [--bh-skeleton-height:var(--bh-text-body-md-regular-line-height)]",
17
+ circle:
18
+ "aspect-square rounded-[var(--bh-radius-full)] [--bh-skeleton-height:var(--bh-skeleton-width)]",
19
+ },
20
+ size: {
21
+ xs: "[--bh-skeleton-height:var(--bh-space-md-8)]",
22
+ sm: "[--bh-skeleton-height:var(--bh-space-xl-12)]",
23
+ md: "[--bh-skeleton-height:var(--bh-space-3xl-16)]",
24
+ lg: "[--bh-skeleton-height:var(--bh-space-5xl-24)]",
25
+ xl: "[--bh-skeleton-height:var(--bh-space-7xl-40)]",
26
+ },
27
+ tone: {
28
+ default:
29
+ "[--bh-skeleton-bg:var(--bh-bg-muted)] [--bh-skeleton-highlight:var(--bh-bg-raised-subtle)]",
30
+ subtle:
31
+ "[--bh-skeleton-bg:var(--bh-bg-subtle)] [--bh-skeleton-highlight:var(--bh-bg-default-hover)]",
32
+ surface:
33
+ "[--bh-skeleton-bg:var(--bh-bg-raised-subtle)] [--bh-skeleton-highlight:var(--bh-bg-default)]",
34
+ brand:
35
+ "[--bh-skeleton-bg:var(--bh-bg-brand-subtle)] [--bh-skeleton-highlight:var(--bh-bg-brand-soft)]",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ shape: "block",
40
+ size: "md",
41
+ tone: "default",
42
+ },
43
+ }
44
+ )
45
+
46
+ type SkeletonCssProperties = React.CSSProperties & {
47
+ "--bh-skeleton-height"?: React.CSSProperties["height"]
48
+ "--bh-skeleton-width"?: React.CSSProperties["width"]
49
+ }
50
+
51
+ type SkeletonProps = React.ComponentProps<"span"> &
52
+ VariantProps<typeof skeletonVariants> & {
53
+ animated?: boolean
54
+ }
55
+
56
+ const Skeleton = React.forwardRef<HTMLSpanElement, SkeletonProps>(function Skeleton(
57
+ {
58
+ animated = true,
59
+ "aria-hidden": ariaHidden = true,
60
+ className,
61
+ shape,
62
+ size,
63
+ tone,
64
+ ...props
65
+ },
66
+ ref
67
+ ) {
68
+ return (
69
+ <span
70
+ aria-hidden={ariaHidden}
71
+ data-animated={animated ? "true" : "false"}
72
+ data-slot="skeleton"
73
+ ref={ref}
74
+ className={cn(skeletonVariants({ shape, size, tone, className }))}
75
+ {...props}
76
+ />
77
+ )
78
+ })
79
+
80
+ type SkeletonTextProps = Omit<React.ComponentProps<"div">, "children"> & {
81
+ animated?: boolean
82
+ lineCount?: number
83
+ lineSize?: SkeletonProps["size"]
84
+ tone?: SkeletonProps["tone"]
85
+ widths?: Array<React.CSSProperties["width"]>
86
+ }
87
+
88
+ function SkeletonText({
89
+ animated,
90
+ className,
91
+ lineCount = 3,
92
+ lineSize = "md",
93
+ tone,
94
+ widths,
95
+ ...props
96
+ }: SkeletonTextProps) {
97
+ const count = Math.max(1, lineCount)
98
+
99
+ return (
100
+ <div
101
+ aria-hidden="true"
102
+ data-slot="skeleton-text"
103
+ className={cn("grid gap-[var(--bh-space-xs-4)]", className)}
104
+ {...props}
105
+ >
106
+ {Array.from({ length: count }, (_, index) => {
107
+ const width =
108
+ widths?.[index] ?? (index === count - 1 && count > 1 ? "66%" : "100%")
109
+
110
+ return (
111
+ <Skeleton
112
+ animated={animated}
113
+ key={index}
114
+ shape="text"
115
+ size={lineSize}
116
+ tone={tone}
117
+ style={{ "--bh-skeleton-width": width } as SkeletonCssProperties}
118
+ />
119
+ )
120
+ })}
121
+ </div>
122
+ )
123
+ }
124
+
125
+ const skeletonAvatarSizes = {
126
+ xs: "size-[var(--bh-space-5xl-24)]",
127
+ sm: "size-[var(--bh-space-6xl-32)]",
128
+ md: "size-[var(--bh-space-7xl-40)]",
129
+ lg: "size-[var(--bh-space-8xl-48)]",
130
+ xl: "size-[var(--bh-space-9xl-64)]",
131
+ } as const
132
+
133
+ type SkeletonAvatarProps = Omit<SkeletonProps, "shape" | "size"> & {
134
+ size?: keyof typeof skeletonAvatarSizes
135
+ }
136
+
137
+ function SkeletonAvatar({
138
+ className,
139
+ size = "md",
140
+ ...props
141
+ }: SkeletonAvatarProps) {
142
+ return (
143
+ <Skeleton
144
+ shape="circle"
145
+ className={cn(skeletonAvatarSizes[size], className)}
146
+ {...props}
147
+ />
148
+ )
149
+ }
150
+
151
+ const skeletonButtonSizes = {
152
+ sm: "h-[var(--bh-button-sm-height)] w-[var(--bh-space-11xl-96)]",
153
+ default: "h-[var(--bh-button-md-height)] w-[var(--bh-space-12xl-128)]",
154
+ lg: "h-[var(--bh-button-lg-height)] w-[var(--bh-space-13xl-160)]",
155
+ xl: "h-[var(--bh-button-xl-height)] w-[var(--bh-space-14xl-192)]",
156
+ icon: "size-[var(--bh-button-md-height)]",
157
+ } as const
158
+
159
+ type SkeletonButtonProps = Omit<SkeletonProps, "shape" | "size"> & {
160
+ size?: keyof typeof skeletonButtonSizes
161
+ }
162
+
163
+ function SkeletonButton({
164
+ className,
165
+ size = "default",
166
+ ...props
167
+ }: SkeletonButtonProps) {
168
+ return (
169
+ <Skeleton
170
+ shape="block"
171
+ className={cn(
172
+ "rounded-[var(--bh-control-default)]",
173
+ skeletonButtonSizes[size],
174
+ className
175
+ )}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+
181
+ type SkeletonInputProps = Omit<React.ComponentProps<"div">, "children"> & {
182
+ animated?: boolean
183
+ helper?: boolean
184
+ label?: boolean
185
+ tone?: SkeletonProps["tone"]
186
+ }
187
+
188
+ function SkeletonInput({
189
+ animated,
190
+ className,
191
+ helper = true,
192
+ label = true,
193
+ tone,
194
+ ...props
195
+ }: SkeletonInputProps) {
196
+ return (
197
+ <div
198
+ aria-hidden="true"
199
+ data-slot="skeleton-input"
200
+ className={cn("grid gap-[var(--bh-space-sm-6)]", className)}
201
+ {...props}
202
+ >
203
+ {label && (
204
+ <Skeleton
205
+ animated={animated}
206
+ shape="text"
207
+ size="sm"
208
+ tone={tone}
209
+ style={{ "--bh-skeleton-width": "42%" } as SkeletonCssProperties}
210
+ />
211
+ )}
212
+ <Skeleton
213
+ animated={animated}
214
+ className="rounded-[var(--bh-control-default)]"
215
+ size="xl"
216
+ tone={tone}
217
+ />
218
+ {helper && (
219
+ <Skeleton
220
+ animated={animated}
221
+ shape="text"
222
+ size="xs"
223
+ tone={tone}
224
+ style={{ "--bh-skeleton-width": "58%" } as SkeletonCssProperties}
225
+ />
226
+ )}
227
+ </div>
228
+ )
229
+ }
230
+
231
+ type SkeletonCardProps = Omit<React.ComponentProps<"div">, "children"> & {
232
+ actions?: boolean
233
+ animated?: boolean
234
+ avatar?: boolean
235
+ lines?: number
236
+ media?: boolean
237
+ tone?: SkeletonProps["tone"]
238
+ }
239
+
240
+ function SkeletonCard({
241
+ actions = true,
242
+ animated,
243
+ avatar = true,
244
+ className,
245
+ lines = 3,
246
+ media = true,
247
+ tone,
248
+ ...props
249
+ }: SkeletonCardProps) {
250
+ return (
251
+ <div
252
+ aria-hidden="true"
253
+ data-slot="skeleton-card"
254
+ className={cn(
255
+ "grid gap-[var(--bh-space-4xl-20)] rounded-[var(--bh-radius-xl-10)] border border-[var(--bh-border-default)] bg-[var(--bh-bg-raised)] p-[var(--bh-card-desktop-content-padding)] shadow-[var(--shadow-component-default)]",
256
+ className
257
+ )}
258
+ {...props}
259
+ >
260
+ {media && (
261
+ <Skeleton
262
+ animated={animated}
263
+ className="aspect-[16/9] rounded-[var(--bh-radius-lg-8)]"
264
+ size="xl"
265
+ tone={tone}
266
+ />
267
+ )}
268
+ <div className="grid grid-cols-[auto_minmax(0,1fr)] gap-[var(--bh-space-xl-12)]">
269
+ {avatar && <SkeletonAvatar animated={animated} tone={tone} />}
270
+ <SkeletonText animated={animated} lineCount={lines} tone={tone} />
271
+ </div>
272
+ {actions && (
273
+ <div className="flex flex-wrap gap-[var(--bh-space-md-8)]">
274
+ <SkeletonButton animated={animated} size="sm" tone={tone} />
275
+ <SkeletonButton animated={animated} size="sm" tone={tone} />
276
+ </div>
277
+ )}
278
+ </div>
279
+ )
280
+ }
281
+
282
+ type SkeletonListProps = Omit<React.ComponentProps<"div">, "children"> & {
283
+ animated?: boolean
284
+ rows?: number
285
+ tone?: SkeletonProps["tone"]
286
+ withAvatar?: boolean
287
+ withMeta?: boolean
288
+ }
289
+
290
+ function SkeletonList({
291
+ animated,
292
+ className,
293
+ rows = 4,
294
+ tone,
295
+ withAvatar = true,
296
+ withMeta = true,
297
+ ...props
298
+ }: SkeletonListProps) {
299
+ const rowCount = Math.max(1, rows)
300
+
301
+ return (
302
+ <div
303
+ aria-hidden="true"
304
+ data-slot="skeleton-list"
305
+ className={cn(
306
+ "overflow-hidden rounded-[var(--bh-radius-lg-8)] border border-[var(--bh-border-default)] bg-[var(--bh-bg-raised)]",
307
+ className
308
+ )}
309
+ {...props}
310
+ >
311
+ {Array.from({ length: rowCount }, (_, index) => (
312
+ <div
313
+ className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-[var(--bh-space-xl-12)] border-b border-[var(--bh-border-subtle)] p-[var(--bh-space-3xl-16)] last:border-b-0"
314
+ key={index}
315
+ >
316
+ {withAvatar && <SkeletonAvatar animated={animated} size="sm" tone={tone} />}
317
+ <SkeletonText
318
+ animated={animated}
319
+ lineCount={2}
320
+ lineSize="sm"
321
+ tone={tone}
322
+ widths={["74%", "46%"]}
323
+ />
324
+ {withMeta && (
325
+ <Skeleton
326
+ animated={animated}
327
+ shape="text"
328
+ size="sm"
329
+ tone={tone}
330
+ style={{ "--bh-skeleton-width": "64%" } as SkeletonCssProperties}
331
+ />
332
+ )}
333
+ </div>
334
+ ))}
335
+ </div>
336
+ )
337
+ }
338
+
339
+ type SkeletonTableProps = Omit<React.ComponentProps<"div">, "children"> & {
340
+ animated?: boolean
341
+ columns?: number
342
+ rows?: number
343
+ showHeader?: boolean
344
+ tone?: SkeletonProps["tone"]
345
+ }
346
+
347
+ function SkeletonTable({
348
+ animated,
349
+ className,
350
+ columns = 4,
351
+ rows = 5,
352
+ showHeader = true,
353
+ tone,
354
+ ...props
355
+ }: SkeletonTableProps) {
356
+ const columnCount = Math.max(1, columns)
357
+ const rowCount = Math.max(1, rows)
358
+ const gridStyle = {
359
+ "--bh-skeleton-table-columns": `repeat(${columnCount}, minmax(0, 1fr))`,
360
+ } as React.CSSProperties
361
+
362
+ return (
363
+ <div
364
+ aria-hidden="true"
365
+ data-slot="skeleton-table"
366
+ className={cn(
367
+ "overflow-hidden rounded-[var(--bh-radius-lg-8)] border border-[var(--bh-border-default)] bg-[var(--bh-bg-raised)]",
368
+ className
369
+ )}
370
+ {...props}
371
+ >
372
+ {showHeader && (
373
+ <div
374
+ className="grid grid-cols-[var(--bh-skeleton-table-columns)] gap-[var(--bh-space-3xl-16)] border-b border-[var(--bh-border-default)] bg-[var(--bh-bg-muted)] p-[var(--bh-space-xl-12)]"
375
+ style={gridStyle}
376
+ >
377
+ {Array.from({ length: columnCount }, (_, index) => (
378
+ <Skeleton
379
+ animated={animated}
380
+ key={index}
381
+ shape="text"
382
+ size="sm"
383
+ tone={tone}
384
+ />
385
+ ))}
386
+ </div>
387
+ )}
388
+ {Array.from({ length: rowCount }, (_, rowIndex) => (
389
+ <div
390
+ className="grid grid-cols-[var(--bh-skeleton-table-columns)] items-center gap-[var(--bh-space-3xl-16)] border-b border-[var(--bh-border-subtle)] p-[var(--bh-space-xl-12)] last:border-b-0"
391
+ key={rowIndex}
392
+ style={gridStyle}
393
+ >
394
+ {Array.from({ length: columnCount }, (_, columnIndex) => (
395
+ <Skeleton
396
+ animated={animated}
397
+ key={columnIndex}
398
+ shape="text"
399
+ size="sm"
400
+ tone={tone}
401
+ style={
402
+ {
403
+ "--bh-skeleton-width":
404
+ columnIndex === columnCount - 1 ? "58%" : "100%",
405
+ } as SkeletonCssProperties
406
+ }
407
+ />
408
+ ))}
409
+ </div>
410
+ ))}
411
+ </div>
412
+ )
413
+ }
414
+
415
+ type SkeletonFormProps = Omit<React.ComponentProps<"div">, "children"> & {
416
+ actions?: boolean
417
+ animated?: boolean
418
+ fields?: number
419
+ tone?: SkeletonProps["tone"]
420
+ }
421
+
422
+ function SkeletonForm({
423
+ actions = true,
424
+ animated,
425
+ className,
426
+ fields = 3,
427
+ tone,
428
+ ...props
429
+ }: SkeletonFormProps) {
430
+ const fieldCount = Math.max(1, fields)
431
+
432
+ return (
433
+ <div
434
+ aria-hidden="true"
435
+ data-slot="skeleton-form"
436
+ className={cn(
437
+ "grid gap-[var(--bh-space-4xl-20)] rounded-[var(--bh-radius-lg-8)] border border-[var(--bh-border-default)] bg-[var(--bh-bg-raised)] p-[var(--bh-space-5xl-24)]",
438
+ className
439
+ )}
440
+ {...props}
441
+ >
442
+ {Array.from({ length: fieldCount }, (_, index) => (
443
+ <SkeletonInput
444
+ animated={animated}
445
+ helper={index === fieldCount - 1}
446
+ key={index}
447
+ tone={tone}
448
+ />
449
+ ))}
450
+ {actions && (
451
+ <div className="flex flex-wrap justify-end gap-[var(--bh-space-md-8)]">
452
+ <SkeletonButton animated={animated} size="sm" tone={tone} />
453
+ <SkeletonButton animated={animated} tone={tone} />
454
+ </div>
455
+ )}
456
+ </div>
457
+ )
458
+ }
459
+
460
+ export {
461
+ Skeleton,
462
+ SkeletonAvatar,
463
+ SkeletonButton,
464
+ SkeletonCard,
465
+ SkeletonForm,
466
+ SkeletonInput,
467
+ SkeletonList,
468
+ SkeletonTable,
469
+ SkeletonText,
470
+ skeletonVariants,
471
+ }
472
+ export type {
473
+ SkeletonAvatarProps,
474
+ SkeletonButtonProps,
475
+ SkeletonCardProps,
476
+ SkeletonFormProps,
477
+ SkeletonInputProps,
478
+ SkeletonListProps,
479
+ SkeletonProps,
480
+ SkeletonTableProps,
481
+ SkeletonTextProps,
482
+ }
@@ -2,16 +2,39 @@ import * as React from "react"
2
2
 
3
3
  import { cn } from "@/lib/utils"
4
4
 
5
- function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
5
+ type SpinnerMotion = "dynamic" | "steady"
6
+
7
+ type SpinnerProps = React.ComponentProps<"svg">
8
+
9
+ type DynamicSpinnerProps = SpinnerProps
10
+
11
+ type SpinnerBaseProps = SpinnerProps & {
12
+ motion: SpinnerMotion
13
+ }
14
+
15
+ function Spinner(props: SpinnerProps) {
16
+ return <SpinnerBase {...props} motion="steady" />
17
+ }
18
+
19
+ function DynamicSpinner(props: DynamicSpinnerProps) {
20
+ return <SpinnerBase {...props} motion="dynamic" />
21
+ }
22
+
23
+ function SpinnerBase({ className, motion, ...props }: SpinnerBaseProps) {
6
24
  return (
7
25
  <svg
8
26
  aria-hidden="true"
27
+ data-motion={motion}
9
28
  data-slot="spinner"
10
29
  viewBox="0 0 24 24"
11
- className={cn("size-[var(--bh-spinner-size)] shrink-0 text-current", className)}
30
+ className={cn(
31
+ "size-[var(--bh-spinner-size)] shrink-0 text-current",
32
+ className
33
+ )}
12
34
  {...props}
13
35
  >
14
36
  <circle
37
+ data-slot="spinner-track"
15
38
  cx="12"
16
39
  cy="12"
17
40
  r="10"
@@ -21,6 +44,7 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
21
44
  opacity="var(--bh-opacity-25)"
22
45
  />
23
46
  <circle
47
+ data-slot="spinner-arc"
24
48
  cx="12"
25
49
  cy="12"
26
50
  r="10"
@@ -31,17 +55,61 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
31
55
  pathLength="100"
32
56
  strokeDasharray="30 70"
33
57
  >
34
- <animateTransform
35
- attributeName="transform"
36
- dur="1s"
37
- from="0 12 12"
38
- repeatCount="indefinite"
39
- to="360 12 12"
40
- type="rotate"
41
- />
58
+ {motion === "dynamic" ? (
59
+ <DynamicSpinnerMotion />
60
+ ) : (
61
+ <SteadySpinnerMotion />
62
+ )}
42
63
  </circle>
43
64
  </svg>
44
65
  )
45
66
  }
46
67
 
47
- export { Spinner }
68
+ function DynamicSpinnerMotion() {
69
+ return (
70
+ <>
71
+ <animate
72
+ attributeName="stroke-dasharray"
73
+ calcMode="spline"
74
+ dur="6s"
75
+ keySplines="0.45 0 0.55 1; 0.45 0 0.55 1; 0.45 0 0.55 1; 0.45 0 0.55 1"
76
+ keyTimes="0; 0.25; 0.5; 0.75; 1"
77
+ repeatCount="indefinite"
78
+ values="22 78; 38 62; 30 70; 42 58; 22 78"
79
+ />
80
+ <animate
81
+ attributeName="stroke-dashoffset"
82
+ calcMode="spline"
83
+ dur="6s"
84
+ keySplines="0.45 0 0.55 1; 0.45 0 0.55 1; 0.45 0 0.55 1; 0.45 0 0.55 1"
85
+ keyTimes="0; 0.25; 0.5; 0.75; 1"
86
+ repeatCount="indefinite"
87
+ values="0; -16; -44; -72; -100"
88
+ />
89
+ <animateTransform
90
+ attributeName="transform"
91
+ dur="6s"
92
+ from="0 12 12"
93
+ repeatCount="indefinite"
94
+ to="1080 12 12"
95
+ type="rotate"
96
+ />
97
+ </>
98
+ )
99
+ }
100
+
101
+ function SteadySpinnerMotion() {
102
+ return (
103
+ <animateTransform
104
+ attributeName="transform"
105
+ dur="1.5s"
106
+ from="0 12 12"
107
+ repeatCount="indefinite"
108
+ to="360 12 12"
109
+ type="rotate"
110
+ />
111
+ )
112
+ }
113
+
114
+ export { DynamicSpinner, Spinner }
115
+ export type { DynamicSpinnerProps, SpinnerProps }
@@ -100,7 +100,7 @@ const textareaControl = cva(
100
100
  variants: {
101
101
  type: {
102
102
  default:
103
- "min-h-[calc(var(--bh-textarea-min-height)_-_var(--bh-textarea-padding)_-_var(--bh-textarea-padding))]",
103
+ "min-h-[calc(var(--bh-textarea-min-height)_-_var(--bh-textarea-padding)_-_var(--bh-textarea-padding))] overflow-y-auto [scrollbar-gutter:stable]",
104
104
  tags:
105
105
  "h-[var(--bh-text-body-md-regular-line-height)] min-h-[var(--bh-text-body-md-regular-line-height)] overflow-hidden",
106
106
  },