myoperator-ui 0.0.69 → 0.0.71
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/index.js +1860 -1780
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -200,91 +200,6 @@ function prefixTailwindClasses(content, prefix) {
|
|
|
200
200
|
}
|
|
201
201
|
async function getRegistry(prefix = "") {
|
|
202
202
|
return {
|
|
203
|
-
"badge": {
|
|
204
|
-
name: "badge",
|
|
205
|
-
description: "A status badge component with active, failed, and disabled variants",
|
|
206
|
-
dependencies: [
|
|
207
|
-
"class-variance-authority",
|
|
208
|
-
"clsx",
|
|
209
|
-
"tailwind-merge"
|
|
210
|
-
],
|
|
211
|
-
files: [
|
|
212
|
-
{
|
|
213
|
-
name: "badge.tsx",
|
|
214
|
-
content: prefixTailwindClasses(`import * as React from "react"
|
|
215
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
216
|
-
|
|
217
|
-
import { cn } from "../../lib/utils"
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Badge variants for status indicators.
|
|
221
|
-
* Pill-shaped badges with different colors for different states.
|
|
222
|
-
*/
|
|
223
|
-
const badgeVariants = cva(
|
|
224
|
-
"inline-flex items-center justify-center rounded-full text-sm font-medium transition-colors whitespace-nowrap",
|
|
225
|
-
{
|
|
226
|
-
variants: {
|
|
227
|
-
variant: {
|
|
228
|
-
active: "bg-[#E5FFF5] text-[#00A651]",
|
|
229
|
-
failed: "bg-[#FFECEC] text-[#FF3B3B]",
|
|
230
|
-
disabled: "bg-[#F3F5F6] text-[#6B7280]",
|
|
231
|
-
default: "bg-[#F3F5F6] text-[#333333]",
|
|
232
|
-
},
|
|
233
|
-
size: {
|
|
234
|
-
default: "px-3 py-1",
|
|
235
|
-
sm: "px-2 py-0.5 text-xs",
|
|
236
|
-
lg: "px-4 py-1.5",
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
defaultVariants: {
|
|
240
|
-
variant: "default",
|
|
241
|
-
size: "default",
|
|
242
|
-
},
|
|
243
|
-
}
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Badge component for displaying status indicators.
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* \`\`\`tsx
|
|
251
|
-
* <Badge variant="active">Active</Badge>
|
|
252
|
-
* <Badge variant="failed">Failed</Badge>
|
|
253
|
-
* <Badge variant="disabled">Disabled</Badge>
|
|
254
|
-
* <Badge variant="active" leftIcon={<CheckIcon />}>Active</Badge>
|
|
255
|
-
* \`\`\`
|
|
256
|
-
*/
|
|
257
|
-
export interface BadgeProps
|
|
258
|
-
extends React.HTMLAttributes<HTMLDivElement>,
|
|
259
|
-
VariantProps<typeof badgeVariants> {
|
|
260
|
-
/** Icon displayed on the left side of the badge text */
|
|
261
|
-
leftIcon?: React.ReactNode
|
|
262
|
-
/** Icon displayed on the right side of the badge text */
|
|
263
|
-
rightIcon?: React.ReactNode
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
|
267
|
-
({ className, variant, size, leftIcon, rightIcon, children, ...props }, ref) => {
|
|
268
|
-
return (
|
|
269
|
-
<div
|
|
270
|
-
className={cn(badgeVariants({ variant, size, className }), "gap-1")}
|
|
271
|
-
ref={ref}
|
|
272
|
-
{...props}
|
|
273
|
-
>
|
|
274
|
-
{leftIcon && <span className="[&_svg]:size-3">{leftIcon}</span>}
|
|
275
|
-
{children}
|
|
276
|
-
{rightIcon && <span className="[&_svg]:size-3">{rightIcon}</span>}
|
|
277
|
-
</div>
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
)
|
|
281
|
-
Badge.displayName = "Badge"
|
|
282
|
-
|
|
283
|
-
export { Badge, badgeVariants }
|
|
284
|
-
`, prefix)
|
|
285
|
-
}
|
|
286
|
-
]
|
|
287
|
-
},
|
|
288
203
|
"button": {
|
|
289
204
|
name: "button",
|
|
290
205
|
description: "A customizable button component with variants, sizes, and icons",
|
|
@@ -408,202 +323,163 @@ export { Button, buttonVariants }
|
|
|
408
323
|
}
|
|
409
324
|
]
|
|
410
325
|
},
|
|
411
|
-
"
|
|
412
|
-
name: "
|
|
413
|
-
description: "A
|
|
326
|
+
"badge": {
|
|
327
|
+
name: "badge",
|
|
328
|
+
description: "A status badge component with active, failed, and disabled variants",
|
|
414
329
|
dependencies: [
|
|
415
330
|
"class-variance-authority",
|
|
416
331
|
"clsx",
|
|
417
|
-
"tailwind-merge"
|
|
418
|
-
"lucide-react"
|
|
332
|
+
"tailwind-merge"
|
|
419
333
|
],
|
|
420
334
|
files: [
|
|
421
335
|
{
|
|
422
|
-
name: "
|
|
336
|
+
name: "badge.tsx",
|
|
423
337
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
424
338
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
425
|
-
import { Check, Minus } from "lucide-react"
|
|
426
339
|
|
|
427
340
|
import { cn } from "../../lib/utils"
|
|
428
341
|
|
|
429
342
|
/**
|
|
430
|
-
*
|
|
343
|
+
* Badge variants for status indicators.
|
|
344
|
+
* Pill-shaped badges with different colors for different states.
|
|
431
345
|
*/
|
|
432
|
-
const
|
|
433
|
-
"inline-flex items-center justify-center rounded
|
|
346
|
+
const badgeVariants = cva(
|
|
347
|
+
"inline-flex items-center justify-center rounded-full text-sm font-medium transition-colors whitespace-nowrap",
|
|
434
348
|
{
|
|
435
349
|
variants: {
|
|
350
|
+
variant: {
|
|
351
|
+
active: "bg-[#E5FFF5] text-[#00A651]",
|
|
352
|
+
failed: "bg-[#FFECEC] text-[#FF3B3B]",
|
|
353
|
+
disabled: "bg-[#F3F5F6] text-[#6B7280]",
|
|
354
|
+
default: "bg-[#F3F5F6] text-[#333333]",
|
|
355
|
+
},
|
|
436
356
|
size: {
|
|
437
|
-
default: "
|
|
438
|
-
sm: "
|
|
439
|
-
lg: "
|
|
357
|
+
default: "px-3 py-1",
|
|
358
|
+
sm: "px-2 py-0.5 text-xs",
|
|
359
|
+
lg: "px-4 py-1.5",
|
|
440
360
|
},
|
|
441
361
|
},
|
|
442
362
|
defaultVariants: {
|
|
363
|
+
variant: "default",
|
|
443
364
|
size: "default",
|
|
444
365
|
},
|
|
445
366
|
}
|
|
446
367
|
)
|
|
447
368
|
|
|
448
369
|
/**
|
|
449
|
-
*
|
|
370
|
+
* Badge component for displaying status indicators.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* \`\`\`tsx
|
|
374
|
+
* <Badge variant="active">Active</Badge>
|
|
375
|
+
* <Badge variant="failed">Failed</Badge>
|
|
376
|
+
* <Badge variant="disabled">Disabled</Badge>
|
|
377
|
+
* <Badge variant="active" leftIcon={<CheckIcon />}>Active</Badge>
|
|
378
|
+
* \`\`\`
|
|
450
379
|
*/
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
380
|
+
export interface BadgeProps
|
|
381
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
382
|
+
VariantProps<typeof badgeVariants> {
|
|
383
|
+
/** Icon displayed on the left side of the badge text */
|
|
384
|
+
leftIcon?: React.ReactNode
|
|
385
|
+
/** Icon displayed on the right side of the badge text */
|
|
386
|
+
rightIcon?: React.ReactNode
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
|
390
|
+
({ className, variant, size, leftIcon, rightIcon, children, ...props }, ref) => {
|
|
391
|
+
return (
|
|
392
|
+
<div
|
|
393
|
+
className={cn(badgeVariants({ variant, size, className }), "gap-1")}
|
|
394
|
+
ref={ref}
|
|
395
|
+
{...props}
|
|
396
|
+
>
|
|
397
|
+
{leftIcon && <span className="[&_svg]:size-3">{leftIcon}</span>}
|
|
398
|
+
{children}
|
|
399
|
+
{rightIcon && <span className="[&_svg]:size-3">{rightIcon}</span>}
|
|
400
|
+
</div>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
Badge.displayName = "Badge"
|
|
405
|
+
|
|
406
|
+
export { Badge, badgeVariants }
|
|
407
|
+
`, prefix)
|
|
408
|
+
}
|
|
409
|
+
]
|
|
457
410
|
},
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
411
|
+
"input": {
|
|
412
|
+
name: "input",
|
|
413
|
+
description: "A text input component with error and disabled states",
|
|
414
|
+
dependencies: [
|
|
415
|
+
"class-variance-authority",
|
|
416
|
+
"clsx",
|
|
417
|
+
"tailwind-merge"
|
|
418
|
+
],
|
|
419
|
+
files: [
|
|
420
|
+
{
|
|
421
|
+
name: "input.tsx",
|
|
422
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
423
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
424
|
+
|
|
425
|
+
import { cn } from "../../lib/utils"
|
|
463
426
|
|
|
464
427
|
/**
|
|
465
|
-
*
|
|
428
|
+
* Input variants for different visual states
|
|
466
429
|
*/
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
430
|
+
const inputVariants = cva(
|
|
431
|
+
"h-10 w-full rounded bg-white px-4 py-2.5 text-sm text-[#333333] transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-[#333333] placeholder:text-[#9CA3AF] disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[#F9FAFB]",
|
|
432
|
+
{
|
|
433
|
+
variants: {
|
|
434
|
+
state: {
|
|
435
|
+
default: "border border-[#E9E9E9] focus:outline-none focus:border-[#2BBBC9]/50 focus:shadow-[0_0_0_1px_rgba(43,187,201,0.15)]",
|
|
436
|
+
error: "border border-[#FF3B3B]/40 focus:outline-none focus:border-[#FF3B3B]/60 focus:shadow-[0_0_0_1px_rgba(255,59,59,0.1)]",
|
|
437
|
+
},
|
|
473
438
|
},
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
export type CheckedState = boolean | "indeterminate"
|
|
439
|
+
defaultVariants: {
|
|
440
|
+
state: "default",
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
)
|
|
481
444
|
|
|
482
445
|
/**
|
|
483
|
-
* A
|
|
446
|
+
* A flexible input component for text entry with state variants.
|
|
484
447
|
*
|
|
485
448
|
* @example
|
|
486
449
|
* \`\`\`tsx
|
|
487
|
-
* <
|
|
488
|
-
* <
|
|
489
|
-
* <
|
|
490
|
-
* <Checkbox label="Accept terms" labelPosition="right" />
|
|
450
|
+
* <Input placeholder="Enter your email" />
|
|
451
|
+
* <Input state="error" placeholder="Invalid input" />
|
|
452
|
+
* <Input state="success" placeholder="Valid input" />
|
|
491
453
|
* \`\`\`
|
|
492
454
|
*/
|
|
493
|
-
export interface
|
|
494
|
-
extends Omit<React.
|
|
495
|
-
VariantProps<typeof
|
|
496
|
-
/** Whether the checkbox is checked, unchecked, or indeterminate */
|
|
497
|
-
checked?: CheckedState
|
|
498
|
-
/** Default checked state for uncontrolled usage */
|
|
499
|
-
defaultChecked?: boolean
|
|
500
|
-
/** Callback when checked state changes */
|
|
501
|
-
onCheckedChange?: (checked: CheckedState) => void
|
|
502
|
-
/** Optional label text */
|
|
503
|
-
label?: string
|
|
504
|
-
/** Position of the label */
|
|
505
|
-
labelPosition?: "left" | "right"
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
509
|
-
(
|
|
510
|
-
{
|
|
511
|
-
className,
|
|
512
|
-
size,
|
|
513
|
-
checked: controlledChecked,
|
|
514
|
-
defaultChecked = false,
|
|
515
|
-
onCheckedChange,
|
|
516
|
-
disabled,
|
|
517
|
-
label,
|
|
518
|
-
labelPosition = "right",
|
|
519
|
-
onClick,
|
|
520
|
-
...props
|
|
521
|
-
},
|
|
522
|
-
ref
|
|
523
|
-
) => {
|
|
524
|
-
const [internalChecked, setInternalChecked] = React.useState<CheckedState>(defaultChecked)
|
|
455
|
+
export interface InputProps
|
|
456
|
+
extends Omit<React.ComponentProps<"input">, "size">,
|
|
457
|
+
VariantProps<typeof inputVariants> {}
|
|
525
458
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// Cycle through states: unchecked -> checked -> unchecked
|
|
533
|
-
// (indeterminate is typically set programmatically, not through user clicks)
|
|
534
|
-
const newValue = checkedState === true ? false : true
|
|
535
|
-
|
|
536
|
-
if (!isControlled) {
|
|
537
|
-
setInternalChecked(newValue)
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
onCheckedChange?.(newValue)
|
|
541
|
-
|
|
542
|
-
// Call external onClick if provided
|
|
543
|
-
onClick?.(e)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const isChecked = checkedState === true
|
|
547
|
-
const isIndeterminate = checkedState === "indeterminate"
|
|
548
|
-
|
|
549
|
-
const checkbox = (
|
|
550
|
-
<button
|
|
551
|
-
type="button"
|
|
552
|
-
role="checkbox"
|
|
553
|
-
aria-checked={isIndeterminate ? "mixed" : isChecked}
|
|
459
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
460
|
+
({ className, state, type, ...props }, ref) => {
|
|
461
|
+
return (
|
|
462
|
+
<input
|
|
463
|
+
type={type}
|
|
464
|
+
className={cn(inputVariants({ state, className }))}
|
|
554
465
|
ref={ref}
|
|
555
|
-
disabled={disabled}
|
|
556
|
-
onClick={handleClick}
|
|
557
|
-
className={cn(
|
|
558
|
-
checkboxVariants({ size, className }),
|
|
559
|
-
"cursor-pointer",
|
|
560
|
-
isChecked || isIndeterminate
|
|
561
|
-
? "bg-[#343E55] border-[#343E55] text-white"
|
|
562
|
-
: "bg-white border-[#E5E7EB] hover:border-[#9CA3AF]"
|
|
563
|
-
)}
|
|
564
466
|
{...props}
|
|
565
|
-
|
|
566
|
-
{isChecked && (
|
|
567
|
-
<Check className={cn(iconSizeVariants({ size }), "stroke-[3]")} />
|
|
568
|
-
)}
|
|
569
|
-
{isIndeterminate && (
|
|
570
|
-
<Minus className={cn(iconSizeVariants({ size }), "stroke-[3]")} />
|
|
571
|
-
)}
|
|
572
|
-
</button>
|
|
467
|
+
/>
|
|
573
468
|
)
|
|
574
|
-
|
|
575
|
-
if (label) {
|
|
576
|
-
return (
|
|
577
|
-
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|
578
|
-
{labelPosition === "left" && (
|
|
579
|
-
<span className={cn(labelSizeVariants({ size }), "text-[#333333]", disabled && "opacity-50")}>
|
|
580
|
-
{label}
|
|
581
|
-
</span>
|
|
582
|
-
)}
|
|
583
|
-
{checkbox}
|
|
584
|
-
{labelPosition === "right" && (
|
|
585
|
-
<span className={cn(labelSizeVariants({ size }), "text-[#333333]", disabled && "opacity-50")}>
|
|
586
|
-
{label}
|
|
587
|
-
</span>
|
|
588
|
-
)}
|
|
589
|
-
</label>
|
|
590
|
-
)
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
return checkbox
|
|
594
469
|
}
|
|
595
470
|
)
|
|
596
|
-
|
|
471
|
+
Input.displayName = "Input"
|
|
597
472
|
|
|
598
|
-
export {
|
|
473
|
+
export { Input, inputVariants }
|
|
599
474
|
`, prefix)
|
|
600
475
|
}
|
|
601
476
|
]
|
|
602
477
|
},
|
|
603
|
-
"
|
|
604
|
-
name: "
|
|
605
|
-
description: "
|
|
478
|
+
"select": {
|
|
479
|
+
name: "select",
|
|
480
|
+
description: "A select dropdown component built on Radix UI Select",
|
|
606
481
|
dependencies: [
|
|
482
|
+
"@radix-ui/react-select",
|
|
607
483
|
"class-variance-authority",
|
|
608
484
|
"clsx",
|
|
609
485
|
"tailwind-merge",
|
|
@@ -611,572 +487,683 @@ export { Checkbox, checkboxVariants }
|
|
|
611
487
|
],
|
|
612
488
|
files: [
|
|
613
489
|
{
|
|
614
|
-
name: "
|
|
490
|
+
name: "select.tsx",
|
|
615
491
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
492
|
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
616
493
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
617
|
-
import { ChevronDown } from "lucide-react"
|
|
494
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
618
495
|
|
|
619
496
|
import { cn } from "../../lib/utils"
|
|
620
497
|
|
|
621
498
|
/**
|
|
622
|
-
*
|
|
623
|
-
*/
|
|
624
|
-
const collapsibleVariants = cva("w-full", {
|
|
625
|
-
variants: {
|
|
626
|
-
variant: {
|
|
627
|
-
default: "",
|
|
628
|
-
bordered: "border border-[#E5E7EB] rounded-lg divide-y divide-[#E5E7EB]",
|
|
629
|
-
},
|
|
630
|
-
},
|
|
631
|
-
defaultVariants: {
|
|
632
|
-
variant: "default",
|
|
633
|
-
},
|
|
634
|
-
})
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Collapsible item variants
|
|
638
|
-
*/
|
|
639
|
-
const collapsibleItemVariants = cva("", {
|
|
640
|
-
variants: {
|
|
641
|
-
variant: {
|
|
642
|
-
default: "",
|
|
643
|
-
bordered: "",
|
|
644
|
-
},
|
|
645
|
-
},
|
|
646
|
-
defaultVariants: {
|
|
647
|
-
variant: "default",
|
|
648
|
-
},
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Collapsible trigger variants
|
|
653
|
-
*/
|
|
654
|
-
const collapsibleTriggerVariants = cva(
|
|
655
|
-
"flex w-full items-center justify-between text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#343E55] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
656
|
-
{
|
|
657
|
-
variants: {
|
|
658
|
-
variant: {
|
|
659
|
-
default: "py-3",
|
|
660
|
-
bordered: "p-4 hover:bg-[#F9FAFB]",
|
|
661
|
-
},
|
|
662
|
-
},
|
|
663
|
-
defaultVariants: {
|
|
664
|
-
variant: "default",
|
|
665
|
-
},
|
|
666
|
-
}
|
|
667
|
-
)
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Collapsible content variants
|
|
499
|
+
* SelectTrigger variants matching TextField styling
|
|
671
500
|
*/
|
|
672
|
-
const
|
|
673
|
-
"
|
|
501
|
+
const selectTriggerVariants = cva(
|
|
502
|
+
"flex h-10 w-full items-center justify-between rounded bg-white px-4 py-2.5 text-sm text-[#333333] transition-all disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[#F9FAFB] [&>span]:line-clamp-1",
|
|
674
503
|
{
|
|
675
504
|
variants: {
|
|
676
|
-
|
|
677
|
-
default: "",
|
|
678
|
-
|
|
505
|
+
state: {
|
|
506
|
+
default: "border border-[#E9E9E9] focus:outline-none focus:border-[#2BBBC9]/50 focus:shadow-[0_0_0_1px_rgba(43,187,201,0.15)]",
|
|
507
|
+
error: "border border-[#FF3B3B]/40 focus:outline-none focus:border-[#FF3B3B]/60 focus:shadow-[0_0_0_1px_rgba(255,59,59,0.1)]",
|
|
679
508
|
},
|
|
680
509
|
},
|
|
681
510
|
defaultVariants: {
|
|
682
|
-
|
|
511
|
+
state: "default",
|
|
683
512
|
},
|
|
684
513
|
}
|
|
685
514
|
)
|
|
686
515
|
|
|
687
|
-
|
|
688
|
-
type CollapsibleType = "single" | "multiple"
|
|
516
|
+
const Select = SelectPrimitive.Root
|
|
689
517
|
|
|
690
|
-
|
|
691
|
-
type: CollapsibleType
|
|
692
|
-
value: string[]
|
|
693
|
-
onValueChange: (value: string[]) => void
|
|
694
|
-
variant: "default" | "bordered"
|
|
695
|
-
}
|
|
518
|
+
const SelectGroup = SelectPrimitive.Group
|
|
696
519
|
|
|
697
|
-
|
|
698
|
-
value: string
|
|
699
|
-
isOpen: boolean
|
|
700
|
-
disabled?: boolean
|
|
701
|
-
}
|
|
520
|
+
const SelectValue = SelectPrimitive.Value
|
|
702
521
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
522
|
+
export interface SelectTriggerProps
|
|
523
|
+
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>,
|
|
524
|
+
VariantProps<typeof selectTriggerVariants> {}
|
|
706
525
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
526
|
+
const SelectTrigger = React.forwardRef<
|
|
527
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
528
|
+
SelectTriggerProps
|
|
529
|
+
>(({ className, state, children, ...props }, ref) => (
|
|
530
|
+
<SelectPrimitive.Trigger
|
|
531
|
+
ref={ref}
|
|
532
|
+
className={cn(selectTriggerVariants({ state, className }))}
|
|
533
|
+
{...props}
|
|
534
|
+
>
|
|
535
|
+
{children}
|
|
536
|
+
<SelectPrimitive.Icon asChild>
|
|
537
|
+
<ChevronDown className="size-4 text-[#6B7280] opacity-70" />
|
|
538
|
+
</SelectPrimitive.Icon>
|
|
539
|
+
</SelectPrimitive.Trigger>
|
|
540
|
+
))
|
|
541
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
714
542
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
543
|
+
const SelectScrollUpButton = React.forwardRef<
|
|
544
|
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
545
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
546
|
+
>(({ className, ...props }, ref) => (
|
|
547
|
+
<SelectPrimitive.ScrollUpButton
|
|
548
|
+
ref={ref}
|
|
549
|
+
className={cn(
|
|
550
|
+
"flex cursor-default items-center justify-center py-1",
|
|
551
|
+
className
|
|
552
|
+
)}
|
|
553
|
+
{...props}
|
|
554
|
+
>
|
|
555
|
+
<ChevronUp className="size-4 text-[#6B7280]" />
|
|
556
|
+
</SelectPrimitive.ScrollUpButton>
|
|
557
|
+
))
|
|
558
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
722
559
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
560
|
+
const SelectScrollDownButton = React.forwardRef<
|
|
561
|
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
562
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
563
|
+
>(({ className, ...props }, ref) => (
|
|
564
|
+
<SelectPrimitive.ScrollDownButton
|
|
565
|
+
ref={ref}
|
|
566
|
+
className={cn(
|
|
567
|
+
"flex cursor-default items-center justify-center py-1",
|
|
568
|
+
className
|
|
569
|
+
)}
|
|
570
|
+
{...props}
|
|
571
|
+
>
|
|
572
|
+
<ChevronDown className="size-4 text-[#6B7280]" />
|
|
573
|
+
</SelectPrimitive.ScrollDownButton>
|
|
574
|
+
))
|
|
575
|
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
|
738
576
|
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
577
|
+
const SelectContent = React.forwardRef<
|
|
578
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
579
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
580
|
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
581
|
+
<SelectPrimitive.Portal>
|
|
582
|
+
<SelectPrimitive.Content
|
|
583
|
+
ref={ref}
|
|
584
|
+
className={cn(
|
|
585
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded bg-white border border-[#E9E9E9] shadow-md",
|
|
586
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
587
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
588
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
589
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
590
|
+
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
591
|
+
position === "popper" &&
|
|
592
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
593
|
+
className
|
|
594
|
+
)}
|
|
595
|
+
position={position}
|
|
596
|
+
{...props}
|
|
597
|
+
>
|
|
598
|
+
<SelectScrollUpButton />
|
|
599
|
+
<SelectPrimitive.Viewport
|
|
600
|
+
className={cn(
|
|
601
|
+
"p-1",
|
|
602
|
+
position === "popper" &&
|
|
603
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
604
|
+
)}
|
|
605
|
+
>
|
|
606
|
+
{children}
|
|
607
|
+
</SelectPrimitive.Viewport>
|
|
608
|
+
<SelectScrollDownButton />
|
|
609
|
+
</SelectPrimitive.Content>
|
|
610
|
+
</SelectPrimitive.Portal>
|
|
611
|
+
))
|
|
612
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
754
613
|
|
|
755
|
-
|
|
756
|
-
|
|
614
|
+
const SelectLabel = React.forwardRef<
|
|
615
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
616
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
617
|
+
>(({ className, ...props }, ref) => (
|
|
618
|
+
<SelectPrimitive.Label
|
|
619
|
+
ref={ref}
|
|
620
|
+
className={cn("px-4 py-1.5 text-xs font-medium text-[#6B7280]", className)}
|
|
621
|
+
{...props}
|
|
622
|
+
/>
|
|
623
|
+
))
|
|
624
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
757
625
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
626
|
+
const SelectItem = React.forwardRef<
|
|
627
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
628
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
629
|
+
>(({ className, children, ...props }, ref) => (
|
|
630
|
+
<SelectPrimitive.Item
|
|
631
|
+
ref={ref}
|
|
632
|
+
className={cn(
|
|
633
|
+
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-2 pl-4 pr-8 text-sm text-[#333333] outline-none",
|
|
634
|
+
"hover:bg-[#F3F4F6] focus:bg-[#F3F4F6]",
|
|
635
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
636
|
+
className
|
|
637
|
+
)}
|
|
638
|
+
{...props}
|
|
639
|
+
>
|
|
640
|
+
<span className="absolute right-2 flex size-4 items-center justify-center">
|
|
641
|
+
<SelectPrimitive.ItemIndicator>
|
|
642
|
+
<Check className="size-4 text-[#2BBBC9]" />
|
|
643
|
+
</SelectPrimitive.ItemIndicator>
|
|
644
|
+
</span>
|
|
645
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
646
|
+
</SelectPrimitive.Item>
|
|
647
|
+
))
|
|
648
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
649
|
+
|
|
650
|
+
const SelectSeparator = React.forwardRef<
|
|
651
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
652
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
653
|
+
>(({ className, ...props }, ref) => (
|
|
654
|
+
<SelectPrimitive.Separator
|
|
655
|
+
ref={ref}
|
|
656
|
+
className={cn("-mx-1 my-1 h-px bg-[#E9E9E9]", className)}
|
|
657
|
+
{...props}
|
|
658
|
+
/>
|
|
659
|
+
))
|
|
660
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
661
|
+
|
|
662
|
+
export {
|
|
663
|
+
Select,
|
|
664
|
+
SelectGroup,
|
|
665
|
+
SelectValue,
|
|
666
|
+
SelectTrigger,
|
|
667
|
+
SelectContent,
|
|
668
|
+
SelectLabel,
|
|
669
|
+
SelectItem,
|
|
670
|
+
SelectSeparator,
|
|
671
|
+
SelectScrollUpButton,
|
|
672
|
+
SelectScrollDownButton,
|
|
673
|
+
selectTriggerVariants,
|
|
674
|
+
}
|
|
675
|
+
`, prefix)
|
|
762
676
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
"checkbox": {
|
|
680
|
+
name: "checkbox",
|
|
681
|
+
description: "A tri-state checkbox component with label support (checked, unchecked, indeterminate)",
|
|
682
|
+
dependencies: [
|
|
683
|
+
"class-variance-authority",
|
|
684
|
+
"clsx",
|
|
685
|
+
"tailwind-merge",
|
|
686
|
+
"lucide-react"
|
|
687
|
+
],
|
|
688
|
+
files: [
|
|
689
|
+
{
|
|
690
|
+
name: "checkbox.tsx",
|
|
691
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
692
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
693
|
+
import { Check, Minus } from "lucide-react"
|
|
767
694
|
|
|
768
|
-
|
|
769
|
-
() => ({
|
|
770
|
-
type,
|
|
771
|
-
value: currentValue,
|
|
772
|
-
onValueChange: handleValueChange,
|
|
773
|
-
variant: variant || "default",
|
|
774
|
-
}),
|
|
775
|
-
[type, currentValue, handleValueChange, variant]
|
|
776
|
-
)
|
|
695
|
+
import { cn } from "../../lib/utils"
|
|
777
696
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Checkbox box variants (the outer container)
|
|
699
|
+
*/
|
|
700
|
+
const checkboxVariants = cva(
|
|
701
|
+
"inline-flex items-center justify-center rounded border-2 transition-colors duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#343E55] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
702
|
+
{
|
|
703
|
+
variants: {
|
|
704
|
+
size: {
|
|
705
|
+
default: "h-5 w-5",
|
|
706
|
+
sm: "h-4 w-4",
|
|
707
|
+
lg: "h-6 w-6",
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
defaultVariants: {
|
|
711
|
+
size: "default",
|
|
712
|
+
},
|
|
789
713
|
}
|
|
790
714
|
)
|
|
791
|
-
Collapsible.displayName = "Collapsible"
|
|
792
715
|
|
|
793
716
|
/**
|
|
794
|
-
*
|
|
717
|
+
* Icon size variants based on checkbox size
|
|
795
718
|
*/
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const isOpen = openValues.includes(value)
|
|
719
|
+
const iconSizeVariants = cva("", {
|
|
720
|
+
variants: {
|
|
721
|
+
size: {
|
|
722
|
+
default: "h-3.5 w-3.5",
|
|
723
|
+
sm: "h-3 w-3",
|
|
724
|
+
lg: "h-4 w-4",
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
defaultVariants: {
|
|
728
|
+
size: "default",
|
|
729
|
+
},
|
|
730
|
+
})
|
|
809
731
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
732
|
+
/**
|
|
733
|
+
* Label text size variants
|
|
734
|
+
*/
|
|
735
|
+
const labelSizeVariants = cva("", {
|
|
736
|
+
variants: {
|
|
737
|
+
size: {
|
|
738
|
+
default: "text-sm",
|
|
739
|
+
sm: "text-xs",
|
|
740
|
+
lg: "text-base",
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
defaultVariants: {
|
|
744
|
+
size: "default",
|
|
745
|
+
},
|
|
746
|
+
})
|
|
818
747
|
|
|
819
|
-
|
|
820
|
-
<CollapsibleItemContext.Provider value={contextValue}>
|
|
821
|
-
<div
|
|
822
|
-
ref={ref}
|
|
823
|
-
data-state={isOpen ? "open" : "closed"}
|
|
824
|
-
className={cn(collapsibleItemVariants({ variant, className }))}
|
|
825
|
-
{...props}
|
|
826
|
-
>
|
|
827
|
-
{children}
|
|
828
|
-
</div>
|
|
829
|
-
</CollapsibleItemContext.Provider>
|
|
830
|
-
)
|
|
831
|
-
}
|
|
832
|
-
)
|
|
833
|
-
CollapsibleItem.displayName = "CollapsibleItem"
|
|
748
|
+
export type CheckedState = boolean | "indeterminate"
|
|
834
749
|
|
|
835
750
|
/**
|
|
836
|
-
*
|
|
751
|
+
* A tri-state checkbox component with label support
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* \`\`\`tsx
|
|
755
|
+
* <Checkbox checked={isEnabled} onCheckedChange={setIsEnabled} />
|
|
756
|
+
* <Checkbox size="sm" disabled />
|
|
757
|
+
* <Checkbox checked="indeterminate" label="Select all" />
|
|
758
|
+
* <Checkbox label="Accept terms" labelPosition="right" />
|
|
759
|
+
* <Checkbox id="terms" label="Accept terms" separateLabel />
|
|
760
|
+
* \`\`\`
|
|
837
761
|
*/
|
|
838
|
-
export interface
|
|
839
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
840
|
-
VariantProps<typeof
|
|
841
|
-
/** Whether
|
|
842
|
-
|
|
762
|
+
export interface CheckboxProps
|
|
763
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange">,
|
|
764
|
+
VariantProps<typeof checkboxVariants> {
|
|
765
|
+
/** Whether the checkbox is checked, unchecked, or indeterminate */
|
|
766
|
+
checked?: CheckedState
|
|
767
|
+
/** Default checked state for uncontrolled usage */
|
|
768
|
+
defaultChecked?: boolean
|
|
769
|
+
/** Callback when checked state changes */
|
|
770
|
+
onCheckedChange?: (checked: CheckedState) => void
|
|
771
|
+
/** Optional label text */
|
|
772
|
+
label?: string
|
|
773
|
+
/** Position of the label */
|
|
774
|
+
labelPosition?: "left" | "right"
|
|
775
|
+
/** The label of the checkbox for accessibility */
|
|
776
|
+
ariaLabel?: string
|
|
777
|
+
/** The ID of an element describing the checkbox */
|
|
778
|
+
ariaLabelledBy?: string
|
|
779
|
+
/** If true, the checkbox automatically receives focus */
|
|
780
|
+
autoFocus?: boolean
|
|
781
|
+
/** Class name applied to the checkbox element */
|
|
782
|
+
checkboxClassName?: string
|
|
783
|
+
/** Class name applied to the label element */
|
|
784
|
+
labelClassName?: string
|
|
785
|
+
/** The name of the checkbox, used for form submission */
|
|
786
|
+
name?: string
|
|
787
|
+
/** The value submitted with the form when checked */
|
|
788
|
+
value?: string
|
|
789
|
+
/** If true, uses separate labels with htmlFor/id association instead of wrapping the input. Requires id prop. */
|
|
790
|
+
separateLabel?: boolean
|
|
843
791
|
}
|
|
844
792
|
|
|
845
|
-
const
|
|
846
|
-
(
|
|
847
|
-
|
|
848
|
-
|
|
793
|
+
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
794
|
+
(
|
|
795
|
+
{
|
|
796
|
+
className,
|
|
797
|
+
size,
|
|
798
|
+
checked: controlledChecked,
|
|
799
|
+
defaultChecked = false,
|
|
800
|
+
onCheckedChange,
|
|
801
|
+
disabled,
|
|
802
|
+
label,
|
|
803
|
+
labelPosition = "right",
|
|
804
|
+
ariaLabel,
|
|
805
|
+
ariaLabelledBy,
|
|
806
|
+
autoFocus,
|
|
807
|
+
checkboxClassName,
|
|
808
|
+
labelClassName,
|
|
809
|
+
name,
|
|
810
|
+
value,
|
|
811
|
+
separateLabel = false,
|
|
812
|
+
id,
|
|
813
|
+
onClick,
|
|
814
|
+
...props
|
|
815
|
+
},
|
|
816
|
+
ref
|
|
817
|
+
) => {
|
|
818
|
+
const [internalChecked, setInternalChecked] = React.useState<CheckedState>(defaultChecked)
|
|
819
|
+
const checkboxRef = React.useRef<HTMLButtonElement>(null)
|
|
849
820
|
|
|
850
|
-
|
|
821
|
+
// Merge refs
|
|
822
|
+
React.useImperativeHandle(ref, () => checkboxRef.current!)
|
|
823
|
+
|
|
824
|
+
// Handle autoFocus
|
|
825
|
+
React.useEffect(() => {
|
|
826
|
+
if (autoFocus && checkboxRef.current) {
|
|
827
|
+
checkboxRef.current.focus()
|
|
828
|
+
}
|
|
829
|
+
}, [autoFocus])
|
|
830
|
+
|
|
831
|
+
const isControlled = controlledChecked !== undefined
|
|
832
|
+
const checkedState = isControlled ? controlledChecked : internalChecked
|
|
833
|
+
|
|
834
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
851
835
|
if (disabled) return
|
|
852
836
|
|
|
853
|
-
|
|
837
|
+
// Cycle through states: unchecked -> checked -> unchecked
|
|
838
|
+
// (indeterminate is typically set programmatically, not through user clicks)
|
|
839
|
+
const newValue = checkedState === true ? false : true
|
|
854
840
|
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
newValue = isOpen ? [] : [value]
|
|
858
|
-
} else {
|
|
859
|
-
// In multiple mode, toggle the item in the array
|
|
860
|
-
newValue = isOpen
|
|
861
|
-
? openValues.filter((v) => v !== value)
|
|
862
|
-
: [...openValues, value]
|
|
841
|
+
if (!isControlled) {
|
|
842
|
+
setInternalChecked(newValue)
|
|
863
843
|
}
|
|
864
844
|
|
|
865
|
-
|
|
845
|
+
onCheckedChange?.(newValue)
|
|
846
|
+
|
|
847
|
+
// Call external onClick if provided
|
|
848
|
+
onClick?.(e)
|
|
866
849
|
}
|
|
867
850
|
|
|
868
|
-
|
|
851
|
+
const isChecked = checkedState === true
|
|
852
|
+
const isIndeterminate = checkedState === "indeterminate"
|
|
853
|
+
|
|
854
|
+
const checkbox = (
|
|
869
855
|
<button
|
|
870
|
-
ref={ref}
|
|
871
856
|
type="button"
|
|
872
|
-
|
|
857
|
+
role="checkbox"
|
|
858
|
+
aria-checked={isIndeterminate ? "mixed" : isChecked}
|
|
859
|
+
aria-label={ariaLabel}
|
|
860
|
+
aria-labelledby={ariaLabelledBy}
|
|
861
|
+
ref={checkboxRef}
|
|
862
|
+
id={id}
|
|
873
863
|
disabled={disabled}
|
|
874
864
|
onClick={handleClick}
|
|
875
|
-
|
|
865
|
+
data-name={name}
|
|
866
|
+
data-value={value}
|
|
867
|
+
className={cn(
|
|
868
|
+
checkboxVariants({ size }),
|
|
869
|
+
"cursor-pointer",
|
|
870
|
+
isChecked || isIndeterminate
|
|
871
|
+
? "bg-[#343E55] border-[#343E55] text-white"
|
|
872
|
+
: "bg-white border-[#E5E7EB] hover:border-[#9CA3AF]",
|
|
873
|
+
className,
|
|
874
|
+
checkboxClassName
|
|
875
|
+
)}
|
|
876
876
|
{...props}
|
|
877
877
|
>
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
isOpen && "rotate-180"
|
|
884
|
-
)}
|
|
885
|
-
/>
|
|
878
|
+
{isChecked && (
|
|
879
|
+
<Check className={cn(iconSizeVariants({ size }), "stroke-[3]")} />
|
|
880
|
+
)}
|
|
881
|
+
{isIndeterminate && (
|
|
882
|
+
<Minus className={cn(iconSizeVariants({ size }), "stroke-[3]")} />
|
|
886
883
|
)}
|
|
887
884
|
</button>
|
|
888
885
|
)
|
|
889
|
-
}
|
|
890
|
-
)
|
|
891
|
-
CollapsibleTrigger.displayName = "CollapsibleTrigger"
|
|
892
886
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
887
|
+
if (label) {
|
|
888
|
+
// separateLabel mode: use htmlFor/id association instead of wrapping
|
|
889
|
+
if (separateLabel && id) {
|
|
890
|
+
return (
|
|
891
|
+
<div className="inline-flex items-center gap-2">
|
|
892
|
+
{labelPosition === "left" && (
|
|
893
|
+
<label
|
|
894
|
+
htmlFor={id}
|
|
895
|
+
className={cn(
|
|
896
|
+
labelSizeVariants({ size }),
|
|
897
|
+
"text-[#333333] cursor-pointer",
|
|
898
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
899
|
+
labelClassName
|
|
900
|
+
)}
|
|
901
|
+
>
|
|
902
|
+
{label}
|
|
903
|
+
</label>
|
|
904
|
+
)}
|
|
905
|
+
{checkbox}
|
|
906
|
+
{labelPosition === "right" && (
|
|
907
|
+
<label
|
|
908
|
+
htmlFor={id}
|
|
909
|
+
className={cn(
|
|
910
|
+
labelSizeVariants({ size }),
|
|
911
|
+
"text-[#333333] cursor-pointer",
|
|
912
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
913
|
+
labelClassName
|
|
914
|
+
)}
|
|
915
|
+
>
|
|
916
|
+
{label}
|
|
917
|
+
</label>
|
|
918
|
+
)}
|
|
919
|
+
</div>
|
|
920
|
+
)
|
|
911
921
|
}
|
|
912
|
-
}, [isOpen, children])
|
|
913
922
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
{
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
923
|
+
// Default: wrapping label
|
|
924
|
+
return (
|
|
925
|
+
<label className={cn("inline-flex items-center gap-2 cursor-pointer", disabled && "cursor-not-allowed")}>
|
|
926
|
+
{labelPosition === "left" && (
|
|
927
|
+
<span className={cn(labelSizeVariants({ size }), "text-[#333333]", disabled && "opacity-50", labelClassName)}>
|
|
928
|
+
{label}
|
|
929
|
+
</span>
|
|
930
|
+
)}
|
|
931
|
+
{checkbox}
|
|
932
|
+
{labelPosition === "right" && (
|
|
933
|
+
<span className={cn(labelSizeVariants({ size }), "text-[#333333]", disabled && "opacity-50", labelClassName)}>
|
|
934
|
+
{label}
|
|
935
|
+
</span>
|
|
936
|
+
)}
|
|
937
|
+
</label>
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return checkbox
|
|
927
942
|
}
|
|
928
943
|
)
|
|
929
|
-
|
|
944
|
+
Checkbox.displayName = "Checkbox"
|
|
930
945
|
|
|
931
|
-
export {
|
|
932
|
-
Collapsible,
|
|
933
|
-
CollapsibleItem,
|
|
934
|
-
CollapsibleTrigger,
|
|
935
|
-
CollapsibleContent,
|
|
936
|
-
collapsibleVariants,
|
|
937
|
-
collapsibleItemVariants,
|
|
938
|
-
collapsibleTriggerVariants,
|
|
939
|
-
collapsibleContentVariants,
|
|
940
|
-
}
|
|
946
|
+
export { Checkbox, checkboxVariants }
|
|
941
947
|
`, prefix)
|
|
942
948
|
}
|
|
943
949
|
]
|
|
944
950
|
},
|
|
945
|
-
"
|
|
946
|
-
name: "
|
|
947
|
-
description: "A
|
|
951
|
+
"toggle": {
|
|
952
|
+
name: "toggle",
|
|
953
|
+
description: "A toggle/switch component for boolean inputs with on/off states",
|
|
948
954
|
dependencies: [
|
|
949
|
-
"
|
|
955
|
+
"class-variance-authority",
|
|
950
956
|
"clsx",
|
|
951
|
-
"tailwind-merge"
|
|
952
|
-
"lucide-react"
|
|
957
|
+
"tailwind-merge"
|
|
953
958
|
],
|
|
954
959
|
files: [
|
|
955
960
|
{
|
|
956
|
-
name: "
|
|
961
|
+
name: "toggle.tsx",
|
|
957
962
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
958
|
-
import
|
|
959
|
-
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
963
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
960
964
|
|
|
961
965
|
import { cn } from "../../lib/utils"
|
|
962
966
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
967
|
+
/**
|
|
968
|
+
* Toggle track variants (the outer container)
|
|
969
|
+
*/
|
|
970
|
+
const toggleVariants = cva(
|
|
971
|
+
"relative inline-flex shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#343E55] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
972
|
+
{
|
|
973
|
+
variants: {
|
|
974
|
+
size: {
|
|
975
|
+
default: "h-6 w-11",
|
|
976
|
+
sm: "h-5 w-9",
|
|
977
|
+
lg: "h-7 w-14",
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
defaultVariants: {
|
|
981
|
+
size: "default",
|
|
982
|
+
},
|
|
983
|
+
}
|
|
984
|
+
)
|
|
966
985
|
|
|
967
|
-
|
|
986
|
+
/**
|
|
987
|
+
* Toggle thumb variants (the sliding circle)
|
|
988
|
+
*/
|
|
989
|
+
const toggleThumbVariants = cva(
|
|
990
|
+
"pointer-events-none inline-block rounded-full bg-white shadow-lg ring-0 transition-transform duration-200 ease-in-out",
|
|
991
|
+
{
|
|
992
|
+
variants: {
|
|
993
|
+
size: {
|
|
994
|
+
default: "h-5 w-5",
|
|
995
|
+
sm: "h-4 w-4",
|
|
996
|
+
lg: "h-6 w-6",
|
|
997
|
+
},
|
|
998
|
+
checked: {
|
|
999
|
+
true: "",
|
|
1000
|
+
false: "translate-x-0",
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
compoundVariants: [
|
|
1004
|
+
{ size: "default", checked: true, className: "translate-x-5" },
|
|
1005
|
+
{ size: "sm", checked: true, className: "translate-x-4" },
|
|
1006
|
+
{ size: "lg", checked: true, className: "translate-x-7" },
|
|
1007
|
+
],
|
|
1008
|
+
defaultVariants: {
|
|
1009
|
+
size: "default",
|
|
1010
|
+
checked: false,
|
|
1011
|
+
},
|
|
1012
|
+
}
|
|
1013
|
+
)
|
|
968
1014
|
|
|
969
|
-
|
|
1015
|
+
/**
|
|
1016
|
+
* A toggle/switch component for boolean inputs with on/off states
|
|
1017
|
+
*
|
|
1018
|
+
* @example
|
|
1019
|
+
* \`\`\`tsx
|
|
1020
|
+
* <Toggle checked={isEnabled} onCheckedChange={setIsEnabled} />
|
|
1021
|
+
* <Toggle size="sm" disabled />
|
|
1022
|
+
* <Toggle size="lg" checked label="Enable notifications" />
|
|
1023
|
+
* \`\`\`
|
|
1024
|
+
*/
|
|
1025
|
+
export interface ToggleProps
|
|
1026
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange">,
|
|
1027
|
+
VariantProps<typeof toggleVariants> {
|
|
1028
|
+
/** Whether the toggle is checked/on */
|
|
1029
|
+
checked?: boolean
|
|
1030
|
+
/** Default checked state for uncontrolled usage */
|
|
1031
|
+
defaultChecked?: boolean
|
|
1032
|
+
/** Callback when checked state changes */
|
|
1033
|
+
onCheckedChange?: (checked: boolean) => void
|
|
1034
|
+
/** Optional label text */
|
|
1035
|
+
label?: string
|
|
1036
|
+
/** Position of the label */
|
|
1037
|
+
labelPosition?: "left" | "right"
|
|
1038
|
+
}
|
|
970
1039
|
|
|
971
|
-
const
|
|
1040
|
+
const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
1041
|
+
(
|
|
1042
|
+
{
|
|
1043
|
+
className,
|
|
1044
|
+
size,
|
|
1045
|
+
checked: controlledChecked,
|
|
1046
|
+
defaultChecked = false,
|
|
1047
|
+
onCheckedChange,
|
|
1048
|
+
disabled,
|
|
1049
|
+
label,
|
|
1050
|
+
labelPosition = "right",
|
|
1051
|
+
...props
|
|
1052
|
+
},
|
|
1053
|
+
ref
|
|
1054
|
+
) => {
|
|
1055
|
+
const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
|
|
972
1056
|
|
|
973
|
-
const
|
|
1057
|
+
const isControlled = controlledChecked !== undefined
|
|
1058
|
+
const isChecked = isControlled ? controlledChecked : internalChecked
|
|
974
1059
|
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
978
|
-
inset?: boolean
|
|
979
|
-
}
|
|
980
|
-
>(({ className, inset, children, ...props }, ref) => (
|
|
981
|
-
<DropdownMenuPrimitive.SubTrigger
|
|
982
|
-
ref={ref}
|
|
983
|
-
className={cn(
|
|
984
|
-
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[#F3F4F6] data-[state=open]:bg-[#F3F4F6]",
|
|
985
|
-
inset && "pl-8",
|
|
986
|
-
className
|
|
987
|
-
)}
|
|
988
|
-
{...props}
|
|
989
|
-
>
|
|
990
|
-
{children}
|
|
991
|
-
<ChevronRight className="ml-auto h-4 w-4" />
|
|
992
|
-
</DropdownMenuPrimitive.SubTrigger>
|
|
993
|
-
))
|
|
994
|
-
DropdownMenuSubTrigger.displayName =
|
|
995
|
-
DropdownMenuPrimitive.SubTrigger.displayName
|
|
1060
|
+
const handleClick = () => {
|
|
1061
|
+
if (disabled) return
|
|
996
1062
|
|
|
997
|
-
const
|
|
998
|
-
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
999
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
1000
|
-
>(({ className, ...props }, ref) => (
|
|
1001
|
-
<DropdownMenuPrimitive.SubContent
|
|
1002
|
-
ref={ref}
|
|
1003
|
-
className={cn(
|
|
1004
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-[#E5E7EB] bg-white p-1 text-[#333333] shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
1005
|
-
className
|
|
1006
|
-
)}
|
|
1007
|
-
{...props}
|
|
1008
|
-
/>
|
|
1009
|
-
))
|
|
1010
|
-
DropdownMenuSubContent.displayName =
|
|
1011
|
-
DropdownMenuPrimitive.SubContent.displayName
|
|
1063
|
+
const newValue = !isChecked
|
|
1012
1064
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
1017
|
-
<DropdownMenuPrimitive.Portal>
|
|
1018
|
-
<DropdownMenuPrimitive.Content
|
|
1019
|
-
ref={ref}
|
|
1020
|
-
sideOffset={sideOffset}
|
|
1021
|
-
className={cn(
|
|
1022
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-[#E5E7EB] bg-white p-1 text-[#333333] shadow-md",
|
|
1023
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
1024
|
-
className
|
|
1025
|
-
)}
|
|
1026
|
-
{...props}
|
|
1027
|
-
/>
|
|
1028
|
-
</DropdownMenuPrimitive.Portal>
|
|
1029
|
-
))
|
|
1030
|
-
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
1065
|
+
if (!isControlled) {
|
|
1066
|
+
setInternalChecked(newValue)
|
|
1067
|
+
}
|
|
1031
1068
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
1035
|
-
inset?: boolean
|
|
1036
|
-
}
|
|
1037
|
-
>(({ className, inset, ...props }, ref) => (
|
|
1038
|
-
<DropdownMenuPrimitive.Item
|
|
1039
|
-
ref={ref}
|
|
1040
|
-
className={cn(
|
|
1041
|
-
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[#F3F4F6] focus:text-[#333333] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1042
|
-
inset && "pl-8",
|
|
1043
|
-
className
|
|
1044
|
-
)}
|
|
1045
|
-
{...props}
|
|
1046
|
-
/>
|
|
1047
|
-
))
|
|
1048
|
-
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
1069
|
+
onCheckedChange?.(newValue)
|
|
1070
|
+
}
|
|
1049
1071
|
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
)
|
|
1071
|
-
DropdownMenuCheckboxItem.displayName =
|
|
1072
|
-
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
1072
|
+
const toggle = (
|
|
1073
|
+
<button
|
|
1074
|
+
type="button"
|
|
1075
|
+
role="switch"
|
|
1076
|
+
aria-checked={isChecked}
|
|
1077
|
+
ref={ref}
|
|
1078
|
+
disabled={disabled}
|
|
1079
|
+
onClick={handleClick}
|
|
1080
|
+
className={cn(
|
|
1081
|
+
toggleVariants({ size, className }),
|
|
1082
|
+
isChecked ? "bg-[#343E55]" : "bg-[#E5E7EB]"
|
|
1083
|
+
)}
|
|
1084
|
+
{...props}
|
|
1085
|
+
>
|
|
1086
|
+
<span
|
|
1087
|
+
className={cn(
|
|
1088
|
+
toggleThumbVariants({ size, checked: isChecked })
|
|
1089
|
+
)}
|
|
1090
|
+
/>
|
|
1091
|
+
</button>
|
|
1092
|
+
)
|
|
1073
1093
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
{children}
|
|
1092
|
-
</DropdownMenuPrimitive.RadioItem>
|
|
1093
|
-
))
|
|
1094
|
-
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
1094
|
+
if (label) {
|
|
1095
|
+
return (
|
|
1096
|
+
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|
1097
|
+
{labelPosition === "left" && (
|
|
1098
|
+
<span className={cn("text-sm text-[#333333]", disabled && "opacity-50")}>
|
|
1099
|
+
{label}
|
|
1100
|
+
</span>
|
|
1101
|
+
)}
|
|
1102
|
+
{toggle}
|
|
1103
|
+
{labelPosition === "right" && (
|
|
1104
|
+
<span className={cn("text-sm text-[#333333]", disabled && "opacity-50")}>
|
|
1105
|
+
{label}
|
|
1106
|
+
</span>
|
|
1107
|
+
)}
|
|
1108
|
+
</label>
|
|
1109
|
+
)
|
|
1110
|
+
}
|
|
1095
1111
|
|
|
1096
|
-
|
|
1097
|
-
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
1098
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
1099
|
-
inset?: boolean
|
|
1112
|
+
return toggle
|
|
1100
1113
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
ref={ref}
|
|
1104
|
-
className={cn(
|
|
1105
|
-
"px-2 py-1.5 text-sm font-semibold",
|
|
1106
|
-
inset && "pl-8",
|
|
1107
|
-
className
|
|
1108
|
-
)}
|
|
1109
|
-
{...props}
|
|
1110
|
-
/>
|
|
1111
|
-
))
|
|
1112
|
-
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
1113
|
-
|
|
1114
|
-
const DropdownMenuSeparator = React.forwardRef<
|
|
1115
|
-
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
1116
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
1117
|
-
>(({ className, ...props }, ref) => (
|
|
1118
|
-
<DropdownMenuPrimitive.Separator
|
|
1119
|
-
ref={ref}
|
|
1120
|
-
className={cn("-mx-1 my-1 h-px bg-[#E5E7EB]", className)}
|
|
1121
|
-
{...props}
|
|
1122
|
-
/>
|
|
1123
|
-
))
|
|
1124
|
-
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
1125
|
-
|
|
1126
|
-
const DropdownMenuShortcut = ({
|
|
1127
|
-
className,
|
|
1128
|
-
...props
|
|
1129
|
-
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
1130
|
-
return (
|
|
1131
|
-
<span
|
|
1132
|
-
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
1133
|
-
{...props}
|
|
1134
|
-
/>
|
|
1135
|
-
)
|
|
1136
|
-
}
|
|
1137
|
-
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
1114
|
+
)
|
|
1115
|
+
Toggle.displayName = "Toggle"
|
|
1138
1116
|
|
|
1139
|
-
export {
|
|
1140
|
-
DropdownMenu,
|
|
1141
|
-
DropdownMenuTrigger,
|
|
1142
|
-
DropdownMenuContent,
|
|
1143
|
-
DropdownMenuItem,
|
|
1144
|
-
DropdownMenuCheckboxItem,
|
|
1145
|
-
DropdownMenuRadioItem,
|
|
1146
|
-
DropdownMenuLabel,
|
|
1147
|
-
DropdownMenuSeparator,
|
|
1148
|
-
DropdownMenuShortcut,
|
|
1149
|
-
DropdownMenuGroup,
|
|
1150
|
-
DropdownMenuPortal,
|
|
1151
|
-
DropdownMenuSub,
|
|
1152
|
-
DropdownMenuSubContent,
|
|
1153
|
-
DropdownMenuSubTrigger,
|
|
1154
|
-
DropdownMenuRadioGroup,
|
|
1155
|
-
}
|
|
1117
|
+
export { Toggle, toggleVariants }
|
|
1156
1118
|
`, prefix)
|
|
1157
1119
|
}
|
|
1158
1120
|
]
|
|
1159
1121
|
},
|
|
1160
|
-
"
|
|
1161
|
-
name: "
|
|
1162
|
-
description: "A text
|
|
1122
|
+
"text-field": {
|
|
1123
|
+
name: "text-field",
|
|
1124
|
+
description: "A text field with label, helper text, icons, and validation states",
|
|
1163
1125
|
dependencies: [
|
|
1164
1126
|
"class-variance-authority",
|
|
1165
1127
|
"clsx",
|
|
1166
|
-
"tailwind-merge"
|
|
1128
|
+
"tailwind-merge",
|
|
1129
|
+
"lucide-react"
|
|
1167
1130
|
],
|
|
1168
1131
|
files: [
|
|
1169
1132
|
{
|
|
1170
|
-
name: "
|
|
1133
|
+
name: "text-field.tsx",
|
|
1171
1134
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
1172
1135
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
1136
|
+
import { Loader2 } from "lucide-react"
|
|
1173
1137
|
|
|
1174
1138
|
import { cn } from "../../lib/utils"
|
|
1175
1139
|
|
|
1176
1140
|
/**
|
|
1177
|
-
*
|
|
1141
|
+
* TextField container variants for when icons/prefix/suffix are present
|
|
1178
1142
|
*/
|
|
1179
|
-
const
|
|
1143
|
+
const textFieldContainerVariants = cva(
|
|
1144
|
+
"relative flex items-center rounded bg-white transition-all",
|
|
1145
|
+
{
|
|
1146
|
+
variants: {
|
|
1147
|
+
state: {
|
|
1148
|
+
default: "border border-[#E9E9E9] focus-within:border-[#2BBBC9]/50 focus-within:shadow-[0_0_0_1px_rgba(43,187,201,0.15)]",
|
|
1149
|
+
error: "border border-[#FF3B3B]/40 focus-within:border-[#FF3B3B]/60 focus-within:shadow-[0_0_0_1px_rgba(255,59,59,0.1)]",
|
|
1150
|
+
},
|
|
1151
|
+
disabled: {
|
|
1152
|
+
true: "cursor-not-allowed opacity-50 bg-[#F9FAFB]",
|
|
1153
|
+
false: "",
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
defaultVariants: {
|
|
1157
|
+
state: "default",
|
|
1158
|
+
disabled: false,
|
|
1159
|
+
},
|
|
1160
|
+
}
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* TextField input variants (standalone without container)
|
|
1165
|
+
*/
|
|
1166
|
+
const textFieldInputVariants = cva(
|
|
1180
1167
|
"h-10 w-full rounded bg-white px-4 py-2.5 text-sm text-[#333333] transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-[#333333] placeholder:text-[#9CA3AF] disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[#F9FAFB]",
|
|
1181
1168
|
{
|
|
1182
1169
|
variants: {
|
|
@@ -1192,84 +1179,236 @@ const inputVariants = cva(
|
|
|
1192
1179
|
)
|
|
1193
1180
|
|
|
1194
1181
|
/**
|
|
1195
|
-
* A
|
|
1182
|
+
* A comprehensive text field component with label, icons, validation states, and more.
|
|
1196
1183
|
*
|
|
1197
1184
|
* @example
|
|
1198
1185
|
* \`\`\`tsx
|
|
1199
|
-
* <
|
|
1200
|
-
* <
|
|
1201
|
-
* <
|
|
1186
|
+
* <TextField label="Email" placeholder="Enter your email" required />
|
|
1187
|
+
* <TextField label="Username" error="Username is taken" />
|
|
1188
|
+
* <TextField label="Website" prefix="https://" suffix=".com" />
|
|
1202
1189
|
* \`\`\`
|
|
1203
1190
|
*/
|
|
1204
|
-
export interface
|
|
1191
|
+
export interface TextFieldProps
|
|
1205
1192
|
extends Omit<React.ComponentProps<"input">, "size">,
|
|
1206
|
-
VariantProps<typeof
|
|
1193
|
+
VariantProps<typeof textFieldInputVariants> {
|
|
1194
|
+
/** Label text displayed above the input */
|
|
1195
|
+
label?: string
|
|
1196
|
+
/** Shows red asterisk next to label when true */
|
|
1197
|
+
required?: boolean
|
|
1198
|
+
/** Helper text displayed below the input */
|
|
1199
|
+
helperText?: string
|
|
1200
|
+
/** Error message - shows error state with red styling */
|
|
1201
|
+
error?: string
|
|
1202
|
+
/** Icon displayed on the left inside the input */
|
|
1203
|
+
leftIcon?: React.ReactNode
|
|
1204
|
+
/** Icon displayed on the right inside the input */
|
|
1205
|
+
rightIcon?: React.ReactNode
|
|
1206
|
+
/** Text prefix inside input (e.g., "https://") */
|
|
1207
|
+
prefix?: string
|
|
1208
|
+
/** Text suffix inside input (e.g., ".com") */
|
|
1209
|
+
suffix?: string
|
|
1210
|
+
/** Shows character count when maxLength is set */
|
|
1211
|
+
showCount?: boolean
|
|
1212
|
+
/** Shows loading spinner inside input */
|
|
1213
|
+
loading?: boolean
|
|
1214
|
+
/** Additional class for the wrapper container */
|
|
1215
|
+
wrapperClassName?: string
|
|
1216
|
+
/** Additional class for the label */
|
|
1217
|
+
labelClassName?: string
|
|
1218
|
+
/** Additional class for the input container (includes prefix/suffix/icons) */
|
|
1219
|
+
inputContainerClassName?: string
|
|
1220
|
+
}
|
|
1207
1221
|
|
|
1208
|
-
const
|
|
1209
|
-
(
|
|
1210
|
-
|
|
1222
|
+
const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|
1223
|
+
(
|
|
1224
|
+
{
|
|
1225
|
+
className,
|
|
1226
|
+
wrapperClassName,
|
|
1227
|
+
labelClassName,
|
|
1228
|
+
inputContainerClassName,
|
|
1229
|
+
state,
|
|
1230
|
+
label,
|
|
1231
|
+
required,
|
|
1232
|
+
helperText,
|
|
1233
|
+
error,
|
|
1234
|
+
leftIcon,
|
|
1235
|
+
rightIcon,
|
|
1236
|
+
prefix,
|
|
1237
|
+
suffix,
|
|
1238
|
+
showCount,
|
|
1239
|
+
loading,
|
|
1240
|
+
maxLength,
|
|
1241
|
+
value,
|
|
1242
|
+
defaultValue,
|
|
1243
|
+
onChange,
|
|
1244
|
+
disabled,
|
|
1245
|
+
id,
|
|
1246
|
+
...props
|
|
1247
|
+
},
|
|
1248
|
+
ref
|
|
1249
|
+
) => {
|
|
1250
|
+
// Internal state for character count in uncontrolled mode
|
|
1251
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '')
|
|
1252
|
+
|
|
1253
|
+
// Determine if controlled
|
|
1254
|
+
const isControlled = value !== undefined
|
|
1255
|
+
const currentValue = isControlled ? value : internalValue
|
|
1256
|
+
|
|
1257
|
+
// Derive state from props
|
|
1258
|
+
const derivedState = error ? 'error' : (state ?? 'default')
|
|
1259
|
+
|
|
1260
|
+
// Handle change for both controlled and uncontrolled
|
|
1261
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1262
|
+
if (!isControlled) {
|
|
1263
|
+
setInternalValue(e.target.value)
|
|
1264
|
+
}
|
|
1265
|
+
onChange?.(e)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Determine if we need the container wrapper (for icons/prefix/suffix)
|
|
1269
|
+
const hasAddons = leftIcon || rightIcon || prefix || suffix || loading
|
|
1270
|
+
|
|
1271
|
+
// Character count
|
|
1272
|
+
const charCount = String(currentValue).length
|
|
1273
|
+
|
|
1274
|
+
// Generate unique IDs for accessibility
|
|
1275
|
+
const generatedId = React.useId()
|
|
1276
|
+
const inputId = id || generatedId
|
|
1277
|
+
const helperId = \`\${inputId}-helper\`
|
|
1278
|
+
const errorId = \`\${inputId}-error\`
|
|
1279
|
+
|
|
1280
|
+
// Determine aria-describedby
|
|
1281
|
+
const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined
|
|
1282
|
+
|
|
1283
|
+
// Render the input element
|
|
1284
|
+
const inputElement = (
|
|
1211
1285
|
<input
|
|
1212
|
-
type={type}
|
|
1213
|
-
className={cn(inputVariants({ state, className }))}
|
|
1214
1286
|
ref={ref}
|
|
1287
|
+
id={inputId}
|
|
1288
|
+
className={cn(
|
|
1289
|
+
hasAddons
|
|
1290
|
+
? "flex-1 bg-transparent border-0 outline-none focus:ring-0 px-0 h-full text-sm text-[#333333] placeholder:text-[#9CA3AF] disabled:cursor-not-allowed"
|
|
1291
|
+
: textFieldInputVariants({ state: derivedState, className })
|
|
1292
|
+
)}
|
|
1293
|
+
disabled={disabled || loading}
|
|
1294
|
+
maxLength={maxLength}
|
|
1295
|
+
value={isControlled ? value : undefined}
|
|
1296
|
+
defaultValue={!isControlled ? defaultValue : undefined}
|
|
1297
|
+
onChange={handleChange}
|
|
1298
|
+
aria-invalid={!!error}
|
|
1299
|
+
aria-describedby={ariaDescribedBy}
|
|
1215
1300
|
{...props}
|
|
1216
1301
|
/>
|
|
1217
1302
|
)
|
|
1303
|
+
|
|
1304
|
+
return (
|
|
1305
|
+
<div className={cn("flex flex-col gap-1", wrapperClassName)}>
|
|
1306
|
+
{/* Label */}
|
|
1307
|
+
{label && (
|
|
1308
|
+
<label
|
|
1309
|
+
htmlFor={inputId}
|
|
1310
|
+
className={cn("text-sm font-medium text-[#333333]", labelClassName)}
|
|
1311
|
+
>
|
|
1312
|
+
{label}
|
|
1313
|
+
{required && <span className="text-[#FF3B3B] ml-0.5">*</span>}
|
|
1314
|
+
</label>
|
|
1315
|
+
)}
|
|
1316
|
+
|
|
1317
|
+
{/* Input or Input Container */}
|
|
1318
|
+
{hasAddons ? (
|
|
1319
|
+
<div
|
|
1320
|
+
className={cn(
|
|
1321
|
+
textFieldContainerVariants({ state: derivedState, disabled: disabled || loading }),
|
|
1322
|
+
"h-10 px-4",
|
|
1323
|
+
inputContainerClassName
|
|
1324
|
+
)}
|
|
1325
|
+
>
|
|
1326
|
+
{prefix && <span className="text-sm text-[#6B7280] mr-2 select-none">{prefix}</span>}
|
|
1327
|
+
{leftIcon && <span className="mr-2 text-[#6B7280] [&_svg]:size-4 flex-shrink-0">{leftIcon}</span>}
|
|
1328
|
+
{inputElement}
|
|
1329
|
+
{loading && <Loader2 className="animate-spin size-4 text-[#6B7280] ml-2 flex-shrink-0" />}
|
|
1330
|
+
{!loading && rightIcon && <span className="ml-2 text-[#6B7280] [&_svg]:size-4 flex-shrink-0">{rightIcon}</span>}
|
|
1331
|
+
{suffix && <span className="text-sm text-[#6B7280] ml-2 select-none">{suffix}</span>}
|
|
1332
|
+
</div>
|
|
1333
|
+
) : (
|
|
1334
|
+
inputElement
|
|
1335
|
+
)}
|
|
1336
|
+
|
|
1337
|
+
{/* Helper text / Error message / Character count */}
|
|
1338
|
+
{(error || helperText || (showCount && maxLength)) && (
|
|
1339
|
+
<div className="flex justify-between items-start gap-2">
|
|
1340
|
+
{error ? (
|
|
1341
|
+
<span id={errorId} className="text-xs text-[#FF3B3B]">
|
|
1342
|
+
{error}
|
|
1343
|
+
</span>
|
|
1344
|
+
) : helperText ? (
|
|
1345
|
+
<span id={helperId} className="text-xs text-[#6B7280]">
|
|
1346
|
+
{helperText}
|
|
1347
|
+
</span>
|
|
1348
|
+
) : (
|
|
1349
|
+
<span />
|
|
1350
|
+
)}
|
|
1351
|
+
{showCount && maxLength && (
|
|
1352
|
+
<span
|
|
1353
|
+
className={cn(
|
|
1354
|
+
"text-xs",
|
|
1355
|
+
charCount > maxLength ? "text-[#FF3B3B]" : "text-[#6B7280]"
|
|
1356
|
+
)}
|
|
1357
|
+
>
|
|
1358
|
+
{charCount}/{maxLength}
|
|
1359
|
+
</span>
|
|
1360
|
+
)}
|
|
1361
|
+
</div>
|
|
1362
|
+
)}
|
|
1363
|
+
</div>
|
|
1364
|
+
)
|
|
1218
1365
|
}
|
|
1219
1366
|
)
|
|
1220
|
-
|
|
1367
|
+
TextField.displayName = "TextField"
|
|
1221
1368
|
|
|
1222
|
-
export {
|
|
1369
|
+
export { TextField, textFieldContainerVariants, textFieldInputVariants }
|
|
1223
1370
|
`, prefix)
|
|
1224
1371
|
}
|
|
1225
1372
|
]
|
|
1226
1373
|
},
|
|
1227
|
-
"
|
|
1228
|
-
name: "
|
|
1229
|
-
description: "A
|
|
1230
|
-
dependencies: [
|
|
1231
|
-
"
|
|
1232
|
-
"clsx",
|
|
1233
|
-
"tailwind-merge",
|
|
1234
|
-
"lucide-react"
|
|
1235
|
-
],
|
|
1236
|
-
files: [
|
|
1237
|
-
{
|
|
1238
|
-
name: "
|
|
1239
|
-
content: prefixTailwindClasses(`import * as React from "react"
|
|
1240
|
-
import {
|
|
1241
|
-
import { Check, ChevronDown, X, Loader2 } from "lucide-react"
|
|
1242
|
-
|
|
1243
|
-
import { cn } from "../../lib/utils"
|
|
1244
|
-
|
|
1245
|
-
/**
|
|
1246
|
-
* MultiSelect trigger variants matching TextField styling
|
|
1247
|
-
*/
|
|
1248
|
-
const multiSelectTriggerVariants = cva(
|
|
1249
|
-
"flex min-h-10 w-full items-center justify-between rounded bg-white px-4 py-2 text-sm text-[#333333] transition-all disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[#F9FAFB]",
|
|
1250
|
-
{
|
|
1251
|
-
variants: {
|
|
1252
|
-
state: {
|
|
1253
|
-
default: "border border-[#E9E9E9] focus:outline-none focus:border-[#2BBBC9]/50 focus:shadow-[0_0_0_1px_rgba(43,187,201,0.15)]",
|
|
1254
|
-
error: "border border-[#FF3B3B]/40 focus:outline-none focus:border-[#FF3B3B]/60 focus:shadow-[0_0_0_1px_rgba(255,59,59,0.1)]",
|
|
1255
|
-
},
|
|
1256
|
-
},
|
|
1257
|
-
defaultVariants: {
|
|
1258
|
-
state: "default",
|
|
1259
|
-
},
|
|
1260
|
-
}
|
|
1261
|
-
)
|
|
1374
|
+
"select-field": {
|
|
1375
|
+
name: "select-field",
|
|
1376
|
+
description: "A select field with label, helper text, and validation states",
|
|
1377
|
+
dependencies: [
|
|
1378
|
+
"@radix-ui/react-select",
|
|
1379
|
+
"clsx",
|
|
1380
|
+
"tailwind-merge",
|
|
1381
|
+
"lucide-react"
|
|
1382
|
+
],
|
|
1383
|
+
files: [
|
|
1384
|
+
{
|
|
1385
|
+
name: "select-field.tsx",
|
|
1386
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
1387
|
+
import { Loader2 } from "lucide-react"
|
|
1262
1388
|
|
|
1263
|
-
|
|
1389
|
+
import { cn } from "../../lib/utils"
|
|
1390
|
+
import {
|
|
1391
|
+
Select,
|
|
1392
|
+
SelectContent,
|
|
1393
|
+
SelectGroup,
|
|
1394
|
+
SelectItem,
|
|
1395
|
+
SelectLabel,
|
|
1396
|
+
SelectTrigger,
|
|
1397
|
+
SelectValue,
|
|
1398
|
+
} from "./select"
|
|
1399
|
+
|
|
1400
|
+
export interface SelectOption {
|
|
1264
1401
|
/** The value of the option */
|
|
1265
1402
|
value: string
|
|
1266
1403
|
/** The display label of the option */
|
|
1267
1404
|
label: string
|
|
1268
1405
|
/** Whether the option is disabled */
|
|
1269
1406
|
disabled?: boolean
|
|
1407
|
+
/** Group name for grouping options */
|
|
1408
|
+
group?: string
|
|
1270
1409
|
}
|
|
1271
1410
|
|
|
1272
|
-
export interface
|
|
1411
|
+
export interface SelectFieldProps {
|
|
1273
1412
|
/** Label text displayed above the select */
|
|
1274
1413
|
label?: string
|
|
1275
1414
|
/** Shows red asterisk next to label when true */
|
|
@@ -1284,20 +1423,18 @@ export interface MultiSelectProps extends VariantProps<typeof multiSelectTrigger
|
|
|
1284
1423
|
loading?: boolean
|
|
1285
1424
|
/** Placeholder text when no value selected */
|
|
1286
1425
|
placeholder?: string
|
|
1287
|
-
/** Currently selected
|
|
1288
|
-
value?: string
|
|
1289
|
-
/** Default
|
|
1290
|
-
defaultValue?: string
|
|
1291
|
-
/** Callback when
|
|
1292
|
-
onValueChange?: (value: string
|
|
1426
|
+
/** Currently selected value (controlled) */
|
|
1427
|
+
value?: string
|
|
1428
|
+
/** Default value (uncontrolled) */
|
|
1429
|
+
defaultValue?: string
|
|
1430
|
+
/** Callback when value changes */
|
|
1431
|
+
onValueChange?: (value: string) => void
|
|
1293
1432
|
/** Options to display */
|
|
1294
|
-
options:
|
|
1433
|
+
options: SelectOption[]
|
|
1295
1434
|
/** Enable search/filter functionality */
|
|
1296
1435
|
searchable?: boolean
|
|
1297
1436
|
/** Search placeholder text */
|
|
1298
1437
|
searchPlaceholder?: string
|
|
1299
|
-
/** Maximum selections allowed */
|
|
1300
|
-
maxSelections?: number
|
|
1301
1438
|
/** Additional class for wrapper */
|
|
1302
1439
|
wrapperClassName?: string
|
|
1303
1440
|
/** Additional class for trigger */
|
|
@@ -1311,23 +1448,23 @@ export interface MultiSelectProps extends VariantProps<typeof multiSelectTrigger
|
|
|
1311
1448
|
}
|
|
1312
1449
|
|
|
1313
1450
|
/**
|
|
1314
|
-
* A
|
|
1451
|
+
* A comprehensive select field component with label, icons, validation states, and more.
|
|
1315
1452
|
*
|
|
1316
1453
|
* @example
|
|
1317
1454
|
* \`\`\`tsx
|
|
1318
|
-
* <
|
|
1319
|
-
* label="
|
|
1320
|
-
* placeholder="Select
|
|
1455
|
+
* <SelectField
|
|
1456
|
+
* label="Authentication"
|
|
1457
|
+
* placeholder="Select authentication method"
|
|
1321
1458
|
* options={[
|
|
1322
|
-
* { value: '
|
|
1323
|
-
* { value: '
|
|
1324
|
-
* { value: '
|
|
1459
|
+
* { value: 'none', label: 'None' },
|
|
1460
|
+
* { value: 'basic', label: 'Basic Auth' },
|
|
1461
|
+
* { value: 'bearer', label: 'Bearer Token' },
|
|
1325
1462
|
* ]}
|
|
1326
|
-
*
|
|
1463
|
+
* required
|
|
1327
1464
|
* />
|
|
1328
1465
|
* \`\`\`
|
|
1329
1466
|
*/
|
|
1330
|
-
const
|
|
1467
|
+
const SelectField = React.forwardRef<HTMLButtonElement, SelectFieldProps>(
|
|
1331
1468
|
(
|
|
1332
1469
|
{
|
|
1333
1470
|
label,
|
|
@@ -1336,39 +1473,26 @@ const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
|
|
1336
1473
|
error,
|
|
1337
1474
|
disabled,
|
|
1338
1475
|
loading,
|
|
1339
|
-
placeholder = "Select
|
|
1476
|
+
placeholder = "Select an option",
|
|
1340
1477
|
value,
|
|
1341
|
-
defaultValue
|
|
1478
|
+
defaultValue,
|
|
1342
1479
|
onValueChange,
|
|
1343
1480
|
options,
|
|
1344
1481
|
searchable,
|
|
1345
1482
|
searchPlaceholder = "Search...",
|
|
1346
|
-
maxSelections,
|
|
1347
1483
|
wrapperClassName,
|
|
1348
1484
|
triggerClassName,
|
|
1349
1485
|
labelClassName,
|
|
1350
|
-
state,
|
|
1351
1486
|
id,
|
|
1352
1487
|
name,
|
|
1353
1488
|
},
|
|
1354
1489
|
ref
|
|
1355
1490
|
) => {
|
|
1356
|
-
// Internal state for
|
|
1357
|
-
const [internalValue, setInternalValue] = React.useState<string[]>(defaultValue)
|
|
1358
|
-
// Dropdown open state
|
|
1359
|
-
const [isOpen, setIsOpen] = React.useState(false)
|
|
1360
|
-
// Search query
|
|
1491
|
+
// Internal state for search
|
|
1361
1492
|
const [searchQuery, setSearchQuery] = React.useState("")
|
|
1362
1493
|
|
|
1363
|
-
// Container ref for click outside detection
|
|
1364
|
-
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
1365
|
-
|
|
1366
|
-
// Determine if controlled
|
|
1367
|
-
const isControlled = value !== undefined
|
|
1368
|
-
const selectedValues = isControlled ? value : internalValue
|
|
1369
|
-
|
|
1370
1494
|
// Derive state from props
|
|
1371
|
-
const derivedState = error ? "error" :
|
|
1495
|
+
const derivedState = error ? "error" : "default"
|
|
1372
1496
|
|
|
1373
1497
|
// Generate unique IDs for accessibility
|
|
1374
1498
|
const generatedId = React.useId()
|
|
@@ -1379,250 +1503,140 @@ const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
|
|
1379
1503
|
// Determine aria-describedby
|
|
1380
1504
|
const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined
|
|
1381
1505
|
|
|
1382
|
-
//
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
|
1387
|
-
)
|
|
1388
|
-
}, [options, searchable, searchQuery])
|
|
1389
|
-
|
|
1390
|
-
// Get selected option labels
|
|
1391
|
-
const selectedLabels = React.useMemo(() => {
|
|
1392
|
-
return selectedValues
|
|
1393
|
-
.map((v) => options.find((o) => o.value === v)?.label)
|
|
1394
|
-
.filter(Boolean) as string[]
|
|
1395
|
-
}, [selectedValues, options])
|
|
1396
|
-
|
|
1397
|
-
// Handle toggle selection
|
|
1398
|
-
const toggleOption = (optionValue: string) => {
|
|
1399
|
-
const newValues = selectedValues.includes(optionValue)
|
|
1400
|
-
? selectedValues.filter((v) => v !== optionValue)
|
|
1401
|
-
: maxSelections && selectedValues.length >= maxSelections
|
|
1402
|
-
? selectedValues
|
|
1403
|
-
: [...selectedValues, optionValue]
|
|
1404
|
-
|
|
1405
|
-
if (!isControlled) {
|
|
1406
|
-
setInternalValue(newValues)
|
|
1407
|
-
}
|
|
1408
|
-
onValueChange?.(newValues)
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
// Handle remove tag
|
|
1412
|
-
const removeValue = (valueToRemove: string, e: React.MouseEvent) => {
|
|
1413
|
-
e.stopPropagation()
|
|
1414
|
-
const newValues = selectedValues.filter((v) => v !== valueToRemove)
|
|
1415
|
-
if (!isControlled) {
|
|
1416
|
-
setInternalValue(newValues)
|
|
1417
|
-
}
|
|
1418
|
-
onValueChange?.(newValues)
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
// Handle clear all
|
|
1422
|
-
const clearAll = (e: React.MouseEvent) => {
|
|
1423
|
-
e.stopPropagation()
|
|
1424
|
-
if (!isControlled) {
|
|
1425
|
-
setInternalValue([])
|
|
1426
|
-
}
|
|
1427
|
-
onValueChange?.([])
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
// Close dropdown when clicking outside
|
|
1431
|
-
React.useEffect(() => {
|
|
1432
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
1433
|
-
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
1434
|
-
setIsOpen(false)
|
|
1435
|
-
setSearchQuery("")
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
document.addEventListener("mousedown", handleClickOutside)
|
|
1440
|
-
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
1441
|
-
}, [])
|
|
1442
|
-
|
|
1443
|
-
// Handle keyboard navigation
|
|
1444
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
1445
|
-
if (e.key === "Escape") {
|
|
1446
|
-
setIsOpen(false)
|
|
1447
|
-
setSearchQuery("")
|
|
1448
|
-
} else if (e.key === "Enter" || e.key === " ") {
|
|
1449
|
-
if (!isOpen) {
|
|
1450
|
-
e.preventDefault()
|
|
1451
|
-
setIsOpen(true)
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
return (
|
|
1457
|
-
<div
|
|
1458
|
-
ref={containerRef}
|
|
1459
|
-
className={cn("flex flex-col gap-1 relative", wrapperClassName)}
|
|
1460
|
-
>
|
|
1461
|
-
{/* Label */}
|
|
1462
|
-
{label && (
|
|
1463
|
-
<label
|
|
1464
|
-
htmlFor={selectId}
|
|
1465
|
-
className={cn("text-sm font-medium text-[#333333]", labelClassName)}
|
|
1466
|
-
>
|
|
1467
|
-
{label}
|
|
1468
|
-
{required && <span className="text-[#FF3B3B] ml-0.5">*</span>}
|
|
1469
|
-
</label>
|
|
1470
|
-
)}
|
|
1471
|
-
|
|
1472
|
-
{/* Trigger */}
|
|
1473
|
-
<button
|
|
1474
|
-
ref={ref}
|
|
1475
|
-
id={selectId}
|
|
1476
|
-
type="button"
|
|
1477
|
-
role="combobox"
|
|
1478
|
-
aria-expanded={isOpen}
|
|
1479
|
-
aria-haspopup="listbox"
|
|
1480
|
-
aria-invalid={!!error}
|
|
1481
|
-
aria-describedby={ariaDescribedBy}
|
|
1482
|
-
disabled={disabled || loading}
|
|
1483
|
-
onClick={() => !disabled && !loading && setIsOpen(!isOpen)}
|
|
1484
|
-
onKeyDown={handleKeyDown}
|
|
1485
|
-
className={cn(
|
|
1486
|
-
multiSelectTriggerVariants({ state: derivedState }),
|
|
1487
|
-
"text-left gap-2",
|
|
1488
|
-
triggerClassName
|
|
1489
|
-
)}
|
|
1490
|
-
>
|
|
1491
|
-
<div className="flex-1 flex flex-wrap gap-1">
|
|
1492
|
-
{selectedValues.length === 0 ? (
|
|
1493
|
-
<span className="text-[#9CA3AF]">{placeholder}</span>
|
|
1494
|
-
) : (
|
|
1495
|
-
selectedLabels.map((label, index) => (
|
|
1496
|
-
<span
|
|
1497
|
-
key={selectedValues[index]}
|
|
1498
|
-
className="inline-flex items-center gap-1 bg-[#F3F4F6] text-[#333333] text-xs px-2 py-0.5 rounded"
|
|
1499
|
-
>
|
|
1500
|
-
{label}
|
|
1501
|
-
<span
|
|
1502
|
-
role="button"
|
|
1503
|
-
tabIndex={0}
|
|
1504
|
-
onClick={(e) => removeValue(selectedValues[index], e)}
|
|
1505
|
-
onKeyDown={(e) => {
|
|
1506
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
1507
|
-
e.preventDefault()
|
|
1508
|
-
removeValue(selectedValues[index], e as unknown as React.MouseEvent)
|
|
1509
|
-
}
|
|
1510
|
-
}}
|
|
1511
|
-
className="cursor-pointer hover:text-[#FF3B3B] focus:outline-none"
|
|
1512
|
-
aria-label={\`Remove \${label}\`}
|
|
1513
|
-
>
|
|
1514
|
-
<X className="size-3" />
|
|
1515
|
-
</span>
|
|
1516
|
-
</span>
|
|
1517
|
-
))
|
|
1518
|
-
)}
|
|
1519
|
-
</div>
|
|
1520
|
-
<div className="flex items-center gap-1">
|
|
1521
|
-
{selectedValues.length > 0 && (
|
|
1522
|
-
<span
|
|
1523
|
-
role="button"
|
|
1524
|
-
tabIndex={0}
|
|
1525
|
-
onClick={clearAll}
|
|
1526
|
-
onKeyDown={(e) => {
|
|
1527
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
1528
|
-
e.preventDefault()
|
|
1529
|
-
clearAll(e as unknown as React.MouseEvent)
|
|
1530
|
-
}
|
|
1531
|
-
}}
|
|
1532
|
-
className="p-0.5 cursor-pointer hover:text-[#FF3B3B] focus:outline-none"
|
|
1533
|
-
aria-label="Clear all"
|
|
1534
|
-
>
|
|
1535
|
-
<X className="size-4 text-[#6B7280]" />
|
|
1536
|
-
</span>
|
|
1537
|
-
)}
|
|
1538
|
-
{loading ? (
|
|
1539
|
-
<Loader2 className="size-4 animate-spin text-[#6B7280]" />
|
|
1540
|
-
) : (
|
|
1541
|
-
<ChevronDown
|
|
1542
|
-
className={cn(
|
|
1543
|
-
"size-4 text-[#6B7280] transition-transform",
|
|
1544
|
-
isOpen && "rotate-180"
|
|
1545
|
-
)}
|
|
1546
|
-
/>
|
|
1547
|
-
)}
|
|
1548
|
-
</div>
|
|
1549
|
-
</button>
|
|
1506
|
+
// Group options by group property
|
|
1507
|
+
const groupedOptions = React.useMemo(() => {
|
|
1508
|
+
const groups: Record<string, SelectOption[]> = {}
|
|
1509
|
+
const ungrouped: SelectOption[] = []
|
|
1550
1510
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1511
|
+
options.forEach((option) => {
|
|
1512
|
+
// Filter by search query if searchable
|
|
1513
|
+
if (searchable && searchQuery) {
|
|
1514
|
+
if (!option.label.toLowerCase().includes(searchQuery.toLowerCase())) {
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if (option.group) {
|
|
1520
|
+
if (!groups[option.group]) {
|
|
1521
|
+
groups[option.group] = []
|
|
1522
|
+
}
|
|
1523
|
+
groups[option.group].push(option)
|
|
1524
|
+
} else {
|
|
1525
|
+
ungrouped.push(option)
|
|
1526
|
+
}
|
|
1527
|
+
})
|
|
1528
|
+
|
|
1529
|
+
return { groups, ungrouped }
|
|
1530
|
+
}, [options, searchable, searchQuery])
|
|
1531
|
+
|
|
1532
|
+
const hasGroups = Object.keys(groupedOptions.groups).length > 0
|
|
1533
|
+
|
|
1534
|
+
// Handle search input change
|
|
1535
|
+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1536
|
+
setSearchQuery(e.target.value)
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Reset search when dropdown closes
|
|
1540
|
+
const handleOpenChange = (open: boolean) => {
|
|
1541
|
+
if (!open) {
|
|
1542
|
+
setSearchQuery("")
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return (
|
|
1547
|
+
<div className={cn("flex flex-col gap-1", wrapperClassName)}>
|
|
1548
|
+
{/* Label */}
|
|
1549
|
+
{label && (
|
|
1550
|
+
<label
|
|
1551
|
+
htmlFor={selectId}
|
|
1552
|
+
className={cn("text-sm font-medium text-[#333333]", labelClassName)}
|
|
1553
|
+
>
|
|
1554
|
+
{label}
|
|
1555
|
+
{required && <span className="text-[#FF3B3B] ml-0.5">*</span>}
|
|
1556
|
+
</label>
|
|
1557
|
+
)}
|
|
1558
|
+
|
|
1559
|
+
{/* Select */}
|
|
1560
|
+
<Select
|
|
1561
|
+
value={value}
|
|
1562
|
+
defaultValue={defaultValue}
|
|
1563
|
+
onValueChange={onValueChange}
|
|
1564
|
+
disabled={disabled || loading}
|
|
1565
|
+
name={name}
|
|
1566
|
+
onOpenChange={handleOpenChange}
|
|
1567
|
+
>
|
|
1568
|
+
<SelectTrigger
|
|
1569
|
+
ref={ref}
|
|
1570
|
+
id={selectId}
|
|
1571
|
+
state={derivedState}
|
|
1554
1572
|
className={cn(
|
|
1555
|
-
|
|
1556
|
-
|
|
1573
|
+
loading && "pr-10",
|
|
1574
|
+
triggerClassName
|
|
1557
1575
|
)}
|
|
1558
|
-
|
|
1559
|
-
aria-
|
|
1576
|
+
aria-invalid={!!error}
|
|
1577
|
+
aria-describedby={ariaDescribedBy}
|
|
1560
1578
|
>
|
|
1579
|
+
<SelectValue placeholder={placeholder} />
|
|
1580
|
+
{loading && (
|
|
1581
|
+
<Loader2 className="absolute right-8 size-4 animate-spin text-[#6B7280]" />
|
|
1582
|
+
)}
|
|
1583
|
+
</SelectTrigger>
|
|
1584
|
+
<SelectContent>
|
|
1561
1585
|
{/* Search input */}
|
|
1562
1586
|
{searchable && (
|
|
1563
|
-
<div className="
|
|
1587
|
+
<div className="px-2 pb-2">
|
|
1564
1588
|
<input
|
|
1565
1589
|
type="text"
|
|
1566
1590
|
placeholder={searchPlaceholder}
|
|
1567
1591
|
value={searchQuery}
|
|
1568
|
-
onChange={
|
|
1592
|
+
onChange={handleSearchChange}
|
|
1569
1593
|
className="w-full h-8 px-3 text-sm border border-[#E9E9E9] rounded bg-white placeholder:text-[#9CA3AF] focus:outline-none focus:border-[#2BBBC9]/50"
|
|
1594
|
+
// Prevent closing dropdown when clicking input
|
|
1570
1595
|
onClick={(e) => e.stopPropagation()}
|
|
1596
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
1571
1597
|
/>
|
|
1572
1598
|
</div>
|
|
1573
1599
|
)}
|
|
1574
1600
|
|
|
1575
|
-
{/*
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
option.disabled ||
|
|
1586
|
-
(!isSelected && maxSelections && selectedValues.length >= maxSelections)
|
|
1601
|
+
{/* Ungrouped options */}
|
|
1602
|
+
{groupedOptions.ungrouped.map((option) => (
|
|
1603
|
+
<SelectItem
|
|
1604
|
+
key={option.value}
|
|
1605
|
+
value={option.value}
|
|
1606
|
+
disabled={option.disabled}
|
|
1607
|
+
>
|
|
1608
|
+
{option.label}
|
|
1609
|
+
</SelectItem>
|
|
1610
|
+
))}
|
|
1587
1611
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1612
|
+
{/* Grouped options */}
|
|
1613
|
+
{hasGroups &&
|
|
1614
|
+
Object.entries(groupedOptions.groups).map(([groupName, groupOptions]) => (
|
|
1615
|
+
<SelectGroup key={groupName}>
|
|
1616
|
+
<SelectLabel>{groupName}</SelectLabel>
|
|
1617
|
+
{groupOptions.map((option) => (
|
|
1618
|
+
<SelectItem
|
|
1590
1619
|
key={option.value}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
aria-selected={isSelected}
|
|
1594
|
-
disabled={isDisabled}
|
|
1595
|
-
onClick={() => !isDisabled && toggleOption(option.value)}
|
|
1596
|
-
className={cn(
|
|
1597
|
-
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-2 pl-4 pr-8 text-sm text-[#333333] outline-none",
|
|
1598
|
-
"hover:bg-[#F3F4F6] focus:bg-[#F3F4F6]",
|
|
1599
|
-
isSelected && "bg-[#F3F4F6]",
|
|
1600
|
-
isDisabled && "pointer-events-none opacity-50"
|
|
1601
|
-
)}
|
|
1620
|
+
value={option.value}
|
|
1621
|
+
disabled={option.disabled}
|
|
1602
1622
|
>
|
|
1603
|
-
<span className="absolute right-2 flex size-4 items-center justify-center">
|
|
1604
|
-
{isSelected && <Check className="size-4 text-[#2BBBC9]" />}
|
|
1605
|
-
</span>
|
|
1606
1623
|
{option.label}
|
|
1607
|
-
</
|
|
1608
|
-
)
|
|
1609
|
-
|
|
1610
|
-
)}
|
|
1611
|
-
</div>
|
|
1612
|
-
|
|
1613
|
-
{/* Footer with count */}
|
|
1614
|
-
{maxSelections && (
|
|
1615
|
-
<div className="p-2 border-t border-[#E9E9E9] text-xs text-[#6B7280]">
|
|
1616
|
-
{selectedValues.length} / {maxSelections} selected
|
|
1617
|
-
</div>
|
|
1618
|
-
)}
|
|
1619
|
-
</div>
|
|
1620
|
-
)}
|
|
1624
|
+
</SelectItem>
|
|
1625
|
+
))}
|
|
1626
|
+
</SelectGroup>
|
|
1627
|
+
))}
|
|
1621
1628
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1629
|
+
{/* No results message */}
|
|
1630
|
+
{searchable &&
|
|
1631
|
+
searchQuery &&
|
|
1632
|
+
groupedOptions.ungrouped.length === 0 &&
|
|
1633
|
+
Object.keys(groupedOptions.groups).length === 0 && (
|
|
1634
|
+
<div className="py-6 text-center text-sm text-[#6B7280]">
|
|
1635
|
+
No results found
|
|
1636
|
+
</div>
|
|
1637
|
+
)}
|
|
1638
|
+
</SelectContent>
|
|
1639
|
+
</Select>
|
|
1626
1640
|
|
|
1627
1641
|
{/* Helper text / Error message */}
|
|
1628
1642
|
{(error || helperText) && (
|
|
@@ -1642,51 +1656,59 @@ const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
|
|
1642
1656
|
)
|
|
1643
1657
|
}
|
|
1644
1658
|
)
|
|
1645
|
-
|
|
1659
|
+
SelectField.displayName = "SelectField"
|
|
1646
1660
|
|
|
1647
|
-
export {
|
|
1661
|
+
export { SelectField }
|
|
1648
1662
|
`, prefix)
|
|
1649
1663
|
}
|
|
1650
1664
|
]
|
|
1651
1665
|
},
|
|
1652
|
-
"select
|
|
1653
|
-
name: "select
|
|
1654
|
-
description: "A select
|
|
1666
|
+
"multi-select": {
|
|
1667
|
+
name: "multi-select",
|
|
1668
|
+
description: "A multi-select dropdown component with search, badges, and async loading",
|
|
1655
1669
|
dependencies: [
|
|
1656
|
-
"
|
|
1670
|
+
"class-variance-authority",
|
|
1657
1671
|
"clsx",
|
|
1658
1672
|
"tailwind-merge",
|
|
1659
1673
|
"lucide-react"
|
|
1660
1674
|
],
|
|
1661
1675
|
files: [
|
|
1662
1676
|
{
|
|
1663
|
-
name: "select
|
|
1677
|
+
name: "multi-select.tsx",
|
|
1664
1678
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
1665
|
-
import {
|
|
1679
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
1680
|
+
import { Check, ChevronDown, X, Loader2 } from "lucide-react"
|
|
1666
1681
|
|
|
1667
1682
|
import { cn } from "../../lib/utils"
|
|
1668
|
-
import {
|
|
1669
|
-
Select,
|
|
1670
|
-
SelectContent,
|
|
1671
|
-
SelectGroup,
|
|
1672
|
-
SelectItem,
|
|
1673
|
-
SelectLabel,
|
|
1674
|
-
SelectTrigger,
|
|
1675
|
-
SelectValue,
|
|
1676
|
-
} from "./select"
|
|
1677
1683
|
|
|
1678
|
-
|
|
1684
|
+
/**
|
|
1685
|
+
* MultiSelect trigger variants matching TextField styling
|
|
1686
|
+
*/
|
|
1687
|
+
const multiSelectTriggerVariants = cva(
|
|
1688
|
+
"flex min-h-10 w-full items-center justify-between rounded bg-white px-4 py-2 text-sm text-[#333333] transition-all disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[#F9FAFB]",
|
|
1689
|
+
{
|
|
1690
|
+
variants: {
|
|
1691
|
+
state: {
|
|
1692
|
+
default: "border border-[#E9E9E9] focus:outline-none focus:border-[#2BBBC9]/50 focus:shadow-[0_0_0_1px_rgba(43,187,201,0.15)]",
|
|
1693
|
+
error: "border border-[#FF3B3B]/40 focus:outline-none focus:border-[#FF3B3B]/60 focus:shadow-[0_0_0_1px_rgba(255,59,59,0.1)]",
|
|
1694
|
+
},
|
|
1695
|
+
},
|
|
1696
|
+
defaultVariants: {
|
|
1697
|
+
state: "default",
|
|
1698
|
+
},
|
|
1699
|
+
}
|
|
1700
|
+
)
|
|
1701
|
+
|
|
1702
|
+
export interface MultiSelectOption {
|
|
1679
1703
|
/** The value of the option */
|
|
1680
1704
|
value: string
|
|
1681
1705
|
/** The display label of the option */
|
|
1682
1706
|
label: string
|
|
1683
1707
|
/** Whether the option is disabled */
|
|
1684
1708
|
disabled?: boolean
|
|
1685
|
-
/** Group name for grouping options */
|
|
1686
|
-
group?: string
|
|
1687
1709
|
}
|
|
1688
1710
|
|
|
1689
|
-
export interface
|
|
1711
|
+
export interface MultiSelectProps extends VariantProps<typeof multiSelectTriggerVariants> {
|
|
1690
1712
|
/** Label text displayed above the select */
|
|
1691
1713
|
label?: string
|
|
1692
1714
|
/** Shows red asterisk next to label when true */
|
|
@@ -1701,18 +1723,20 @@ export interface SelectFieldProps {
|
|
|
1701
1723
|
loading?: boolean
|
|
1702
1724
|
/** Placeholder text when no value selected */
|
|
1703
1725
|
placeholder?: string
|
|
1704
|
-
/** Currently selected
|
|
1705
|
-
value?: string
|
|
1706
|
-
/** Default
|
|
1707
|
-
defaultValue?: string
|
|
1708
|
-
/** Callback when
|
|
1709
|
-
onValueChange?: (value: string) => void
|
|
1726
|
+
/** Currently selected values (controlled) */
|
|
1727
|
+
value?: string[]
|
|
1728
|
+
/** Default values (uncontrolled) */
|
|
1729
|
+
defaultValue?: string[]
|
|
1730
|
+
/** Callback when values change */
|
|
1731
|
+
onValueChange?: (value: string[]) => void
|
|
1710
1732
|
/** Options to display */
|
|
1711
|
-
options:
|
|
1733
|
+
options: MultiSelectOption[]
|
|
1712
1734
|
/** Enable search/filter functionality */
|
|
1713
1735
|
searchable?: boolean
|
|
1714
1736
|
/** Search placeholder text */
|
|
1715
1737
|
searchPlaceholder?: string
|
|
1738
|
+
/** Maximum selections allowed */
|
|
1739
|
+
maxSelections?: number
|
|
1716
1740
|
/** Additional class for wrapper */
|
|
1717
1741
|
wrapperClassName?: string
|
|
1718
1742
|
/** Additional class for trigger */
|
|
@@ -1726,23 +1750,23 @@ export interface SelectFieldProps {
|
|
|
1726
1750
|
}
|
|
1727
1751
|
|
|
1728
1752
|
/**
|
|
1729
|
-
* A
|
|
1753
|
+
* A multi-select component with tags, search, and validation states.
|
|
1730
1754
|
*
|
|
1731
1755
|
* @example
|
|
1732
1756
|
* \`\`\`tsx
|
|
1733
|
-
* <
|
|
1734
|
-
* label="
|
|
1735
|
-
* placeholder="Select
|
|
1757
|
+
* <MultiSelect
|
|
1758
|
+
* label="Skills"
|
|
1759
|
+
* placeholder="Select skills"
|
|
1736
1760
|
* options={[
|
|
1737
|
-
* { value: '
|
|
1738
|
-
* { value: '
|
|
1739
|
-
* { value: '
|
|
1761
|
+
* { value: 'react', label: 'React' },
|
|
1762
|
+
* { value: 'vue', label: 'Vue' },
|
|
1763
|
+
* { value: 'angular', label: 'Angular' },
|
|
1740
1764
|
* ]}
|
|
1741
|
-
*
|
|
1765
|
+
* onValueChange={(values) => console.log(values)}
|
|
1742
1766
|
* />
|
|
1743
1767
|
* \`\`\`
|
|
1744
1768
|
*/
|
|
1745
|
-
const
|
|
1769
|
+
const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
|
1746
1770
|
(
|
|
1747
1771
|
{
|
|
1748
1772
|
label,
|
|
@@ -1751,26 +1775,39 @@ const SelectField = React.forwardRef<HTMLButtonElement, SelectFieldProps>(
|
|
|
1751
1775
|
error,
|
|
1752
1776
|
disabled,
|
|
1753
1777
|
loading,
|
|
1754
|
-
placeholder = "Select
|
|
1778
|
+
placeholder = "Select options",
|
|
1755
1779
|
value,
|
|
1756
|
-
defaultValue,
|
|
1780
|
+
defaultValue = [],
|
|
1757
1781
|
onValueChange,
|
|
1758
1782
|
options,
|
|
1759
1783
|
searchable,
|
|
1760
1784
|
searchPlaceholder = "Search...",
|
|
1785
|
+
maxSelections,
|
|
1761
1786
|
wrapperClassName,
|
|
1762
1787
|
triggerClassName,
|
|
1763
1788
|
labelClassName,
|
|
1789
|
+
state,
|
|
1764
1790
|
id,
|
|
1765
1791
|
name,
|
|
1766
1792
|
},
|
|
1767
1793
|
ref
|
|
1768
1794
|
) => {
|
|
1769
|
-
// Internal state for
|
|
1795
|
+
// Internal state for selected values (uncontrolled mode)
|
|
1796
|
+
const [internalValue, setInternalValue] = React.useState<string[]>(defaultValue)
|
|
1797
|
+
// Dropdown open state
|
|
1798
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
1799
|
+
// Search query
|
|
1770
1800
|
const [searchQuery, setSearchQuery] = React.useState("")
|
|
1771
1801
|
|
|
1802
|
+
// Container ref for click outside detection
|
|
1803
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
1804
|
+
|
|
1805
|
+
// Determine if controlled
|
|
1806
|
+
const isControlled = value !== undefined
|
|
1807
|
+
const selectedValues = isControlled ? value : internalValue
|
|
1808
|
+
|
|
1772
1809
|
// Derive state from props
|
|
1773
|
-
const derivedState = error ? "error" : "default"
|
|
1810
|
+
const derivedState = error ? "error" : (state ?? "default")
|
|
1774
1811
|
|
|
1775
1812
|
// Generate unique IDs for accessibility
|
|
1776
1813
|
const generatedId = React.useId()
|
|
@@ -1781,48 +1818,85 @@ const SelectField = React.forwardRef<HTMLButtonElement, SelectFieldProps>(
|
|
|
1781
1818
|
// Determine aria-describedby
|
|
1782
1819
|
const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined
|
|
1783
1820
|
|
|
1784
|
-
//
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1821
|
+
// Filter options by search query
|
|
1822
|
+
const filteredOptions = React.useMemo(() => {
|
|
1823
|
+
if (!searchable || !searchQuery) return options
|
|
1824
|
+
return options.filter((option) =>
|
|
1825
|
+
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
|
1826
|
+
)
|
|
1827
|
+
}, [options, searchable, searchQuery])
|
|
1788
1828
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1829
|
+
// Get selected option labels
|
|
1830
|
+
const selectedLabels = React.useMemo(() => {
|
|
1831
|
+
return selectedValues
|
|
1832
|
+
.map((v) => options.find((o) => o.value === v)?.label)
|
|
1833
|
+
.filter(Boolean) as string[]
|
|
1834
|
+
}, [selectedValues, options])
|
|
1796
1835
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
}
|
|
1805
|
-
})
|
|
1836
|
+
// Handle toggle selection
|
|
1837
|
+
const toggleOption = (optionValue: string) => {
|
|
1838
|
+
const newValues = selectedValues.includes(optionValue)
|
|
1839
|
+
? selectedValues.filter((v) => v !== optionValue)
|
|
1840
|
+
: maxSelections && selectedValues.length >= maxSelections
|
|
1841
|
+
? selectedValues
|
|
1842
|
+
: [...selectedValues, optionValue]
|
|
1806
1843
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1844
|
+
if (!isControlled) {
|
|
1845
|
+
setInternalValue(newValues)
|
|
1846
|
+
}
|
|
1847
|
+
onValueChange?.(newValues)
|
|
1848
|
+
}
|
|
1809
1849
|
|
|
1810
|
-
|
|
1850
|
+
// Handle remove tag
|
|
1851
|
+
const removeValue = (valueToRemove: string, e: React.MouseEvent) => {
|
|
1852
|
+
e.stopPropagation()
|
|
1853
|
+
const newValues = selectedValues.filter((v) => v !== valueToRemove)
|
|
1854
|
+
if (!isControlled) {
|
|
1855
|
+
setInternalValue(newValues)
|
|
1856
|
+
}
|
|
1857
|
+
onValueChange?.(newValues)
|
|
1858
|
+
}
|
|
1811
1859
|
|
|
1812
|
-
// Handle
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1860
|
+
// Handle clear all
|
|
1861
|
+
const clearAll = (e: React.MouseEvent) => {
|
|
1862
|
+
e.stopPropagation()
|
|
1863
|
+
if (!isControlled) {
|
|
1864
|
+
setInternalValue([])
|
|
1865
|
+
}
|
|
1866
|
+
onValueChange?.([])
|
|
1815
1867
|
}
|
|
1816
1868
|
|
|
1817
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
1869
|
+
// Close dropdown when clicking outside
|
|
1870
|
+
React.useEffect(() => {
|
|
1871
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
1872
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
1873
|
+
setIsOpen(false)
|
|
1874
|
+
setSearchQuery("")
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
document.addEventListener("mousedown", handleClickOutside)
|
|
1879
|
+
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
1880
|
+
}, [])
|
|
1881
|
+
|
|
1882
|
+
// Handle keyboard navigation
|
|
1883
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
1884
|
+
if (e.key === "Escape") {
|
|
1885
|
+
setIsOpen(false)
|
|
1820
1886
|
setSearchQuery("")
|
|
1887
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
1888
|
+
if (!isOpen) {
|
|
1889
|
+
e.preventDefault()
|
|
1890
|
+
setIsOpen(true)
|
|
1891
|
+
}
|
|
1821
1892
|
}
|
|
1822
1893
|
}
|
|
1823
1894
|
|
|
1824
1895
|
return (
|
|
1825
|
-
<div
|
|
1896
|
+
<div
|
|
1897
|
+
ref={containerRef}
|
|
1898
|
+
className={cn("flex flex-col gap-1 relative", wrapperClassName)}
|
|
1899
|
+
>
|
|
1826
1900
|
{/* Label */}
|
|
1827
1901
|
{label && (
|
|
1828
1902
|
<label
|
|
@@ -1834,87 +1908,160 @@ const SelectField = React.forwardRef<HTMLButtonElement, SelectFieldProps>(
|
|
|
1834
1908
|
</label>
|
|
1835
1909
|
)}
|
|
1836
1910
|
|
|
1837
|
-
{/*
|
|
1838
|
-
<
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1911
|
+
{/* Trigger */}
|
|
1912
|
+
<button
|
|
1913
|
+
ref={ref}
|
|
1914
|
+
id={selectId}
|
|
1915
|
+
type="button"
|
|
1916
|
+
role="combobox"
|
|
1917
|
+
aria-expanded={isOpen}
|
|
1918
|
+
aria-haspopup="listbox"
|
|
1919
|
+
aria-invalid={!!error}
|
|
1920
|
+
aria-describedby={ariaDescribedBy}
|
|
1842
1921
|
disabled={disabled || loading}
|
|
1843
|
-
|
|
1844
|
-
|
|
1922
|
+
onClick={() => !disabled && !loading && setIsOpen(!isOpen)}
|
|
1923
|
+
onKeyDown={handleKeyDown}
|
|
1924
|
+
className={cn(
|
|
1925
|
+
multiSelectTriggerVariants({ state: derivedState }),
|
|
1926
|
+
"text-left gap-2",
|
|
1927
|
+
triggerClassName
|
|
1928
|
+
)}
|
|
1845
1929
|
>
|
|
1846
|
-
<
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1930
|
+
<div className="flex-1 flex flex-wrap gap-1">
|
|
1931
|
+
{selectedValues.length === 0 ? (
|
|
1932
|
+
<span className="text-[#9CA3AF]">{placeholder}</span>
|
|
1933
|
+
) : (
|
|
1934
|
+
selectedLabels.map((label, index) => (
|
|
1935
|
+
<span
|
|
1936
|
+
key={selectedValues[index]}
|
|
1937
|
+
className="inline-flex items-center gap-1 bg-[#F3F4F6] text-[#333333] text-xs px-2 py-0.5 rounded"
|
|
1938
|
+
>
|
|
1939
|
+
{label}
|
|
1940
|
+
<span
|
|
1941
|
+
role="button"
|
|
1942
|
+
tabIndex={0}
|
|
1943
|
+
onClick={(e) => removeValue(selectedValues[index], e)}
|
|
1944
|
+
onKeyDown={(e) => {
|
|
1945
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1946
|
+
e.preventDefault()
|
|
1947
|
+
removeValue(selectedValues[index], e as unknown as React.MouseEvent)
|
|
1948
|
+
}
|
|
1949
|
+
}}
|
|
1950
|
+
className="cursor-pointer hover:text-[#FF3B3B] focus:outline-none"
|
|
1951
|
+
aria-label={\`Remove \${label}\`}
|
|
1952
|
+
>
|
|
1953
|
+
<X className="size-3" />
|
|
1954
|
+
</span>
|
|
1955
|
+
</span>
|
|
1956
|
+
))
|
|
1957
|
+
)}
|
|
1958
|
+
</div>
|
|
1959
|
+
<div className="flex items-center gap-1">
|
|
1960
|
+
{selectedValues.length > 0 && (
|
|
1961
|
+
<span
|
|
1962
|
+
role="button"
|
|
1963
|
+
tabIndex={0}
|
|
1964
|
+
onClick={clearAll}
|
|
1965
|
+
onKeyDown={(e) => {
|
|
1966
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1967
|
+
e.preventDefault()
|
|
1968
|
+
clearAll(e as unknown as React.MouseEvent)
|
|
1969
|
+
}
|
|
1970
|
+
}}
|
|
1971
|
+
className="p-0.5 cursor-pointer hover:text-[#FF3B3B] focus:outline-none"
|
|
1972
|
+
aria-label="Clear all"
|
|
1973
|
+
>
|
|
1974
|
+
<X className="size-4 text-[#6B7280]" />
|
|
1975
|
+
</span>
|
|
1976
|
+
)}
|
|
1977
|
+
{loading ? (
|
|
1978
|
+
<Loader2 className="size-4 animate-spin text-[#6B7280]" />
|
|
1979
|
+
) : (
|
|
1980
|
+
<ChevronDown
|
|
1981
|
+
className={cn(
|
|
1982
|
+
"size-4 text-[#6B7280] transition-transform",
|
|
1983
|
+
isOpen && "rotate-180"
|
|
1984
|
+
)}
|
|
1985
|
+
/>
|
|
1986
|
+
)}
|
|
1987
|
+
</div>
|
|
1988
|
+
</button>
|
|
1989
|
+
|
|
1990
|
+
{/* Dropdown */}
|
|
1991
|
+
{isOpen && (
|
|
1992
|
+
<div
|
|
1850
1993
|
className={cn(
|
|
1851
|
-
|
|
1852
|
-
|
|
1994
|
+
"absolute z-50 mt-1 w-full rounded bg-white border border-[#E9E9E9] shadow-md",
|
|
1995
|
+
"top-full"
|
|
1853
1996
|
)}
|
|
1854
|
-
|
|
1855
|
-
aria-
|
|
1997
|
+
role="listbox"
|
|
1998
|
+
aria-multiselectable="true"
|
|
1856
1999
|
>
|
|
1857
|
-
<SelectValue placeholder={placeholder} />
|
|
1858
|
-
{loading && (
|
|
1859
|
-
<Loader2 className="absolute right-8 size-4 animate-spin text-[#6B7280]" />
|
|
1860
|
-
)}
|
|
1861
|
-
</SelectTrigger>
|
|
1862
|
-
<SelectContent>
|
|
1863
2000
|
{/* Search input */}
|
|
1864
2001
|
{searchable && (
|
|
1865
|
-
<div className="
|
|
1866
|
-
<input
|
|
1867
|
-
type="text"
|
|
1868
|
-
placeholder={searchPlaceholder}
|
|
1869
|
-
value={searchQuery}
|
|
1870
|
-
onChange={
|
|
1871
|
-
className="w-full h-8 px-3 text-sm border border-[#E9E9E9] rounded bg-white placeholder:text-[#9CA3AF] focus:outline-none focus:border-[#2BBBC9]/50"
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
</div>
|
|
1877
|
-
)}
|
|
1878
|
-
|
|
1879
|
-
{/* Ungrouped options */}
|
|
1880
|
-
{groupedOptions.ungrouped.map((option) => (
|
|
1881
|
-
<SelectItem
|
|
1882
|
-
key={option.value}
|
|
1883
|
-
value={option.value}
|
|
1884
|
-
disabled={option.disabled}
|
|
1885
|
-
>
|
|
1886
|
-
{option.label}
|
|
1887
|
-
</SelectItem>
|
|
1888
|
-
))}
|
|
1889
|
-
|
|
1890
|
-
{/* Grouped options */}
|
|
1891
|
-
{hasGroups &&
|
|
1892
|
-
Object.entries(groupedOptions.groups).map(([groupName, groupOptions]) => (
|
|
1893
|
-
<SelectGroup key={groupName}>
|
|
1894
|
-
<SelectLabel>{groupName}</SelectLabel>
|
|
1895
|
-
{groupOptions.map((option) => (
|
|
1896
|
-
<SelectItem
|
|
1897
|
-
key={option.value}
|
|
1898
|
-
value={option.value}
|
|
1899
|
-
disabled={option.disabled}
|
|
1900
|
-
>
|
|
1901
|
-
{option.label}
|
|
1902
|
-
</SelectItem>
|
|
1903
|
-
))}
|
|
1904
|
-
</SelectGroup>
|
|
1905
|
-
))}
|
|
2002
|
+
<div className="p-2 border-b border-[#E9E9E9]">
|
|
2003
|
+
<input
|
|
2004
|
+
type="text"
|
|
2005
|
+
placeholder={searchPlaceholder}
|
|
2006
|
+
value={searchQuery}
|
|
2007
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
2008
|
+
className="w-full h-8 px-3 text-sm border border-[#E9E9E9] rounded bg-white placeholder:text-[#9CA3AF] focus:outline-none focus:border-[#2BBBC9]/50"
|
|
2009
|
+
onClick={(e) => e.stopPropagation()}
|
|
2010
|
+
/>
|
|
2011
|
+
</div>
|
|
2012
|
+
)}
|
|
1906
2013
|
|
|
1907
|
-
{/*
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
groupedOptions.ungrouped.length === 0 &&
|
|
1911
|
-
Object.keys(groupedOptions.groups).length === 0 && (
|
|
2014
|
+
{/* Options */}
|
|
2015
|
+
<div className="max-h-60 overflow-auto p-1">
|
|
2016
|
+
{filteredOptions.length === 0 ? (
|
|
1912
2017
|
<div className="py-6 text-center text-sm text-[#6B7280]">
|
|
1913
2018
|
No results found
|
|
1914
2019
|
</div>
|
|
2020
|
+
) : (
|
|
2021
|
+
filteredOptions.map((option) => {
|
|
2022
|
+
const isSelected = selectedValues.includes(option.value)
|
|
2023
|
+
const isDisabled =
|
|
2024
|
+
option.disabled ||
|
|
2025
|
+
(!isSelected && maxSelections && selectedValues.length >= maxSelections)
|
|
2026
|
+
|
|
2027
|
+
return (
|
|
2028
|
+
<button
|
|
2029
|
+
key={option.value}
|
|
2030
|
+
type="button"
|
|
2031
|
+
role="option"
|
|
2032
|
+
aria-selected={isSelected}
|
|
2033
|
+
disabled={isDisabled}
|
|
2034
|
+
onClick={() => !isDisabled && toggleOption(option.value)}
|
|
2035
|
+
className={cn(
|
|
2036
|
+
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-2 pl-4 pr-8 text-sm text-[#333333] outline-none",
|
|
2037
|
+
"hover:bg-[#F3F4F6] focus:bg-[#F3F4F6]",
|
|
2038
|
+
isSelected && "bg-[#F3F4F6]",
|
|
2039
|
+
isDisabled && "pointer-events-none opacity-50"
|
|
2040
|
+
)}
|
|
2041
|
+
>
|
|
2042
|
+
<span className="absolute right-2 flex size-4 items-center justify-center">
|
|
2043
|
+
{isSelected && <Check className="size-4 text-[#2BBBC9]" />}
|
|
2044
|
+
</span>
|
|
2045
|
+
{option.label}
|
|
2046
|
+
</button>
|
|
2047
|
+
)
|
|
2048
|
+
})
|
|
1915
2049
|
)}
|
|
1916
|
-
|
|
1917
|
-
|
|
2050
|
+
</div>
|
|
2051
|
+
|
|
2052
|
+
{/* Footer with count */}
|
|
2053
|
+
{maxSelections && (
|
|
2054
|
+
<div className="p-2 border-t border-[#E9E9E9] text-xs text-[#6B7280]">
|
|
2055
|
+
{selectedValues.length} / {maxSelections} selected
|
|
2056
|
+
</div>
|
|
2057
|
+
)}
|
|
2058
|
+
</div>
|
|
2059
|
+
)}
|
|
2060
|
+
|
|
2061
|
+
{/* Hidden input for form submission */}
|
|
2062
|
+
{name && selectedValues.map((v) => (
|
|
2063
|
+
<input key={v} type="hidden" name={name} value={v} />
|
|
2064
|
+
))}
|
|
1918
2065
|
|
|
1919
2066
|
{/* Helper text / Error message */}
|
|
1920
2067
|
{(error || helperText) && (
|
|
@@ -1934,210 +2081,9 @@ const SelectField = React.forwardRef<HTMLButtonElement, SelectFieldProps>(
|
|
|
1934
2081
|
)
|
|
1935
2082
|
}
|
|
1936
2083
|
)
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
export { SelectField }
|
|
1940
|
-
`, prefix)
|
|
1941
|
-
}
|
|
1942
|
-
]
|
|
1943
|
-
},
|
|
1944
|
-
"select": {
|
|
1945
|
-
name: "select",
|
|
1946
|
-
description: "A select dropdown component built on Radix UI Select",
|
|
1947
|
-
dependencies: [
|
|
1948
|
-
"@radix-ui/react-select",
|
|
1949
|
-
"class-variance-authority",
|
|
1950
|
-
"clsx",
|
|
1951
|
-
"tailwind-merge",
|
|
1952
|
-
"lucide-react"
|
|
1953
|
-
],
|
|
1954
|
-
files: [
|
|
1955
|
-
{
|
|
1956
|
-
name: "select.tsx",
|
|
1957
|
-
content: prefixTailwindClasses(`import * as React from "react"
|
|
1958
|
-
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
1959
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
1960
|
-
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
1961
|
-
|
|
1962
|
-
import { cn } from "../../lib/utils"
|
|
1963
|
-
|
|
1964
|
-
/**
|
|
1965
|
-
* SelectTrigger variants matching TextField styling
|
|
1966
|
-
*/
|
|
1967
|
-
const selectTriggerVariants = cva(
|
|
1968
|
-
"flex h-10 w-full items-center justify-between rounded bg-white px-4 py-2.5 text-sm text-[#333333] transition-all disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[#F9FAFB] [&>span]:line-clamp-1",
|
|
1969
|
-
{
|
|
1970
|
-
variants: {
|
|
1971
|
-
state: {
|
|
1972
|
-
default: "border border-[#E9E9E9] focus:outline-none focus:border-[#2BBBC9]/50 focus:shadow-[0_0_0_1px_rgba(43,187,201,0.15)]",
|
|
1973
|
-
error: "border border-[#FF3B3B]/40 focus:outline-none focus:border-[#FF3B3B]/60 focus:shadow-[0_0_0_1px_rgba(255,59,59,0.1)]",
|
|
1974
|
-
},
|
|
1975
|
-
},
|
|
1976
|
-
defaultVariants: {
|
|
1977
|
-
state: "default",
|
|
1978
|
-
},
|
|
1979
|
-
}
|
|
1980
|
-
)
|
|
1981
|
-
|
|
1982
|
-
const Select = SelectPrimitive.Root
|
|
1983
|
-
|
|
1984
|
-
const SelectGroup = SelectPrimitive.Group
|
|
1985
|
-
|
|
1986
|
-
const SelectValue = SelectPrimitive.Value
|
|
1987
|
-
|
|
1988
|
-
export interface SelectTriggerProps
|
|
1989
|
-
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>,
|
|
1990
|
-
VariantProps<typeof selectTriggerVariants> {}
|
|
1991
|
-
|
|
1992
|
-
const SelectTrigger = React.forwardRef<
|
|
1993
|
-
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
1994
|
-
SelectTriggerProps
|
|
1995
|
-
>(({ className, state, children, ...props }, ref) => (
|
|
1996
|
-
<SelectPrimitive.Trigger
|
|
1997
|
-
ref={ref}
|
|
1998
|
-
className={cn(selectTriggerVariants({ state, className }))}
|
|
1999
|
-
{...props}
|
|
2000
|
-
>
|
|
2001
|
-
{children}
|
|
2002
|
-
<SelectPrimitive.Icon asChild>
|
|
2003
|
-
<ChevronDown className="size-4 text-[#6B7280] opacity-70" />
|
|
2004
|
-
</SelectPrimitive.Icon>
|
|
2005
|
-
</SelectPrimitive.Trigger>
|
|
2006
|
-
))
|
|
2007
|
-
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
2008
|
-
|
|
2009
|
-
const SelectScrollUpButton = React.forwardRef<
|
|
2010
|
-
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
2011
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
2012
|
-
>(({ className, ...props }, ref) => (
|
|
2013
|
-
<SelectPrimitive.ScrollUpButton
|
|
2014
|
-
ref={ref}
|
|
2015
|
-
className={cn(
|
|
2016
|
-
"flex cursor-default items-center justify-center py-1",
|
|
2017
|
-
className
|
|
2018
|
-
)}
|
|
2019
|
-
{...props}
|
|
2020
|
-
>
|
|
2021
|
-
<ChevronUp className="size-4 text-[#6B7280]" />
|
|
2022
|
-
</SelectPrimitive.ScrollUpButton>
|
|
2023
|
-
))
|
|
2024
|
-
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
2025
|
-
|
|
2026
|
-
const SelectScrollDownButton = React.forwardRef<
|
|
2027
|
-
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
2028
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
2029
|
-
>(({ className, ...props }, ref) => (
|
|
2030
|
-
<SelectPrimitive.ScrollDownButton
|
|
2031
|
-
ref={ref}
|
|
2032
|
-
className={cn(
|
|
2033
|
-
"flex cursor-default items-center justify-center py-1",
|
|
2034
|
-
className
|
|
2035
|
-
)}
|
|
2036
|
-
{...props}
|
|
2037
|
-
>
|
|
2038
|
-
<ChevronDown className="size-4 text-[#6B7280]" />
|
|
2039
|
-
</SelectPrimitive.ScrollDownButton>
|
|
2040
|
-
))
|
|
2041
|
-
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
|
2042
|
-
|
|
2043
|
-
const SelectContent = React.forwardRef<
|
|
2044
|
-
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
2045
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
2046
|
-
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
2047
|
-
<SelectPrimitive.Portal>
|
|
2048
|
-
<SelectPrimitive.Content
|
|
2049
|
-
ref={ref}
|
|
2050
|
-
className={cn(
|
|
2051
|
-
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded bg-white border border-[#E9E9E9] shadow-md",
|
|
2052
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
2053
|
-
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
2054
|
-
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
2055
|
-
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
2056
|
-
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
2057
|
-
position === "popper" &&
|
|
2058
|
-
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
2059
|
-
className
|
|
2060
|
-
)}
|
|
2061
|
-
position={position}
|
|
2062
|
-
{...props}
|
|
2063
|
-
>
|
|
2064
|
-
<SelectScrollUpButton />
|
|
2065
|
-
<SelectPrimitive.Viewport
|
|
2066
|
-
className={cn(
|
|
2067
|
-
"p-1",
|
|
2068
|
-
position === "popper" &&
|
|
2069
|
-
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
2070
|
-
)}
|
|
2071
|
-
>
|
|
2072
|
-
{children}
|
|
2073
|
-
</SelectPrimitive.Viewport>
|
|
2074
|
-
<SelectScrollDownButton />
|
|
2075
|
-
</SelectPrimitive.Content>
|
|
2076
|
-
</SelectPrimitive.Portal>
|
|
2077
|
-
))
|
|
2078
|
-
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
2079
|
-
|
|
2080
|
-
const SelectLabel = React.forwardRef<
|
|
2081
|
-
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
2082
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
2083
|
-
>(({ className, ...props }, ref) => (
|
|
2084
|
-
<SelectPrimitive.Label
|
|
2085
|
-
ref={ref}
|
|
2086
|
-
className={cn("px-4 py-1.5 text-xs font-medium text-[#6B7280]", className)}
|
|
2087
|
-
{...props}
|
|
2088
|
-
/>
|
|
2089
|
-
))
|
|
2090
|
-
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
2091
|
-
|
|
2092
|
-
const SelectItem = React.forwardRef<
|
|
2093
|
-
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
2094
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
2095
|
-
>(({ className, children, ...props }, ref) => (
|
|
2096
|
-
<SelectPrimitive.Item
|
|
2097
|
-
ref={ref}
|
|
2098
|
-
className={cn(
|
|
2099
|
-
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-2 pl-4 pr-8 text-sm text-[#333333] outline-none",
|
|
2100
|
-
"hover:bg-[#F3F4F6] focus:bg-[#F3F4F6]",
|
|
2101
|
-
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
2102
|
-
className
|
|
2103
|
-
)}
|
|
2104
|
-
{...props}
|
|
2105
|
-
>
|
|
2106
|
-
<span className="absolute right-2 flex size-4 items-center justify-center">
|
|
2107
|
-
<SelectPrimitive.ItemIndicator>
|
|
2108
|
-
<Check className="size-4 text-[#2BBBC9]" />
|
|
2109
|
-
</SelectPrimitive.ItemIndicator>
|
|
2110
|
-
</span>
|
|
2111
|
-
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
2112
|
-
</SelectPrimitive.Item>
|
|
2113
|
-
))
|
|
2114
|
-
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
2115
|
-
|
|
2116
|
-
const SelectSeparator = React.forwardRef<
|
|
2117
|
-
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
2118
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
2119
|
-
>(({ className, ...props }, ref) => (
|
|
2120
|
-
<SelectPrimitive.Separator
|
|
2121
|
-
ref={ref}
|
|
2122
|
-
className={cn("-mx-1 my-1 h-px bg-[#E9E9E9]", className)}
|
|
2123
|
-
{...props}
|
|
2124
|
-
/>
|
|
2125
|
-
))
|
|
2126
|
-
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
2084
|
+
MultiSelect.displayName = "MultiSelect"
|
|
2127
2085
|
|
|
2128
|
-
export {
|
|
2129
|
-
Select,
|
|
2130
|
-
SelectGroup,
|
|
2131
|
-
SelectValue,
|
|
2132
|
-
SelectTrigger,
|
|
2133
|
-
SelectContent,
|
|
2134
|
-
SelectLabel,
|
|
2135
|
-
SelectItem,
|
|
2136
|
-
SelectSeparator,
|
|
2137
|
-
SelectScrollUpButton,
|
|
2138
|
-
SelectScrollDownButton,
|
|
2139
|
-
selectTriggerVariants,
|
|
2140
|
-
}
|
|
2086
|
+
export { MultiSelect, multiSelectTriggerVariants }
|
|
2141
2087
|
`, prefix)
|
|
2142
2088
|
}
|
|
2143
2089
|
]
|
|
@@ -2454,576 +2400,710 @@ export {
|
|
|
2454
2400
|
}
|
|
2455
2401
|
]
|
|
2456
2402
|
},
|
|
2457
|
-
"
|
|
2458
|
-
name: "
|
|
2459
|
-
description: "A
|
|
2403
|
+
"dropdown-menu": {
|
|
2404
|
+
name: "dropdown-menu",
|
|
2405
|
+
description: "A dropdown menu component for displaying actions and options",
|
|
2460
2406
|
dependencies: [
|
|
2461
|
-
"
|
|
2407
|
+
"@radix-ui/react-dropdown-menu",
|
|
2462
2408
|
"clsx",
|
|
2463
|
-
"tailwind-merge"
|
|
2409
|
+
"tailwind-merge",
|
|
2410
|
+
"lucide-react"
|
|
2464
2411
|
],
|
|
2465
2412
|
files: [
|
|
2466
2413
|
{
|
|
2467
|
-
name: "
|
|
2414
|
+
name: "dropdown-menu.tsx",
|
|
2468
2415
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
2469
|
-
import
|
|
2416
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
2417
|
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
2470
2418
|
|
|
2471
2419
|
import { cn } from "../../lib/utils"
|
|
2472
2420
|
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
const
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
size: {
|
|
2490
|
-
default: "px-2 py-1",
|
|
2491
|
-
sm: "px-1.5 py-0.5 text-xs",
|
|
2492
|
-
lg: "px-3 py-1.5",
|
|
2493
|
-
},
|
|
2494
|
-
},
|
|
2495
|
-
defaultVariants: {
|
|
2496
|
-
variant: "default",
|
|
2497
|
-
size: "default",
|
|
2498
|
-
},
|
|
2421
|
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
2422
|
+
|
|
2423
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
2424
|
+
|
|
2425
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
2426
|
+
|
|
2427
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
2428
|
+
|
|
2429
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
2430
|
+
|
|
2431
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
2432
|
+
|
|
2433
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
2434
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
2435
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
2436
|
+
inset?: boolean
|
|
2499
2437
|
}
|
|
2500
|
-
)
|
|
2438
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
2439
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
2440
|
+
ref={ref}
|
|
2441
|
+
className={cn(
|
|
2442
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[#F3F4F6] data-[state=open]:bg-[#F3F4F6]",
|
|
2443
|
+
inset && "pl-8",
|
|
2444
|
+
className
|
|
2445
|
+
)}
|
|
2446
|
+
{...props}
|
|
2447
|
+
>
|
|
2448
|
+
{children}
|
|
2449
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
2450
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
2451
|
+
))
|
|
2452
|
+
DropdownMenuSubTrigger.displayName =
|
|
2453
|
+
DropdownMenuPrimitive.SubTrigger.displayName
|
|
2501
2454
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2455
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
2456
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
2457
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
2458
|
+
>(({ className, ...props }, ref) => (
|
|
2459
|
+
<DropdownMenuPrimitive.SubContent
|
|
2460
|
+
ref={ref}
|
|
2461
|
+
className={cn(
|
|
2462
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-[#E5E7EB] bg-white p-1 text-[#333333] shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
2463
|
+
className
|
|
2464
|
+
)}
|
|
2465
|
+
{...props}
|
|
2466
|
+
/>
|
|
2467
|
+
))
|
|
2468
|
+
DropdownMenuSubContent.displayName =
|
|
2469
|
+
DropdownMenuPrimitive.SubContent.displayName
|
|
2517
2470
|
|
|
2518
|
-
const
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2471
|
+
const DropdownMenuContent = React.forwardRef<
|
|
2472
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
2473
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
2474
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
2475
|
+
<DropdownMenuPrimitive.Portal>
|
|
2476
|
+
<DropdownMenuPrimitive.Content
|
|
2477
|
+
ref={ref}
|
|
2478
|
+
sideOffset={sideOffset}
|
|
2479
|
+
className={cn(
|
|
2480
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-[#E5E7EB] bg-white p-1 text-[#333333] shadow-md",
|
|
2481
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
2482
|
+
className
|
|
2483
|
+
)}
|
|
2484
|
+
{...props}
|
|
2485
|
+
/>
|
|
2486
|
+
</DropdownMenuPrimitive.Portal>
|
|
2487
|
+
))
|
|
2488
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
2489
|
+
|
|
2490
|
+
const DropdownMenuItem = React.forwardRef<
|
|
2491
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
2492
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
2493
|
+
inset?: boolean
|
|
2532
2494
|
}
|
|
2533
|
-
)
|
|
2534
|
-
|
|
2495
|
+
>(({ className, inset, ...props }, ref) => (
|
|
2496
|
+
<DropdownMenuPrimitive.Item
|
|
2497
|
+
ref={ref}
|
|
2498
|
+
className={cn(
|
|
2499
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[#F3F4F6] focus:text-[#333333] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
2500
|
+
inset && "pl-8",
|
|
2501
|
+
className
|
|
2502
|
+
)}
|
|
2503
|
+
{...props}
|
|
2504
|
+
/>
|
|
2505
|
+
))
|
|
2506
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
2535
2507
|
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2508
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
2509
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
2510
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
2511
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
2512
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
2513
|
+
ref={ref}
|
|
2514
|
+
className={cn(
|
|
2515
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[#F3F4F6] focus:text-[#333333] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
2516
|
+
className
|
|
2517
|
+
)}
|
|
2518
|
+
checked={checked}
|
|
2519
|
+
{...props}
|
|
2520
|
+
>
|
|
2521
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
2522
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
2523
|
+
<Check className="h-4 w-4" />
|
|
2524
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
2525
|
+
</span>
|
|
2526
|
+
{children}
|
|
2527
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
2528
|
+
))
|
|
2529
|
+
DropdownMenuCheckboxItem.displayName =
|
|
2530
|
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
2531
|
+
|
|
2532
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
2533
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
2534
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
2535
|
+
>(({ className, children, ...props }, ref) => (
|
|
2536
|
+
<DropdownMenuPrimitive.RadioItem
|
|
2537
|
+
ref={ref}
|
|
2538
|
+
className={cn(
|
|
2539
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[#F3F4F6] focus:text-[#333333] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
2540
|
+
className
|
|
2541
|
+
)}
|
|
2542
|
+
{...props}
|
|
2543
|
+
>
|
|
2544
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
2545
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
2546
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
2547
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
2548
|
+
</span>
|
|
2549
|
+
{children}
|
|
2550
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
2551
|
+
))
|
|
2552
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
2553
|
+
|
|
2554
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
2555
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
2556
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
2557
|
+
inset?: boolean
|
|
2558
|
+
}
|
|
2559
|
+
>(({ className, inset, ...props }, ref) => (
|
|
2560
|
+
<DropdownMenuPrimitive.Label
|
|
2561
|
+
ref={ref}
|
|
2562
|
+
className={cn(
|
|
2563
|
+
"px-2 py-1.5 text-sm font-semibold",
|
|
2564
|
+
inset && "pl-8",
|
|
2565
|
+
className
|
|
2566
|
+
)}
|
|
2567
|
+
{...props}
|
|
2568
|
+
/>
|
|
2569
|
+
))
|
|
2570
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
2563
2571
|
|
|
2564
|
-
const
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2572
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
2573
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
2574
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
2575
|
+
>(({ className, ...props }, ref) => (
|
|
2576
|
+
<DropdownMenuPrimitive.Separator
|
|
2577
|
+
ref={ref}
|
|
2578
|
+
className={cn("-mx-1 my-1 h-px bg-[#E5E7EB]", className)}
|
|
2579
|
+
{...props}
|
|
2580
|
+
/>
|
|
2581
|
+
))
|
|
2582
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
2573
2583
|
|
|
2584
|
+
const DropdownMenuShortcut = ({
|
|
2585
|
+
className,
|
|
2586
|
+
...props
|
|
2587
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
2574
2588
|
return (
|
|
2575
|
-
<
|
|
2576
|
-
{
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
if (isLastVisible) {
|
|
2580
|
-
return (
|
|
2581
|
-
<div key={index} className="flex items-center gap-2">
|
|
2582
|
-
<Tag label={tag.label} variant={variant} size={size}>
|
|
2583
|
-
{tag.value}
|
|
2584
|
-
</Tag>
|
|
2585
|
-
<Tag variant={variant} size={size}>
|
|
2586
|
-
+{overflowCount} more
|
|
2587
|
-
</Tag>
|
|
2588
|
-
</div>
|
|
2589
|
-
)
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
return (
|
|
2593
|
-
<Tag key={index} label={tag.label} variant={variant} size={size}>
|
|
2594
|
-
{tag.value}
|
|
2595
|
-
</Tag>
|
|
2596
|
-
)
|
|
2597
|
-
})}
|
|
2598
|
-
</div>
|
|
2589
|
+
<span
|
|
2590
|
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
2591
|
+
{...props}
|
|
2592
|
+
/>
|
|
2599
2593
|
)
|
|
2600
2594
|
}
|
|
2601
|
-
|
|
2595
|
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
2602
2596
|
|
|
2603
|
-
export {
|
|
2597
|
+
export {
|
|
2598
|
+
DropdownMenu,
|
|
2599
|
+
DropdownMenuTrigger,
|
|
2600
|
+
DropdownMenuContent,
|
|
2601
|
+
DropdownMenuItem,
|
|
2602
|
+
DropdownMenuCheckboxItem,
|
|
2603
|
+
DropdownMenuRadioItem,
|
|
2604
|
+
DropdownMenuLabel,
|
|
2605
|
+
DropdownMenuSeparator,
|
|
2606
|
+
DropdownMenuShortcut,
|
|
2607
|
+
DropdownMenuGroup,
|
|
2608
|
+
DropdownMenuPortal,
|
|
2609
|
+
DropdownMenuSub,
|
|
2610
|
+
DropdownMenuSubContent,
|
|
2611
|
+
DropdownMenuSubTrigger,
|
|
2612
|
+
DropdownMenuRadioGroup,
|
|
2613
|
+
}
|
|
2604
2614
|
`, prefix)
|
|
2605
2615
|
}
|
|
2606
2616
|
]
|
|
2607
2617
|
},
|
|
2608
|
-
"
|
|
2609
|
-
name: "
|
|
2610
|
-
description: "A
|
|
2618
|
+
"tag": {
|
|
2619
|
+
name: "tag",
|
|
2620
|
+
description: "A tag component for event labels with optional bold label prefix",
|
|
2611
2621
|
dependencies: [
|
|
2612
2622
|
"class-variance-authority",
|
|
2613
2623
|
"clsx",
|
|
2614
|
-
"tailwind-merge"
|
|
2615
|
-
"lucide-react"
|
|
2624
|
+
"tailwind-merge"
|
|
2616
2625
|
],
|
|
2617
2626
|
files: [
|
|
2618
2627
|
{
|
|
2619
|
-
name: "
|
|
2628
|
+
name: "tag.tsx",
|
|
2620
2629
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
2621
2630
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
2622
|
-
import { Loader2 } from "lucide-react"
|
|
2623
2631
|
|
|
2624
2632
|
import { cn } from "../../lib/utils"
|
|
2625
2633
|
|
|
2626
2634
|
/**
|
|
2627
|
-
*
|
|
2635
|
+
* Tag variants for event labels and categories.
|
|
2636
|
+
* Rounded rectangle tags with optional bold labels.
|
|
2628
2637
|
*/
|
|
2629
|
-
const
|
|
2630
|
-
"
|
|
2638
|
+
const tagVariants = cva(
|
|
2639
|
+
"inline-flex items-center rounded text-sm",
|
|
2631
2640
|
{
|
|
2632
2641
|
variants: {
|
|
2633
|
-
|
|
2634
|
-
default: "
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2642
|
+
variant: {
|
|
2643
|
+
default: "bg-[#F3F4F6] text-[#333333]",
|
|
2644
|
+
primary: "bg-[#343E55]/10 text-[#343E55]",
|
|
2645
|
+
secondary: "bg-[#E5E7EB] text-[#374151]",
|
|
2646
|
+
success: "bg-[#E5FFF5] text-[#00A651]",
|
|
2647
|
+
warning: "bg-[#FFF8E5] text-[#F59E0B]",
|
|
2648
|
+
error: "bg-[#FFECEC] text-[#FF3B3B]",
|
|
2640
2649
|
},
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
},
|
|
2646
|
-
}
|
|
2647
|
-
)
|
|
2648
|
-
|
|
2649
|
-
/**
|
|
2650
|
-
* TextField input variants (standalone without container)
|
|
2651
|
-
*/
|
|
2652
|
-
const textFieldInputVariants = cva(
|
|
2653
|
-
"h-10 w-full rounded bg-white px-4 py-2.5 text-sm text-[#333333] transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-[#333333] placeholder:text-[#9CA3AF] disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[#F9FAFB]",
|
|
2654
|
-
{
|
|
2655
|
-
variants: {
|
|
2656
|
-
state: {
|
|
2657
|
-
default: "border border-[#E9E9E9] focus:outline-none focus:border-[#2BBBC9]/50 focus:shadow-[0_0_0_1px_rgba(43,187,201,0.15)]",
|
|
2658
|
-
error: "border border-[#FF3B3B]/40 focus:outline-none focus:border-[#FF3B3B]/60 focus:shadow-[0_0_0_1px_rgba(255,59,59,0.1)]",
|
|
2650
|
+
size: {
|
|
2651
|
+
default: "px-2 py-1",
|
|
2652
|
+
sm: "px-1.5 py-0.5 text-xs",
|
|
2653
|
+
lg: "px-3 py-1.5",
|
|
2659
2654
|
},
|
|
2660
2655
|
},
|
|
2661
2656
|
defaultVariants: {
|
|
2662
|
-
|
|
2657
|
+
variant: "default",
|
|
2658
|
+
size: "default",
|
|
2663
2659
|
},
|
|
2664
2660
|
}
|
|
2665
2661
|
)
|
|
2666
2662
|
|
|
2667
2663
|
/**
|
|
2668
|
-
*
|
|
2664
|
+
* Tag component for displaying event labels and categories.
|
|
2669
2665
|
*
|
|
2670
2666
|
* @example
|
|
2671
2667
|
* \`\`\`tsx
|
|
2672
|
-
* <
|
|
2673
|
-
* <
|
|
2674
|
-
* <TextField label="Website" prefix="https://" suffix=".com" />
|
|
2668
|
+
* <Tag>After Call Event</Tag>
|
|
2669
|
+
* <Tag label="In Call Event:">Start of call, Bridge, Call ended</Tag>
|
|
2675
2670
|
* \`\`\`
|
|
2676
2671
|
*/
|
|
2677
|
-
export interface
|
|
2678
|
-
extends
|
|
2679
|
-
VariantProps<typeof
|
|
2680
|
-
/**
|
|
2672
|
+
export interface TagProps
|
|
2673
|
+
extends React.HTMLAttributes<HTMLSpanElement>,
|
|
2674
|
+
VariantProps<typeof tagVariants> {
|
|
2675
|
+
/** Bold label prefix displayed before the content */
|
|
2681
2676
|
label?: string
|
|
2682
|
-
|
|
2683
|
-
required?: boolean
|
|
2684
|
-
/** Helper text displayed below the input */
|
|
2685
|
-
helperText?: string
|
|
2686
|
-
/** Error message - shows error state with red styling */
|
|
2687
|
-
error?: string
|
|
2688
|
-
/** Icon displayed on the left inside the input */
|
|
2689
|
-
leftIcon?: React.ReactNode
|
|
2690
|
-
/** Icon displayed on the right inside the input */
|
|
2691
|
-
rightIcon?: React.ReactNode
|
|
2692
|
-
/** Text prefix inside input (e.g., "https://") */
|
|
2693
|
-
prefix?: string
|
|
2694
|
-
/** Text suffix inside input (e.g., ".com") */
|
|
2695
|
-
suffix?: string
|
|
2696
|
-
/** Shows character count when maxLength is set */
|
|
2697
|
-
showCount?: boolean
|
|
2698
|
-
/** Shows loading spinner inside input */
|
|
2699
|
-
loading?: boolean
|
|
2700
|
-
/** Additional class for the wrapper container */
|
|
2701
|
-
wrapperClassName?: string
|
|
2702
|
-
/** Additional class for the label */
|
|
2703
|
-
labelClassName?: string
|
|
2704
|
-
/** Additional class for the input container (includes prefix/suffix/icons) */
|
|
2705
|
-
inputContainerClassName?: string
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|
2709
|
-
(
|
|
2710
|
-
{
|
|
2711
|
-
className,
|
|
2712
|
-
wrapperClassName,
|
|
2713
|
-
labelClassName,
|
|
2714
|
-
inputContainerClassName,
|
|
2715
|
-
state,
|
|
2716
|
-
label,
|
|
2717
|
-
required,
|
|
2718
|
-
helperText,
|
|
2719
|
-
error,
|
|
2720
|
-
leftIcon,
|
|
2721
|
-
rightIcon,
|
|
2722
|
-
prefix,
|
|
2723
|
-
suffix,
|
|
2724
|
-
showCount,
|
|
2725
|
-
loading,
|
|
2726
|
-
maxLength,
|
|
2727
|
-
value,
|
|
2728
|
-
defaultValue,
|
|
2729
|
-
onChange,
|
|
2730
|
-
disabled,
|
|
2731
|
-
id,
|
|
2732
|
-
...props
|
|
2733
|
-
},
|
|
2734
|
-
ref
|
|
2735
|
-
) => {
|
|
2736
|
-
// Internal state for character count in uncontrolled mode
|
|
2737
|
-
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '')
|
|
2738
|
-
|
|
2739
|
-
// Determine if controlled
|
|
2740
|
-
const isControlled = value !== undefined
|
|
2741
|
-
const currentValue = isControlled ? value : internalValue
|
|
2742
|
-
|
|
2743
|
-
// Derive state from props
|
|
2744
|
-
const derivedState = error ? 'error' : (state ?? 'default')
|
|
2745
|
-
|
|
2746
|
-
// Handle change for both controlled and uncontrolled
|
|
2747
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
2748
|
-
if (!isControlled) {
|
|
2749
|
-
setInternalValue(e.target.value)
|
|
2750
|
-
}
|
|
2751
|
-
onChange?.(e)
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
// Determine if we need the container wrapper (for icons/prefix/suffix)
|
|
2755
|
-
const hasAddons = leftIcon || rightIcon || prefix || suffix || loading
|
|
2756
|
-
|
|
2757
|
-
// Character count
|
|
2758
|
-
const charCount = String(currentValue).length
|
|
2759
|
-
|
|
2760
|
-
// Generate unique IDs for accessibility
|
|
2761
|
-
const generatedId = React.useId()
|
|
2762
|
-
const inputId = id || generatedId
|
|
2763
|
-
const helperId = \`\${inputId}-helper\`
|
|
2764
|
-
const errorId = \`\${inputId}-error\`
|
|
2765
|
-
|
|
2766
|
-
// Determine aria-describedby
|
|
2767
|
-
const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined
|
|
2768
|
-
|
|
2769
|
-
// Render the input element
|
|
2770
|
-
const inputElement = (
|
|
2771
|
-
<input
|
|
2772
|
-
ref={ref}
|
|
2773
|
-
id={inputId}
|
|
2774
|
-
className={cn(
|
|
2775
|
-
hasAddons
|
|
2776
|
-
? "flex-1 bg-transparent border-0 outline-none focus:ring-0 px-0 h-full text-sm text-[#333333] placeholder:text-[#9CA3AF] disabled:cursor-not-allowed"
|
|
2777
|
-
: textFieldInputVariants({ state: derivedState, className })
|
|
2778
|
-
)}
|
|
2779
|
-
disabled={disabled || loading}
|
|
2780
|
-
maxLength={maxLength}
|
|
2781
|
-
value={isControlled ? value : undefined}
|
|
2782
|
-
defaultValue={!isControlled ? defaultValue : undefined}
|
|
2783
|
-
onChange={handleChange}
|
|
2784
|
-
aria-invalid={!!error}
|
|
2785
|
-
aria-describedby={ariaDescribedBy}
|
|
2786
|
-
{...props}
|
|
2787
|
-
/>
|
|
2788
|
-
)
|
|
2677
|
+
}
|
|
2789
2678
|
|
|
2679
|
+
const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
|
2680
|
+
({ className, variant, size, label, children, ...props }, ref) => {
|
|
2790
2681
|
return (
|
|
2791
|
-
<
|
|
2792
|
-
{
|
|
2682
|
+
<span
|
|
2683
|
+
className={cn(tagVariants({ variant, size, className }))}
|
|
2684
|
+
ref={ref}
|
|
2685
|
+
{...props}
|
|
2686
|
+
>
|
|
2793
2687
|
{label && (
|
|
2794
|
-
<label
|
|
2795
|
-
htmlFor={inputId}
|
|
2796
|
-
className={cn("text-sm font-medium text-[#333333]", labelClassName)}
|
|
2797
|
-
>
|
|
2798
|
-
{label}
|
|
2799
|
-
{required && <span className="text-[#FF3B3B] ml-0.5">*</span>}
|
|
2800
|
-
</label>
|
|
2801
|
-
)}
|
|
2802
|
-
|
|
2803
|
-
{/* Input or Input Container */}
|
|
2804
|
-
{hasAddons ? (
|
|
2805
|
-
<div
|
|
2806
|
-
className={cn(
|
|
2807
|
-
textFieldContainerVariants({ state: derivedState, disabled: disabled || loading }),
|
|
2808
|
-
"h-10 px-4",
|
|
2809
|
-
inputContainerClassName
|
|
2810
|
-
)}
|
|
2811
|
-
>
|
|
2812
|
-
{prefix && <span className="text-sm text-[#6B7280] mr-2 select-none">{prefix}</span>}
|
|
2813
|
-
{leftIcon && <span className="mr-2 text-[#6B7280] [&_svg]:size-4 flex-shrink-0">{leftIcon}</span>}
|
|
2814
|
-
{inputElement}
|
|
2815
|
-
{loading && <Loader2 className="animate-spin size-4 text-[#6B7280] ml-2 flex-shrink-0" />}
|
|
2816
|
-
{!loading && rightIcon && <span className="ml-2 text-[#6B7280] [&_svg]:size-4 flex-shrink-0">{rightIcon}</span>}
|
|
2817
|
-
{suffix && <span className="text-sm text-[#6B7280] ml-2 select-none">{suffix}</span>}
|
|
2818
|
-
</div>
|
|
2819
|
-
) : (
|
|
2820
|
-
inputElement
|
|
2821
|
-
)}
|
|
2822
|
-
|
|
2823
|
-
{/* Helper text / Error message / Character count */}
|
|
2824
|
-
{(error || helperText || (showCount && maxLength)) && (
|
|
2825
|
-
<div className="flex justify-between items-start gap-2">
|
|
2826
|
-
{error ? (
|
|
2827
|
-
<span id={errorId} className="text-xs text-[#FF3B3B]">
|
|
2828
|
-
{error}
|
|
2829
|
-
</span>
|
|
2830
|
-
) : helperText ? (
|
|
2831
|
-
<span id={helperId} className="text-xs text-[#6B7280]">
|
|
2832
|
-
{helperText}
|
|
2833
|
-
</span>
|
|
2834
|
-
) : (
|
|
2835
|
-
<span />
|
|
2836
|
-
)}
|
|
2837
|
-
{showCount && maxLength && (
|
|
2838
|
-
<span
|
|
2839
|
-
className={cn(
|
|
2840
|
-
"text-xs",
|
|
2841
|
-
charCount > maxLength ? "text-[#FF3B3B]" : "text-[#6B7280]"
|
|
2842
|
-
)}
|
|
2843
|
-
>
|
|
2844
|
-
{charCount}/{maxLength}
|
|
2845
|
-
</span>
|
|
2846
|
-
)}
|
|
2847
|
-
</div>
|
|
2688
|
+
<span className="font-semibold mr-1">{label}</span>
|
|
2848
2689
|
)}
|
|
2849
|
-
|
|
2690
|
+
<span className="font-normal">{children}</span>
|
|
2691
|
+
</span>
|
|
2850
2692
|
)
|
|
2851
2693
|
}
|
|
2852
2694
|
)
|
|
2853
|
-
|
|
2695
|
+
Tag.displayName = "Tag"
|
|
2854
2696
|
|
|
2855
|
-
|
|
2697
|
+
/**
|
|
2698
|
+
* TagGroup component for displaying multiple tags with overflow indicator.
|
|
2699
|
+
*
|
|
2700
|
+
* @example
|
|
2701
|
+
* \`\`\`tsx
|
|
2702
|
+
* <TagGroup
|
|
2703
|
+
* tags={[
|
|
2704
|
+
* { label: "In Call Event:", value: "Call Begin, Start Dialing" },
|
|
2705
|
+
* { label: "Whatsapp Event:", value: "message.Delivered" },
|
|
2706
|
+
* { value: "After Call Event" },
|
|
2707
|
+
* ]}
|
|
2708
|
+
* maxVisible={2}
|
|
2709
|
+
* />
|
|
2710
|
+
* \`\`\`
|
|
2711
|
+
*/
|
|
2712
|
+
export interface TagGroupProps {
|
|
2713
|
+
/** Array of tags to display */
|
|
2714
|
+
tags: Array<{ label?: string; value: string }>
|
|
2715
|
+
/** Maximum number of tags to show before overflow (default: 2) */
|
|
2716
|
+
maxVisible?: number
|
|
2717
|
+
/** Tag variant */
|
|
2718
|
+
variant?: TagProps['variant']
|
|
2719
|
+
/** Tag size */
|
|
2720
|
+
size?: TagProps['size']
|
|
2721
|
+
/** Additional className for the container */
|
|
2722
|
+
className?: string
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
const TagGroup = ({
|
|
2726
|
+
tags,
|
|
2727
|
+
maxVisible = 2,
|
|
2728
|
+
variant,
|
|
2729
|
+
size,
|
|
2730
|
+
className,
|
|
2731
|
+
}: TagGroupProps) => {
|
|
2732
|
+
const visibleTags = tags.slice(0, maxVisible)
|
|
2733
|
+
const overflowCount = tags.length - maxVisible
|
|
2734
|
+
|
|
2735
|
+
return (
|
|
2736
|
+
<div className={cn("flex flex-col items-start gap-2", className)}>
|
|
2737
|
+
{visibleTags.map((tag, index) => {
|
|
2738
|
+
const isLastVisible = index === visibleTags.length - 1 && overflowCount > 0
|
|
2739
|
+
|
|
2740
|
+
if (isLastVisible) {
|
|
2741
|
+
return (
|
|
2742
|
+
<div key={index} className="flex items-center gap-2">
|
|
2743
|
+
<Tag label={tag.label} variant={variant} size={size}>
|
|
2744
|
+
{tag.value}
|
|
2745
|
+
</Tag>
|
|
2746
|
+
<Tag variant={variant} size={size}>
|
|
2747
|
+
+{overflowCount} more
|
|
2748
|
+
</Tag>
|
|
2749
|
+
</div>
|
|
2750
|
+
)
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
return (
|
|
2754
|
+
<Tag key={index} label={tag.label} variant={variant} size={size}>
|
|
2755
|
+
{tag.value}
|
|
2756
|
+
</Tag>
|
|
2757
|
+
)
|
|
2758
|
+
})}
|
|
2759
|
+
</div>
|
|
2760
|
+
)
|
|
2761
|
+
}
|
|
2762
|
+
TagGroup.displayName = "TagGroup"
|
|
2763
|
+
|
|
2764
|
+
export { Tag, TagGroup, tagVariants }
|
|
2856
2765
|
`, prefix)
|
|
2857
2766
|
}
|
|
2858
2767
|
]
|
|
2859
2768
|
},
|
|
2860
|
-
"
|
|
2861
|
-
name: "
|
|
2862
|
-
description: "
|
|
2769
|
+
"collapsible": {
|
|
2770
|
+
name: "collapsible",
|
|
2771
|
+
description: "An expandable/collapsible section component with single or multiple mode support",
|
|
2863
2772
|
dependencies: [
|
|
2864
2773
|
"class-variance-authority",
|
|
2865
2774
|
"clsx",
|
|
2866
|
-
"tailwind-merge"
|
|
2775
|
+
"tailwind-merge",
|
|
2776
|
+
"lucide-react"
|
|
2867
2777
|
],
|
|
2868
2778
|
files: [
|
|
2869
2779
|
{
|
|
2870
|
-
name: "
|
|
2780
|
+
name: "collapsible.tsx",
|
|
2871
2781
|
content: prefixTailwindClasses(`import * as React from "react"
|
|
2872
2782
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
2783
|
+
import { ChevronDown } from "lucide-react"
|
|
2873
2784
|
|
|
2874
2785
|
import { cn } from "../../lib/utils"
|
|
2875
2786
|
|
|
2876
2787
|
/**
|
|
2877
|
-
*
|
|
2788
|
+
* Collapsible root variants
|
|
2878
2789
|
*/
|
|
2879
|
-
const
|
|
2880
|
-
|
|
2790
|
+
const collapsibleVariants = cva("w-full", {
|
|
2791
|
+
variants: {
|
|
2792
|
+
variant: {
|
|
2793
|
+
default: "",
|
|
2794
|
+
bordered: "border border-[#E5E7EB] rounded-lg divide-y divide-[#E5E7EB]",
|
|
2795
|
+
},
|
|
2796
|
+
},
|
|
2797
|
+
defaultVariants: {
|
|
2798
|
+
variant: "default",
|
|
2799
|
+
},
|
|
2800
|
+
})
|
|
2801
|
+
|
|
2802
|
+
/**
|
|
2803
|
+
* Collapsible item variants
|
|
2804
|
+
*/
|
|
2805
|
+
const collapsibleItemVariants = cva("", {
|
|
2806
|
+
variants: {
|
|
2807
|
+
variant: {
|
|
2808
|
+
default: "",
|
|
2809
|
+
bordered: "",
|
|
2810
|
+
},
|
|
2811
|
+
},
|
|
2812
|
+
defaultVariants: {
|
|
2813
|
+
variant: "default",
|
|
2814
|
+
},
|
|
2815
|
+
})
|
|
2816
|
+
|
|
2817
|
+
/**
|
|
2818
|
+
* Collapsible trigger variants
|
|
2819
|
+
*/
|
|
2820
|
+
const collapsibleTriggerVariants = cva(
|
|
2821
|
+
"flex w-full items-center justify-between text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#343E55] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
2881
2822
|
{
|
|
2882
2823
|
variants: {
|
|
2883
|
-
|
|
2884
|
-
default: "
|
|
2885
|
-
|
|
2886
|
-
lg: "h-7 w-14",
|
|
2824
|
+
variant: {
|
|
2825
|
+
default: "py-3",
|
|
2826
|
+
bordered: "p-4 hover:bg-[#F9FAFB]",
|
|
2887
2827
|
},
|
|
2888
2828
|
},
|
|
2889
2829
|
defaultVariants: {
|
|
2890
|
-
|
|
2830
|
+
variant: "default",
|
|
2891
2831
|
},
|
|
2892
2832
|
}
|
|
2893
2833
|
)
|
|
2894
2834
|
|
|
2895
2835
|
/**
|
|
2896
|
-
*
|
|
2836
|
+
* Collapsible content variants
|
|
2897
2837
|
*/
|
|
2898
|
-
const
|
|
2899
|
-
"
|
|
2838
|
+
const collapsibleContentVariants = cva(
|
|
2839
|
+
"overflow-hidden transition-all duration-300 ease-in-out",
|
|
2900
2840
|
{
|
|
2901
2841
|
variants: {
|
|
2902
|
-
|
|
2903
|
-
default: "
|
|
2904
|
-
|
|
2905
|
-
lg: "h-6 w-6",
|
|
2906
|
-
},
|
|
2907
|
-
checked: {
|
|
2908
|
-
true: "",
|
|
2909
|
-
false: "translate-x-0",
|
|
2842
|
+
variant: {
|
|
2843
|
+
default: "",
|
|
2844
|
+
bordered: "px-4",
|
|
2910
2845
|
},
|
|
2911
2846
|
},
|
|
2912
|
-
compoundVariants: [
|
|
2913
|
-
{ size: "default", checked: true, className: "translate-x-5" },
|
|
2914
|
-
{ size: "sm", checked: true, className: "translate-x-4" },
|
|
2915
|
-
{ size: "lg", checked: true, className: "translate-x-7" },
|
|
2916
|
-
],
|
|
2917
2847
|
defaultVariants: {
|
|
2918
|
-
|
|
2919
|
-
checked: false,
|
|
2848
|
+
variant: "default",
|
|
2920
2849
|
},
|
|
2921
2850
|
}
|
|
2922
|
-
)
|
|
2851
|
+
)
|
|
2852
|
+
|
|
2853
|
+
// Types
|
|
2854
|
+
type CollapsibleType = "single" | "multiple"
|
|
2855
|
+
|
|
2856
|
+
interface CollapsibleContextValue {
|
|
2857
|
+
type: CollapsibleType
|
|
2858
|
+
value: string[]
|
|
2859
|
+
onValueChange: (value: string[]) => void
|
|
2860
|
+
variant: "default" | "bordered"
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
interface CollapsibleItemContextValue {
|
|
2864
|
+
value: string
|
|
2865
|
+
isOpen: boolean
|
|
2866
|
+
disabled?: boolean
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// Contexts
|
|
2870
|
+
const CollapsibleContext = React.createContext<CollapsibleContextValue | null>(null)
|
|
2871
|
+
const CollapsibleItemContext = React.createContext<CollapsibleItemContextValue | null>(null)
|
|
2872
|
+
|
|
2873
|
+
function useCollapsibleContext() {
|
|
2874
|
+
const context = React.useContext(CollapsibleContext)
|
|
2875
|
+
if (!context) {
|
|
2876
|
+
throw new Error("Collapsible components must be used within a Collapsible")
|
|
2877
|
+
}
|
|
2878
|
+
return context
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
function useCollapsibleItemContext() {
|
|
2882
|
+
const context = React.useContext(CollapsibleItemContext)
|
|
2883
|
+
if (!context) {
|
|
2884
|
+
throw new Error("CollapsibleTrigger/CollapsibleContent must be used within a CollapsibleItem")
|
|
2885
|
+
}
|
|
2886
|
+
return context
|
|
2887
|
+
}
|
|
2923
2888
|
|
|
2924
2889
|
/**
|
|
2925
|
-
*
|
|
2926
|
-
*
|
|
2927
|
-
* @example
|
|
2928
|
-
* \`\`\`tsx
|
|
2929
|
-
* <Toggle checked={isEnabled} onCheckedChange={setIsEnabled} />
|
|
2930
|
-
* <Toggle size="sm" disabled />
|
|
2931
|
-
* <Toggle size="lg" checked label="Enable notifications" />
|
|
2932
|
-
* \`\`\`
|
|
2890
|
+
* Root collapsible component that manages state
|
|
2933
2891
|
*/
|
|
2934
|
-
export interface
|
|
2935
|
-
extends
|
|
2936
|
-
VariantProps<typeof
|
|
2937
|
-
/** Whether
|
|
2938
|
-
|
|
2939
|
-
/**
|
|
2940
|
-
|
|
2941
|
-
/**
|
|
2942
|
-
|
|
2943
|
-
/**
|
|
2944
|
-
|
|
2945
|
-
/** Position of the label */
|
|
2946
|
-
labelPosition?: "left" | "right"
|
|
2892
|
+
export interface CollapsibleProps
|
|
2893
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
2894
|
+
VariantProps<typeof collapsibleVariants> {
|
|
2895
|
+
/** Whether only one item can be open at a time ('single') or multiple ('multiple') */
|
|
2896
|
+
type?: CollapsibleType
|
|
2897
|
+
/** Controlled value - array of open item values */
|
|
2898
|
+
value?: string[]
|
|
2899
|
+
/** Default open items for uncontrolled usage */
|
|
2900
|
+
defaultValue?: string[]
|
|
2901
|
+
/** Callback when open items change */
|
|
2902
|
+
onValueChange?: (value: string[]) => void
|
|
2947
2903
|
}
|
|
2948
2904
|
|
|
2949
|
-
const
|
|
2905
|
+
const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
|
|
2950
2906
|
(
|
|
2951
2907
|
{
|
|
2952
2908
|
className,
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
labelPosition = "right",
|
|
2909
|
+
variant = "default",
|
|
2910
|
+
type = "multiple",
|
|
2911
|
+
value: controlledValue,
|
|
2912
|
+
defaultValue = [],
|
|
2913
|
+
onValueChange,
|
|
2914
|
+
children,
|
|
2960
2915
|
...props
|
|
2961
2916
|
},
|
|
2962
2917
|
ref
|
|
2963
2918
|
) => {
|
|
2964
|
-
const [
|
|
2919
|
+
const [internalValue, setInternalValue] = React.useState<string[]>(defaultValue)
|
|
2965
2920
|
|
|
2966
|
-
const isControlled =
|
|
2967
|
-
const
|
|
2921
|
+
const isControlled = controlledValue !== undefined
|
|
2922
|
+
const currentValue = isControlled ? controlledValue : internalValue
|
|
2923
|
+
|
|
2924
|
+
const handleValueChange = React.useCallback(
|
|
2925
|
+
(newValue: string[]) => {
|
|
2926
|
+
if (!isControlled) {
|
|
2927
|
+
setInternalValue(newValue)
|
|
2928
|
+
}
|
|
2929
|
+
onValueChange?.(newValue)
|
|
2930
|
+
},
|
|
2931
|
+
[isControlled, onValueChange]
|
|
2932
|
+
)
|
|
2933
|
+
|
|
2934
|
+
const contextValue = React.useMemo(
|
|
2935
|
+
() => ({
|
|
2936
|
+
type,
|
|
2937
|
+
value: currentValue,
|
|
2938
|
+
onValueChange: handleValueChange,
|
|
2939
|
+
variant: variant || "default",
|
|
2940
|
+
}),
|
|
2941
|
+
[type, currentValue, handleValueChange, variant]
|
|
2942
|
+
)
|
|
2943
|
+
|
|
2944
|
+
return (
|
|
2945
|
+
<CollapsibleContext.Provider value={contextValue}>
|
|
2946
|
+
<div
|
|
2947
|
+
ref={ref}
|
|
2948
|
+
className={cn(collapsibleVariants({ variant, className }))}
|
|
2949
|
+
{...props}
|
|
2950
|
+
>
|
|
2951
|
+
{children}
|
|
2952
|
+
</div>
|
|
2953
|
+
</CollapsibleContext.Provider>
|
|
2954
|
+
)
|
|
2955
|
+
}
|
|
2956
|
+
)
|
|
2957
|
+
Collapsible.displayName = "Collapsible"
|
|
2958
|
+
|
|
2959
|
+
/**
|
|
2960
|
+
* Individual collapsible item
|
|
2961
|
+
*/
|
|
2962
|
+
export interface CollapsibleItemProps
|
|
2963
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
2964
|
+
VariantProps<typeof collapsibleItemVariants> {
|
|
2965
|
+
/** Unique value for this item */
|
|
2966
|
+
value: string
|
|
2967
|
+
/** Whether this item is disabled */
|
|
2968
|
+
disabled?: boolean
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
const CollapsibleItem = React.forwardRef<HTMLDivElement, CollapsibleItemProps>(
|
|
2972
|
+
({ className, value, disabled, children, ...props }, ref) => {
|
|
2973
|
+
const { value: openValues, variant } = useCollapsibleContext()
|
|
2974
|
+
const isOpen = openValues.includes(value)
|
|
2975
|
+
|
|
2976
|
+
const contextValue = React.useMemo(
|
|
2977
|
+
() => ({
|
|
2978
|
+
value,
|
|
2979
|
+
isOpen,
|
|
2980
|
+
disabled,
|
|
2981
|
+
}),
|
|
2982
|
+
[value, isOpen, disabled]
|
|
2983
|
+
)
|
|
2984
|
+
|
|
2985
|
+
return (
|
|
2986
|
+
<CollapsibleItemContext.Provider value={contextValue}>
|
|
2987
|
+
<div
|
|
2988
|
+
ref={ref}
|
|
2989
|
+
data-state={isOpen ? "open" : "closed"}
|
|
2990
|
+
className={cn(collapsibleItemVariants({ variant, className }))}
|
|
2991
|
+
{...props}
|
|
2992
|
+
>
|
|
2993
|
+
{children}
|
|
2994
|
+
</div>
|
|
2995
|
+
</CollapsibleItemContext.Provider>
|
|
2996
|
+
)
|
|
2997
|
+
}
|
|
2998
|
+
)
|
|
2999
|
+
CollapsibleItem.displayName = "CollapsibleItem"
|
|
3000
|
+
|
|
3001
|
+
/**
|
|
3002
|
+
* Trigger button that toggles the collapsible item
|
|
3003
|
+
*/
|
|
3004
|
+
export interface CollapsibleTriggerProps
|
|
3005
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
3006
|
+
VariantProps<typeof collapsibleTriggerVariants> {
|
|
3007
|
+
/** Whether to show the chevron icon */
|
|
3008
|
+
showChevron?: boolean
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
|
|
3012
|
+
({ className, showChevron = true, children, ...props }, ref) => {
|
|
3013
|
+
const { type, value: openValues, onValueChange, variant } = useCollapsibleContext()
|
|
3014
|
+
const { value, isOpen, disabled } = useCollapsibleItemContext()
|
|
2968
3015
|
|
|
2969
3016
|
const handleClick = () => {
|
|
2970
3017
|
if (disabled) return
|
|
2971
3018
|
|
|
2972
|
-
|
|
3019
|
+
let newValue: string[]
|
|
2973
3020
|
|
|
2974
|
-
if (
|
|
2975
|
-
|
|
3021
|
+
if (type === "single") {
|
|
3022
|
+
// In single mode, toggle current item (close if open, open if closed)
|
|
3023
|
+
newValue = isOpen ? [] : [value]
|
|
3024
|
+
} else {
|
|
3025
|
+
// In multiple mode, toggle the item in the array
|
|
3026
|
+
newValue = isOpen
|
|
3027
|
+
? openValues.filter((v) => v !== value)
|
|
3028
|
+
: [...openValues, value]
|
|
2976
3029
|
}
|
|
2977
3030
|
|
|
2978
|
-
|
|
3031
|
+
onValueChange(newValue)
|
|
2979
3032
|
}
|
|
2980
3033
|
|
|
2981
|
-
|
|
3034
|
+
return (
|
|
2982
3035
|
<button
|
|
2983
|
-
type="button"
|
|
2984
|
-
role="switch"
|
|
2985
|
-
aria-checked={isChecked}
|
|
2986
3036
|
ref={ref}
|
|
3037
|
+
type="button"
|
|
3038
|
+
aria-expanded={isOpen}
|
|
2987
3039
|
disabled={disabled}
|
|
2988
3040
|
onClick={handleClick}
|
|
2989
|
-
className={cn(
|
|
2990
|
-
toggleVariants({ size, className }),
|
|
2991
|
-
isChecked ? "bg-[#343E55]" : "bg-[#E5E7EB]"
|
|
2992
|
-
)}
|
|
3041
|
+
className={cn(collapsibleTriggerVariants({ variant, className }))}
|
|
2993
3042
|
{...props}
|
|
2994
3043
|
>
|
|
2995
|
-
<span
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3044
|
+
<span className="flex-1">{children}</span>
|
|
3045
|
+
{showChevron && (
|
|
3046
|
+
<ChevronDown
|
|
3047
|
+
className={cn(
|
|
3048
|
+
"h-4 w-4 shrink-0 text-[#6B7280] transition-transform duration-300",
|
|
3049
|
+
isOpen && "rotate-180"
|
|
3050
|
+
)}
|
|
3051
|
+
/>
|
|
3052
|
+
)}
|
|
3000
3053
|
</button>
|
|
3001
3054
|
)
|
|
3055
|
+
}
|
|
3056
|
+
)
|
|
3057
|
+
CollapsibleTrigger.displayName = "CollapsibleTrigger"
|
|
3002
3058
|
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
</span>
|
|
3010
|
-
)}
|
|
3011
|
-
{toggle}
|
|
3012
|
-
{labelPosition === "right" && (
|
|
3013
|
-
<span className={cn("text-sm text-[#333333]", disabled && "opacity-50")}>
|
|
3014
|
-
{label}
|
|
3015
|
-
</span>
|
|
3016
|
-
)}
|
|
3017
|
-
</label>
|
|
3018
|
-
)
|
|
3019
|
-
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Content that is shown/hidden when the item is toggled
|
|
3061
|
+
*/
|
|
3062
|
+
export interface CollapsibleContentProps
|
|
3063
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
3064
|
+
VariantProps<typeof collapsibleContentVariants> {}
|
|
3020
3065
|
|
|
3021
|
-
|
|
3066
|
+
const CollapsibleContent = React.forwardRef<HTMLDivElement, CollapsibleContentProps>(
|
|
3067
|
+
({ className, children, ...props }, ref) => {
|
|
3068
|
+
const { variant } = useCollapsibleContext()
|
|
3069
|
+
const { isOpen } = useCollapsibleItemContext()
|
|
3070
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
3071
|
+
const [height, setHeight] = React.useState<number | undefined>(undefined)
|
|
3072
|
+
|
|
3073
|
+
React.useEffect(() => {
|
|
3074
|
+
if (contentRef.current) {
|
|
3075
|
+
const contentHeight = contentRef.current.scrollHeight
|
|
3076
|
+
setHeight(isOpen ? contentHeight : 0)
|
|
3077
|
+
}
|
|
3078
|
+
}, [isOpen, children])
|
|
3079
|
+
|
|
3080
|
+
return (
|
|
3081
|
+
<div
|
|
3082
|
+
ref={ref}
|
|
3083
|
+
className={cn(collapsibleContentVariants({ variant, className }))}
|
|
3084
|
+
style={{ height: height !== undefined ? \`\${height}px\` : undefined }}
|
|
3085
|
+
aria-hidden={!isOpen}
|
|
3086
|
+
{...props}
|
|
3087
|
+
>
|
|
3088
|
+
<div ref={contentRef} className="pb-4">
|
|
3089
|
+
{children}
|
|
3090
|
+
</div>
|
|
3091
|
+
</div>
|
|
3092
|
+
)
|
|
3022
3093
|
}
|
|
3023
3094
|
)
|
|
3024
|
-
|
|
3095
|
+
CollapsibleContent.displayName = "CollapsibleContent"
|
|
3025
3096
|
|
|
3026
|
-
export {
|
|
3097
|
+
export {
|
|
3098
|
+
Collapsible,
|
|
3099
|
+
CollapsibleItem,
|
|
3100
|
+
CollapsibleTrigger,
|
|
3101
|
+
CollapsibleContent,
|
|
3102
|
+
collapsibleVariants,
|
|
3103
|
+
collapsibleItemVariants,
|
|
3104
|
+
collapsibleTriggerVariants,
|
|
3105
|
+
collapsibleContentVariants,
|
|
3106
|
+
}
|
|
3027
3107
|
`, prefix)
|
|
3028
3108
|
}
|
|
3029
3109
|
]
|