myoperator-ui 0.0.68 → 0.0.70

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