myoperator-ui 0.0.69 → 0.0.71

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