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