@edux-design/forms 0.0.4 → 0.0.6

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.mjs CHANGED
@@ -25,6 +25,13 @@ var FieldProvider = ({ children }) => {
25
25
  if (!id) return;
26
26
  setDescribedByIds((prev) => prev.filter((x) => x !== id));
27
27
  }, []);
28
+ const getPartId = useCallback(
29
+ (part) => {
30
+ if (!part) return baseId;
31
+ return `${baseId}-${part}`;
32
+ },
33
+ [baseId]
34
+ );
28
35
  const value = useMemo(
29
36
  () => ({
30
37
  labelHTMLForId: baseId,
@@ -32,9 +39,17 @@ var FieldProvider = ({ children }) => {
32
39
  setType,
33
40
  describedByIds,
34
41
  registerDescription,
35
- unregisterDescription
42
+ unregisterDescription,
43
+ getPartId
36
44
  }),
37
- [baseId, type, describedByIds, registerDescription, unregisterDescription]
45
+ [
46
+ baseId,
47
+ type,
48
+ describedByIds,
49
+ registerDescription,
50
+ unregisterDescription,
51
+ getPartId
52
+ ]
38
53
  );
39
54
  return /* @__PURE__ */ jsx(FieldContext.Provider, { value, children });
40
55
  };
@@ -100,7 +115,7 @@ var Input = forwardRef(
100
115
  }, ref) => {
101
116
  const { labelHTMLForId } = useFieldContext();
102
117
  const hasValue = Boolean(props.value);
103
- const defaults = "h-[44px] w-full border-2 rounded-md border-border-base focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out pl-5";
118
+ const defaults = "h-[44px] bg-white w-full border-2 rounded-md border-border-base focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out pl-5";
104
119
  const fonts = "font-normal text-base";
105
120
  const variants = {
106
121
  primary: "hover:border-2 hover:border-border-primary-base focus:border-2 focus:border-border-primary-base focus-within:border-2 focus-within:border-border-primary-base",
@@ -175,9 +190,16 @@ import React4, { useEffect, useId as useId2 } from "react";
175
190
  import { cx as cx2 } from "@edux-design/utils";
176
191
  import { Close as Close2 } from "@edux-design/icons";
177
192
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
178
- var Field = ({ children }) => {
193
+ var Field = ({
194
+ children,
195
+ orientation = "vertical",
196
+ className
197
+ }) => {
198
+ const orientationClasses = orientation === "horizontal" ? "flex-row items-center gap-6" : "flex-col items-start gap-8";
179
199
  const FIELD_GROUP_WRAPPER_STYLES = cx2(
180
- "flex flex-col items-start relative gap-8"
200
+ "flex relative",
201
+ orientationClasses,
202
+ className
181
203
  );
182
204
  return /* @__PURE__ */ jsx4(FieldProvider, { children: /* @__PURE__ */ jsx4("div", { className: FIELD_GROUP_WRAPPER_STYLES, children }) });
183
205
  };
@@ -252,12 +274,376 @@ function Radio() {
252
274
  ] });
253
275
  }
254
276
 
255
- // src/elements/Textarea.jsx
256
- import React6, { forwardRef as forwardRef2 } from "react";
277
+ // src/elements/Checkbox.jsx
278
+ import React6, { forwardRef as forwardRef2, useCallback as useCallback3, useEffect as useEffect2, useRef } from "react";
257
279
  import { cx as cx4 } from "@edux-design/utils";
258
- import { Close as Close3 } from "@edux-design/icons";
280
+ import { Check, Indeterminate } from "@edux-design/icons";
259
281
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
260
- var Textarea = forwardRef2(
282
+ var VARIANT_INTENTS = {
283
+ primary: {
284
+ baseBorder: "border-border-base",
285
+ baseBg: "bg-bg-base",
286
+ hoverBorder: "peer-hover:border-border-primary-base",
287
+ activeBorder: "peer-active:border-border-primary-emphasis",
288
+ activeBg: "peer-active:bg-bg-primary-subtle",
289
+ peerSelectedBorder: "peer-checked:border-border-primary-base",
290
+ peerSelectedBg: "peer-checked:bg-bg-primary-base",
291
+ selectedBorder: "border-border-primary-base",
292
+ selectedBg: "bg-bg-primary-base",
293
+ iconColor: "text-white"
294
+ },
295
+ success: {
296
+ baseBorder: "border-border-success-base",
297
+ baseBg: "bg-bg-base",
298
+ hoverBorder: "peer-hover:border-border-success-emphasis",
299
+ activeBorder: "peer-active:border-border-success-emphasis",
300
+ activeBg: "peer-active:bg-bg-success-subtle",
301
+ peerSelectedBorder: "peer-checked:border-border-success-base",
302
+ peerSelectedBg: "peer-checked:bg-bg-success-base",
303
+ selectedBorder: "border-border-success-base",
304
+ selectedBg: "bg-bg-success-base",
305
+ iconColor: "text-white"
306
+ },
307
+ corrective: {
308
+ baseBorder: "border-border-corrective-base",
309
+ baseBg: "bg-bg-base",
310
+ hoverBorder: "peer-hover:border-border-corrective-emphasis",
311
+ activeBorder: "peer-active:border-border-corrective-emphasis",
312
+ activeBg: "peer-active:bg-bg-corrective-subtle",
313
+ peerSelectedBorder: "peer-checked:border-border-corrective-base",
314
+ peerSelectedBg: "peer-checked:bg-bg-corrective-base",
315
+ selectedBorder: "border-border-corrective-base",
316
+ selectedBg: "bg-bg-corrective-base",
317
+ iconColor: "text-fg-base"
318
+ },
319
+ error: {
320
+ baseBorder: "border-border-danger-base",
321
+ baseBg: "bg-bg-base",
322
+ hoverBorder: "peer-hover:border-border-danger-emphasis",
323
+ activeBorder: "peer-active:border-border-danger-emphasis",
324
+ activeBg: "peer-active:bg-bg-danger-subtle",
325
+ peerSelectedBorder: "peer-checked:border-border-danger-base",
326
+ peerSelectedBg: "peer-checked:bg-bg-danger-base",
327
+ selectedBorder: "border-border-danger-base",
328
+ selectedBg: "bg-bg-danger-base",
329
+ iconColor: "text-white"
330
+ },
331
+ inactive: {
332
+ baseBorder: "border-border-subtle",
333
+ baseBg: "bg-bg-inactive",
334
+ hoverBorder: null,
335
+ activeBorder: null,
336
+ activeBg: null,
337
+ peerSelectedBorder: null,
338
+ peerSelectedBg: null,
339
+ selectedBorder: "border-border-subtle",
340
+ selectedBg: "bg-bg-inactive",
341
+ iconColor: "text-fg-inactive"
342
+ }
343
+ };
344
+ var Checkbox = forwardRef2(
345
+ ({
346
+ className,
347
+ indeterminate = false,
348
+ variant = "primary",
349
+ disabled,
350
+ id: providedId,
351
+ ...rest
352
+ }, ref) => {
353
+ const {
354
+ ["aria-describedby"]: ariaDescribedByProp,
355
+ ["aria-checked"]: ariaCheckedProp,
356
+ ...inputProps
357
+ } = rest;
358
+ const {
359
+ labelHTMLForId,
360
+ describedByIds = []
361
+ } = useFieldContext();
362
+ const resolvedId = providedId ?? labelHTMLForId;
363
+ const intent = VARIANT_INTENTS[variant] ?? VARIANT_INTENTS.primary;
364
+ const derivedDisabled = Boolean(disabled);
365
+ const isInactiveVariant = variant === "inactive";
366
+ const isDisabled = derivedDisabled || isInactiveVariant;
367
+ const describedByTokens = [
368
+ ...describedByIds,
369
+ ...ariaDescribedByProp ? ariaDescribedByProp.split(" ") : []
370
+ ].filter(Boolean);
371
+ const describedBy = describedByTokens.length ? describedByTokens.join(" ") : void 0;
372
+ const ariaChecked = indeterminate ? "mixed" : ariaCheckedProp;
373
+ const inputRef = useRef(null);
374
+ const setRefs = useCallback3(
375
+ (node) => {
376
+ inputRef.current = node;
377
+ if (typeof ref === "function") {
378
+ ref(node);
379
+ } else if (ref) {
380
+ ref.current = node;
381
+ }
382
+ },
383
+ [ref]
384
+ );
385
+ useEffect2(() => {
386
+ if (inputRef.current) {
387
+ inputRef.current.indeterminate = indeterminate;
388
+ }
389
+ }, [indeterminate]);
390
+ return /* @__PURE__ */ jsxs5(
391
+ "div",
392
+ {
393
+ className: cx4(
394
+ "relative inline-flex h-[28px] w-[28px] items-center justify-center",
395
+ className
396
+ ),
397
+ children: [
398
+ /* @__PURE__ */ jsx6(
399
+ "input",
400
+ {
401
+ id: resolvedId,
402
+ ref: setRefs,
403
+ type: "checkbox",
404
+ className: cx4(
405
+ "peer absolute inset-0 h-full w-full cursor-pointer appearance-none rounded-md bg-transparent",
406
+ "focus:shadow-focus focus-visible:shadow-focus focus:outline-none focus-visible:outline-none",
407
+ isDisabled && "cursor-not-allowed"
408
+ ),
409
+ "aria-checked": ariaChecked,
410
+ "aria-describedby": describedBy,
411
+ "aria-disabled": isDisabled ? "true" : void 0,
412
+ "aria-invalid": variant === "error" ? "true" : void 0,
413
+ disabled: isDisabled,
414
+ ...inputProps
415
+ }
416
+ ),
417
+ /* @__PURE__ */ jsx6(
418
+ "span",
419
+ {
420
+ "data-slot": "checkbox-control",
421
+ "aria-hidden": "true",
422
+ className: cx4(
423
+ "flex h-full w-full items-center justify-center rounded-md border-2 transition-all duration-200 ease-in-out",
424
+ "peer-focus-visible:shadow-focus",
425
+ "peer-disabled:border-border-subtle peer-disabled:bg-bg-inactive peer-disabled:text-fg-inactive",
426
+ intent.baseBg,
427
+ intent.baseBorder,
428
+ !isDisabled && intent.hoverBorder,
429
+ !isDisabled && intent.activeBorder,
430
+ !isDisabled && intent.activeBg,
431
+ intent.peerSelectedBorder,
432
+ intent.peerSelectedBg,
433
+ indeterminate && intent.selectedBorder,
434
+ indeterminate && intent.selectedBg
435
+ )
436
+ }
437
+ ),
438
+ /* @__PURE__ */ jsx6(
439
+ Check,
440
+ {
441
+ "aria-hidden": "true",
442
+ className: cx4(
443
+ "pointer-events-none absolute left-1/2 top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 transition-opacity duration-150",
444
+ intent.iconColor,
445
+ "peer-disabled:text-fg-inactive",
446
+ indeterminate ? "opacity-0" : "opacity-0 peer-checked:opacity-100"
447
+ )
448
+ }
449
+ ),
450
+ /* @__PURE__ */ jsx6(
451
+ Indeterminate,
452
+ {
453
+ "aria-hidden": "true",
454
+ className: cx4(
455
+ "pointer-events-none absolute left-1/2 top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 transition-opacity duration-150",
456
+ intent.iconColor,
457
+ "peer-disabled:text-fg-inactive",
458
+ indeterminate ? "opacity-100" : "opacity-0"
459
+ )
460
+ }
461
+ )
462
+ ]
463
+ }
464
+ );
465
+ }
466
+ );
467
+ Checkbox.displayName = "Checkbox";
468
+
469
+ // src/elements/Switch.jsx
470
+ import React7, { forwardRef as forwardRef3, useState as useState2 } from "react";
471
+ import { cx as cx5 } from "@edux-design/utils";
472
+ import { Check as Check2, Close as Close3 } from "@edux-design/icons";
473
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
474
+ var SWITCH_INTENTS = {
475
+ primary: {
476
+ trackBase: "bg-bg-subtle border-2 border-border-base",
477
+ trackChecked: "peer-checked:bg-bg-primary-subtle peer-checked:border-border-primary-base",
478
+ trackHover: "peer-hover:border-border-primary-base",
479
+ trackActive: "peer-active:border-border-primary-emphasis peer-active:bg-bg-primary-subtle",
480
+ thumbBase: "bg-fg-inactive border-border-base text-bg-emphasis",
481
+ thumbChecked: "peer-checked:bg-bg-primary-base peer-checked:border-border-primary-base peer-checked:text-white",
482
+ thumbHover: "peer-hover:border-border-primary-base",
483
+ thumbActive: "peer-active:border-border-primary-emphasis",
484
+ offIcon: "text-white",
485
+ onIcon: "text-white"
486
+ },
487
+ success: {
488
+ trackBase: "bg-bg-subtle border-2 border-border-success-base",
489
+ trackChecked: "peer-checked:bg-bg-success-subtle peer-checked:border-border-success-base",
490
+ trackHover: "peer-hover:border-border-success-emphasis",
491
+ trackActive: "peer-active:border-border-success-emphasis peer-active:bg-bg-success-subtle",
492
+ thumbBase: "bg-bg-success-subtle border-border-success-base text-fg-success-base",
493
+ thumbChecked: "peer-checked:bg-bg-success-base peer-checked:border-border-success-base peer-checked:text-white",
494
+ thumbHover: "peer-hover:border-border-success-emphasis",
495
+ thumbActive: "peer-active:border-border-success-emphasis",
496
+ offIcon: "text-fg-success-base",
497
+ onIcon: "text-white"
498
+ },
499
+ corrective: {
500
+ trackBase: "bg-bg-corrective-subtle border-2 border-border-corrective-base",
501
+ trackChecked: "peer-checked:bg-bg-corrective-subtle peer-checked:border-border-corrective-base",
502
+ trackHover: "peer-hover:border-border-corrective-emphasis",
503
+ trackActive: "peer-active:border-border-corrective-emphasis peer-active:bg-bg-corrective-subtle",
504
+ thumbBase: "bg-bg-corrective-base border-border-corrective-base text-fg-base",
505
+ thumbChecked: "peer-checked:bg-bg-corrective-base peer-checked:border-border-corrective-base peer-checked:text-fg-base",
506
+ thumbHover: "peer-hover:border-border-corrective-emphasis",
507
+ thumbActive: "peer-active:border-border-corrective-emphasis",
508
+ offIcon: "text-white",
509
+ onIcon: "text-white"
510
+ },
511
+ error: {
512
+ trackBase: "bg-bg-danger-subtle border-2 border-border-danger-base",
513
+ trackChecked: "peer-checked:bg-bg-danger-subtle peer-checked:border-border-danger-base",
514
+ trackHover: "peer-hover:border-border-danger-emphasis",
515
+ trackActive: "peer-active:border-border-danger-emphasis peer-active:bg-bg-danger-subtle",
516
+ thumbBase: "bg-white border-border-danger-base text-fg-danger-base",
517
+ thumbChecked: "peer-checked:bg-bg-danger-base peer-checked:border-border-danger-base peer-checked:text-white",
518
+ thumbHover: "peer-hover:border-border-danger-emphasis",
519
+ thumbActive: "peer-active:border-border-danger-emphasis",
520
+ offIcon: "text-fg-danger-base",
521
+ onIcon: "text-white"
522
+ },
523
+ inactive: {
524
+ trackBase: "bg-bg-inactive border-2 border-border-subtle",
525
+ trackChecked: "peer-checked:bg-bg-inactive peer-checked:border-border-subtle",
526
+ trackHover: null,
527
+ trackActive: null,
528
+ thumbBase: "bg-bg-inactive border-border-subtle text-fg-inactive",
529
+ thumbChecked: "peer-checked:bg-bg-inactive peer-checked:border-border-subtle peer-checked:text-fg-inactive",
530
+ thumbHover: null,
531
+ thumbActive: null,
532
+ offIcon: "text-fg-inactive",
533
+ onIcon: "text-fg-inactive"
534
+ }
535
+ };
536
+ var Switch = forwardRef3(
537
+ ({
538
+ className,
539
+ variant = "primary",
540
+ disabled,
541
+ id: providedId,
542
+ checked,
543
+ onChange,
544
+ ...rest
545
+ }, ref) => {
546
+ const { ["aria-describedby"]: ariaDescribedByProp, ...inputProps } = rest;
547
+ const { labelHTMLForId, describedByIds = [] } = useFieldContext();
548
+ const resolvedId = providedId ?? labelHTMLForId;
549
+ const describedByTokens = [
550
+ ...describedByIds,
551
+ ...ariaDescribedByProp ? ariaDescribedByProp.split(" ") : []
552
+ ].filter(Boolean);
553
+ const ariaDescribedBy = describedByTokens.length > 0 ? describedByTokens.join(" ") : void 0;
554
+ const intent = SWITCH_INTENTS[variant] ?? SWITCH_INTENTS.primary;
555
+ const derivedDisabled = Boolean(disabled);
556
+ const isInactiveVariant = variant === "inactive";
557
+ const isDisabled = derivedDisabled || isInactiveVariant;
558
+ const [unControlledChecked, setUnControlledChecked] = useState2(false);
559
+ const isOn = checked ?? unControlledChecked;
560
+ return /* @__PURE__ */ jsxs6(
561
+ "div",
562
+ {
563
+ className: cx5(
564
+ "relative flex h-[28px] w-[46px] items-center justify-center",
565
+ className
566
+ ),
567
+ children: [
568
+ /* @__PURE__ */ jsx7(
569
+ "input",
570
+ {
571
+ id: resolvedId,
572
+ ref,
573
+ onChange: onChange ? onChange : () => setUnControlledChecked(!unControlledChecked),
574
+ checked: checked ? checked : unControlledChecked,
575
+ type: "checkbox",
576
+ role: "switch",
577
+ className: cx5(
578
+ "peer absolute inset-0 z-10 h-full w-full cursor-pointer appearance-none rounded-full opacity-0",
579
+ "focus:shadow-focus focus-visible:shadow-focus focus:outline-none focus-visible:outline-none",
580
+ isDisabled && "cursor-not-allowed"
581
+ ),
582
+ "aria-describedby": ariaDescribedBy,
583
+ "aria-invalid": variant === "error" ? "true" : void 0,
584
+ "aria-disabled": isDisabled ? "true" : void 0,
585
+ disabled: isDisabled,
586
+ ...inputProps
587
+ }
588
+ ),
589
+ /* @__PURE__ */ jsx7(
590
+ "span",
591
+ {
592
+ "aria-hidden": "true",
593
+ "data-slot": "switch-track",
594
+ className: cx5(
595
+ "pointer-events-none absolute inset-0 rounded-full border-1 transition-all duration-200 ease-out",
596
+ "peer-focus-visible:shadow-focus",
597
+ "peer-disabled:border-border-subtle peer-disabled:bg-bg-inactive",
598
+ intent.trackBase,
599
+ intent.trackChecked,
600
+ !isDisabled && intent.trackHover,
601
+ !isDisabled && intent.trackActive
602
+ )
603
+ }
604
+ ),
605
+ /* @__PURE__ */ jsx7(
606
+ "span",
607
+ {
608
+ "data-slot": "switch-thumb",
609
+ "aria-hidden": "true",
610
+ className: cx5(
611
+ "pointer-events-none absolute left-[5px] top-[5px] flex h-[18px] w-[18px] items-center justify-center rounded-full transition-all duration-200 ease-out transform-gpu",
612
+ "peer-disabled:border-border-subtle peer-disabled:bg-bg-inactive",
613
+ intent.thumbBase,
614
+ intent.thumbChecked,
615
+ !isDisabled && intent.thumbHover,
616
+ !isDisabled && intent.thumbActive,
617
+ "peer-checked:translate-x-[19px]"
618
+ ),
619
+ children: !isOn ? /* @__PURE__ */ jsx7(
620
+ Close3,
621
+ {
622
+ "aria-hidden": "true",
623
+ className: cx5("absolute duration-150", intent.offIcon)
624
+ }
625
+ ) : /* @__PURE__ */ jsx7(
626
+ Check2,
627
+ {
628
+ "aria-hidden": "true",
629
+ className: cx5("absolute duration-150", intent.onIcon)
630
+ }
631
+ )
632
+ }
633
+ )
634
+ ]
635
+ }
636
+ );
637
+ }
638
+ );
639
+ Switch.displayName = "Switch";
640
+
641
+ // src/elements/Textarea.jsx
642
+ import React8, { forwardRef as forwardRef4 } from "react";
643
+ import { cx as cx6 } from "@edux-design/utils";
644
+ import { Close as Close4 } from "@edux-design/icons";
645
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
646
+ var Textarea = forwardRef4(
261
647
  ({
262
648
  children,
263
649
  validation,
@@ -270,7 +656,7 @@ var Textarea = forwardRef2(
270
656
  }, ref) => {
271
657
  const { labelHTMLForId } = useFieldContext();
272
658
  const hasValue = Boolean(props.value);
273
- const defaults = "w-full border-2 rounded-md border-border-base focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out pl-5 py-3 resize-y";
659
+ const defaults = "w-full bg-white border-2 rounded-md border-border-base focus:shadow-focus focus-visible:shadow-focus outline-none transition-all duration-300 ease-in-out pl-5 py-3 resize-y";
274
660
  const fonts = "font-normal text-base leading-relaxed";
275
661
  const variants = {
276
662
  primary: "hover:border-2 hover:border-border-primary-base focus:border-2 focus:border-border-primary-base focus-within:border-2 focus-within:border-border-primary-base",
@@ -279,7 +665,7 @@ var Textarea = forwardRef2(
279
665
  success: "border-border-success-base",
280
666
  inactive: "border-border-subtle bg-bg-subtle"
281
667
  };
282
- const TEXTAREA_STYLES = cx4(
668
+ const TEXTAREA_STYLES = cx6(
283
669
  defaults,
284
670
  fonts,
285
671
  variants[variant],
@@ -287,8 +673,8 @@ var Textarea = forwardRef2(
287
673
  clearable && endIcon && "pr-10",
288
674
  clearable && !endIcon && "pr-5"
289
675
  );
290
- return /* @__PURE__ */ jsxs5("div", { className: "w-full relative flex items-start", children: [
291
- startIcon && /* @__PURE__ */ jsx6(
676
+ return /* @__PURE__ */ jsxs7("div", { className: "w-full relative flex items-start", children: [
677
+ startIcon && /* @__PURE__ */ jsx8(
292
678
  "span",
293
679
  {
294
680
  className: "absolute left-3 top-3 flex items-center pointer-events-none text-fg-subtle",
@@ -296,7 +682,7 @@ var Textarea = forwardRef2(
296
682
  children: startIcon
297
683
  }
298
684
  ),
299
- /* @__PURE__ */ jsx6(
685
+ /* @__PURE__ */ jsx8(
300
686
  "textarea",
301
687
  {
302
688
  id: labelHTMLForId,
@@ -309,7 +695,7 @@ var Textarea = forwardRef2(
309
695
  ...props
310
696
  }
311
697
  ),
312
- clearable && /* @__PURE__ */ jsx6(
698
+ clearable && /* @__PURE__ */ jsx8(
313
699
  "button",
314
700
  {
315
701
  type: "button",
@@ -318,20 +704,20 @@ var Textarea = forwardRef2(
318
704
  var _a;
319
705
  return (_a = props.onChange) == null ? void 0 : _a.call(props, { target: { value: "" } });
320
706
  },
321
- className: cx4(
707
+ className: cx6(
322
708
  clearable && hasValue && endIcon ? "right-9" : "right-3",
323
709
  "absolute top-3 flex items-center p-1 rounded-full outline-none",
324
710
  "transition-all duration-300 ease-in-out",
325
711
  hasValue ? "opacity-100 scale-100" : "opacity-0 scale-90 pointer-events-none",
326
712
  "focus:shadow-focus"
327
713
  ),
328
- children: /* @__PURE__ */ jsx6(Close3, { "aria-hidden": "true", className: "text-fg-subtle" })
714
+ children: /* @__PURE__ */ jsx8(Close4, { "aria-hidden": "true", className: "text-fg-subtle" })
329
715
  }
330
716
  ),
331
- endIcon && /* @__PURE__ */ jsx6(
717
+ endIcon && /* @__PURE__ */ jsx8(
332
718
  "span",
333
719
  {
334
- className: cx4(
720
+ className: cx6(
335
721
  "absolute flex items-center right-3 top-3 text-fg-subtle"
336
722
  ),
337
723
  "aria-hidden": typeof endIcon === "string" ? void 0 : "true",
@@ -341,11 +727,559 @@ var Textarea = forwardRef2(
341
727
  ] });
342
728
  }
343
729
  );
730
+
731
+ // src/elements/Combobox.jsx
732
+ import React9, {
733
+ forwardRef as forwardRef5,
734
+ useCallback as useCallback4,
735
+ useEffect as useEffect3,
736
+ useId as useId3,
737
+ useLayoutEffect,
738
+ useMemo as useMemo2,
739
+ useRef as useRef2,
740
+ useState as useState3
741
+ } from "react";
742
+ import { cx as cx7 } from "@edux-design/utils";
743
+ import { Popover, PopoverAnchor, PopoverContent } from "@edux-design/popovers";
744
+ import { Chip } from "@edux-design/chips";
745
+ import { Chevron } from "@edux-design/icons";
746
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
747
+ var OPTION_COMPONENT_NAME = "ComboboxOption";
748
+ var MENU_ITEM_STYLES = cx7(
749
+ "flex items-center px-32 py-8 cursor-pointer min-w-[100px]",
750
+ "transition-colors duration-200 ease-in-out w-full text-sm",
751
+ "focus:bg-bg-focus focus:outline-none hover:underline hover:bg-bg-primary-subtle"
752
+ );
753
+ var getOptionLabel = (child) => {
754
+ if (!(child == null ? void 0 : child.props)) return "";
755
+ if (typeof child.props.label === "string") {
756
+ return child.props.label;
757
+ }
758
+ if (typeof child.props.children === "string") {
759
+ return child.props.children;
760
+ }
761
+ if (typeof child.props.value === "string") {
762
+ return child.props.value;
763
+ }
764
+ return "";
765
+ };
766
+ var defaultFilter = (option, query) => {
767
+ var _a;
768
+ const normalisedLabel = ((_a = option.label) == null ? void 0 : _a.toString().toLowerCase()) ?? "";
769
+ return normalisedLabel.includes(query.trim().toLowerCase());
770
+ };
771
+ var ComboboxRoot = forwardRef5(
772
+ ({
773
+ children,
774
+ value,
775
+ defaultValue = null,
776
+ onValueChange,
777
+ placeholder = "Select an option",
778
+ disabled = false,
779
+ autoComplete = false,
780
+ multiSelect = false,
781
+ emptyState = "No options found",
782
+ filterFunction = defaultFilter,
783
+ selectOnly = false,
784
+ wrapperClassName,
785
+ contentClassName,
786
+ listClassName,
787
+ optionClassName,
788
+ ...inputProps
789
+ }, ref) => {
790
+ const {
791
+ labelHTMLForId,
792
+ describedByIds = [],
793
+ getPartId
794
+ } = useFieldContext();
795
+ const optionElements = useMemo2(
796
+ () => React9.Children.toArray(children).filter(
797
+ (child) => {
798
+ var _a;
799
+ return React9.isValidElement(child) && ((_a = child.type) == null ? void 0 : _a.displayName) === OPTION_COMPONENT_NAME;
800
+ }
801
+ ),
802
+ [children]
803
+ );
804
+ const normalisedOptions = useMemo2(
805
+ () => optionElements.map((child) => {
806
+ if (child.props.value === void 0 || child.props.value === null) {
807
+ return null;
808
+ }
809
+ return {
810
+ value: child.props.value,
811
+ label: getOptionLabel(child),
812
+ disabled: Boolean(child.props.disabled),
813
+ element: child
814
+ };
815
+ }).filter(Boolean),
816
+ [optionElements]
817
+ );
818
+ const isMultiSelectEnabled = Boolean(multiSelect);
819
+ const isControlled = value !== void 0;
820
+ const [internalValue, setInternalValue] = useState3(() => {
821
+ if (isMultiSelectEnabled) {
822
+ if (Array.isArray(defaultValue)) return defaultValue;
823
+ if (defaultValue === null || defaultValue === void 0) {
824
+ return [];
825
+ }
826
+ return [defaultValue];
827
+ }
828
+ return defaultValue ?? null;
829
+ });
830
+ const resolvedValue = isControlled ? value : internalValue;
831
+ const singleSelectedValue = !isMultiSelectEnabled ? resolvedValue ?? null : null;
832
+ const multiSelectedValues = isMultiSelectEnabled ? Array.isArray(resolvedValue) ? resolvedValue : resolvedValue === null || resolvedValue === void 0 ? [] : [resolvedValue] : [];
833
+ const selectedOption = useMemo2(() => {
834
+ if (isMultiSelectEnabled) return null;
835
+ return normalisedOptions.find(
836
+ (option) => option.value === singleSelectedValue
837
+ ) ?? null;
838
+ }, [isMultiSelectEnabled, normalisedOptions, singleSelectedValue]);
839
+ const multiSelectedOptions = useMemo2(() => {
840
+ if (!isMultiSelectEnabled) return [];
841
+ return normalisedOptions.filter(
842
+ (option) => multiSelectedValues.includes(option.value)
843
+ );
844
+ }, [isMultiSelectEnabled, normalisedOptions, multiSelectedValues]);
845
+ const selectionSummary = isMultiSelectEnabled ? multiSelectedOptions.map((option) => option.label).join(", ") : (selectedOption == null ? void 0 : selectedOption.label) ?? "";
846
+ const shouldFilter = !selectOnly && (autoComplete || isMultiSelectEnabled);
847
+ const [open, setOpen] = useState3(false);
848
+ const [inputValue, setInputValue] = useState3(
849
+ () => isMultiSelectEnabled && shouldFilter ? "" : selectionSummary
850
+ );
851
+ const [isUserTyping, setIsUserTyping] = useState3(false);
852
+ const [activeIndex, setActiveIndex] = useState3(-1);
853
+ const fallbackListboxId = useId3();
854
+ const listboxId = getPartId ? getPartId("listbox") : fallbackListboxId;
855
+ const anchorRef = useRef2(null);
856
+ const inputRef = useRef2(null);
857
+ const mergedRef = useCallback4(
858
+ (node) => {
859
+ inputRef.current = node;
860
+ if (typeof ref === "function") {
861
+ ref(node);
862
+ } else if (ref) {
863
+ ref.current = node;
864
+ }
865
+ },
866
+ [ref]
867
+ );
868
+ useEffect3(() => {
869
+ if (!isMultiSelectEnabled && !open) {
870
+ setInputValue((selectedOption == null ? void 0 : selectedOption.label) ?? "");
871
+ }
872
+ }, [isMultiSelectEnabled, open, selectedOption]);
873
+ const filteredOptions = useMemo2(() => {
874
+ const query = inputValue.trim();
875
+ if (!shouldFilter || !query) {
876
+ return normalisedOptions;
877
+ }
878
+ return normalisedOptions.filter(
879
+ (option) => filterFunction(option, query)
880
+ );
881
+ }, [normalisedOptions, filterFunction, inputValue, shouldFilter]);
882
+ useEffect3(() => {
883
+ if (!open) {
884
+ setActiveIndex(-1);
885
+ return;
886
+ }
887
+ if (!filteredOptions.length) {
888
+ setActiveIndex(-1);
889
+ return;
890
+ }
891
+ if (!isMultiSelectEnabled) {
892
+ const selectedIndex = filteredOptions.findIndex(
893
+ (option) => option.value === singleSelectedValue
894
+ );
895
+ setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
896
+ return;
897
+ }
898
+ setActiveIndex((prev) => prev >= 0 ? prev : 0);
899
+ }, [open, filteredOptions, singleSelectedValue, isMultiSelectEnabled]);
900
+ const [contentWidth, setContentWidth] = useState3(0);
901
+ useLayoutEffect(() => {
902
+ if (anchorRef.current) {
903
+ setContentWidth(anchorRef.current.offsetWidth);
904
+ }
905
+ }, [open]);
906
+ const emitChange = useCallback4(
907
+ (nextValue, option) => {
908
+ if (!isControlled) {
909
+ setInternalValue(nextValue);
910
+ }
911
+ onValueChange == null ? void 0 : onValueChange(nextValue, option);
912
+ },
913
+ [isControlled, onValueChange]
914
+ );
915
+ const selectOption = useCallback4(
916
+ (option) => {
917
+ if (!option || option.disabled) return;
918
+ if (isMultiSelectEnabled) {
919
+ const exists = multiSelectedValues.includes(option.value);
920
+ const nextValues = exists ? multiSelectedValues.filter((value2) => value2 !== option.value) : [...multiSelectedValues, option.value];
921
+ emitChange(nextValues, option);
922
+ if (shouldFilter) {
923
+ setInputValue("");
924
+ setIsUserTyping(false);
925
+ }
926
+ return;
927
+ }
928
+ emitChange(option.value, option);
929
+ setInputValue(option.label);
930
+ setOpen(false);
931
+ },
932
+ [emitChange, isMultiSelectEnabled, multiSelectedValues, shouldFilter]
933
+ );
934
+ const {
935
+ onFocus: userOnFocus,
936
+ onBlur: userOnBlur,
937
+ onKeyDown: userOnKeyDown,
938
+ onChange: userOnInputChange,
939
+ onMouseDown: userOnMouseDown,
940
+ ...restInputProps
941
+ } = inputProps;
942
+ const handleInputChange = (event) => {
943
+ if (disabled) return;
944
+ if (selectOnly) {
945
+ setOpen(true);
946
+ userOnInputChange == null ? void 0 : userOnInputChange(event);
947
+ return;
948
+ }
949
+ if (isMultiSelectEnabled && shouldFilter) {
950
+ setIsUserTyping(true);
951
+ }
952
+ setOpen(true);
953
+ setInputValue(event.target.value);
954
+ userOnInputChange == null ? void 0 : userOnInputChange(event);
955
+ };
956
+ const handleFocus = (event) => {
957
+ if (disabled) return;
958
+ setOpen(true);
959
+ if (isMultiSelectEnabled && autoComplete && !selectOnly) {
960
+ setIsUserTyping(true);
961
+ }
962
+ userOnFocus == null ? void 0 : userOnFocus(event);
963
+ };
964
+ const handleBlur = (event) => {
965
+ setIsUserTyping(false);
966
+ if (isMultiSelectEnabled) {
967
+ setInputValue("");
968
+ }
969
+ userOnBlur == null ? void 0 : userOnBlur(event);
970
+ };
971
+ const handleKeyDown = (event) => {
972
+ if (disabled) {
973
+ userOnKeyDown == null ? void 0 : userOnKeyDown(event);
974
+ return;
975
+ }
976
+ if (event.key === "ArrowDown") {
977
+ event.preventDefault();
978
+ setOpen(true);
979
+ setActiveIndex((prev) => {
980
+ if (!filteredOptions.length) return -1;
981
+ const next = prev + 1;
982
+ return next >= filteredOptions.length ? filteredOptions.length - 1 : next;
983
+ });
984
+ } else if (event.key === "ArrowUp") {
985
+ event.preventDefault();
986
+ setOpen(true);
987
+ setActiveIndex((prev) => {
988
+ if (!filteredOptions.length) return -1;
989
+ const next = prev <= 0 ? 0 : prev - 1;
990
+ return next;
991
+ });
992
+ } else if (event.key === "Enter") {
993
+ if (open && activeIndex >= 0 && filteredOptions[activeIndex]) {
994
+ event.preventDefault();
995
+ selectOption(filteredOptions[activeIndex]);
996
+ }
997
+ } else if (event.key === "Escape") {
998
+ event.preventDefault();
999
+ setOpen(false);
1000
+ } else if (event.key === "Tab") {
1001
+ setOpen(false);
1002
+ }
1003
+ userOnKeyDown == null ? void 0 : userOnKeyDown(event);
1004
+ };
1005
+ const handleInputMouseDown = (event) => {
1006
+ if (disabled) {
1007
+ userOnMouseDown == null ? void 0 : userOnMouseDown(event);
1008
+ return;
1009
+ }
1010
+ setOpen(true);
1011
+ userOnMouseDown == null ? void 0 : userOnMouseDown(event);
1012
+ };
1013
+ const handleChevronMouseDown = (event) => {
1014
+ var _a;
1015
+ event.preventDefault();
1016
+ if (disabled) return;
1017
+ (_a = inputRef.current) == null ? void 0 : _a.focus();
1018
+ setOpen((prev) => !prev);
1019
+ };
1020
+ const handleSingleAnchorMouseDown = (event) => {
1021
+ var _a;
1022
+ if (disabled || isMultiSelectEnabled) return;
1023
+ if (!open) {
1024
+ setOpen(true);
1025
+ }
1026
+ if (selectOnly) {
1027
+ event.preventDefault();
1028
+ (_a = inputRef.current) == null ? void 0 : _a.focus();
1029
+ }
1030
+ };
1031
+ const handleOpenChange = (nextOpen) => {
1032
+ if (disabled) return;
1033
+ setOpen(nextOpen);
1034
+ };
1035
+ const activeDescendant = open && activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : void 0;
1036
+ const describedBy = describedByIds.length ? describedByIds.join(" ") : void 0;
1037
+ const selectionSummaryForDisplay = selectionSummary || "";
1038
+ const inputDisplayValue = isMultiSelectEnabled ? inputValue : inputValue || selectionSummaryForDisplay;
1039
+ const comboboxInputProps = {
1040
+ role: "combobox",
1041
+ "aria-expanded": open,
1042
+ "aria-controls": listboxId,
1043
+ "aria-activedescendant": activeDescendant,
1044
+ "aria-autocomplete": shouldFilter ? "list" : "none",
1045
+ "aria-disabled": disabled || void 0,
1046
+ "aria-describedby": describedBy,
1047
+ "aria-readonly": selectOnly ? "true" : void 0
1048
+ };
1049
+ const listboxAriaProps = isMultiSelectEnabled ? { "aria-multiselectable": "true" } : {};
1050
+ const handleMultiContainerClick = () => {
1051
+ var _a;
1052
+ if (disabled) return;
1053
+ setOpen(true);
1054
+ (_a = inputRef.current) == null ? void 0 : _a.focus();
1055
+ };
1056
+ const selectedChips = isMultiSelectEnabled && multiSelectedOptions.length > 0 ? multiSelectedOptions.map((option) => /* @__PURE__ */ jsx9(
1057
+ Chip,
1058
+ {
1059
+ label: option.label,
1060
+ size: "small",
1061
+ onRemove: () => selectOption(option),
1062
+ removeLabel: `Remove ${option.label}`,
1063
+ disabled: disabled || option.disabled,
1064
+ "data-slot": "combobox-selected-chip",
1065
+ className: "max-w-full"
1066
+ },
1067
+ option.value
1068
+ )) : null;
1069
+ console.log(inputDisplayValue);
1070
+ return /* @__PURE__ */ jsxs8(Popover, { open, onOpenChange: handleOpenChange, children: [
1071
+ /* @__PURE__ */ jsx9(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx9(
1072
+ "div",
1073
+ {
1074
+ ref: anchorRef,
1075
+ className: cx7("w-full", wrapperClassName),
1076
+ onMouseDown: isMultiSelectEnabled ? void 0 : handleSingleAnchorMouseDown,
1077
+ children: isMultiSelectEnabled ? /* @__PURE__ */ jsxs8(
1078
+ "div",
1079
+ {
1080
+ className: cx7(
1081
+ "w-full min-h-[44px] border-2 rounded-md bg-white px-4 py-2 flex items-center gap-3 cursor-text",
1082
+ "border-border-base hover:border-border-primary-base focus-within:border-border-primary-base focus-within:shadow-focus",
1083
+ disabled && "bg-bg-inactive text-fg-inactive cursor-not-allowed border-border-subtle"
1084
+ ),
1085
+ onClick: handleMultiContainerClick,
1086
+ children: [
1087
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-1 flex-wrap items-center gap-2", children: [
1088
+ selectedChips,
1089
+ /* @__PURE__ */ jsx9(
1090
+ "input",
1091
+ {
1092
+ ref: mergedRef,
1093
+ className: cx7(
1094
+ "flex-1 basis-[140px] min-w-[120px] border-none bg-transparent outline-none text-base leading-tight py-1 pl-5",
1095
+ disabled && "cursor-not-allowed text-fg-inactive"
1096
+ ),
1097
+ value: inputDisplayValue,
1098
+ disabled,
1099
+ readOnly: selectOnly,
1100
+ autoComplete: "off",
1101
+ placeholder: multiSelectedOptions.length === 0 ? placeholder : "",
1102
+ onChange: handleInputChange,
1103
+ onFocus: handleFocus,
1104
+ onBlur: handleBlur,
1105
+ onKeyDown: handleKeyDown,
1106
+ ...comboboxInputProps,
1107
+ ...restInputProps
1108
+ }
1109
+ )
1110
+ ] }),
1111
+ /* @__PURE__ */ jsx9(
1112
+ "button",
1113
+ {
1114
+ type: "button",
1115
+ "aria-label": open ? "Collapse options" : "Expand options",
1116
+ onMouseDown: handleChevronMouseDown,
1117
+ className: "flex items-center justify-center rounded-full text-fg-subtle hover:text-fg-primary-base focus-visible:shadow-focus focus-visible:outline-none transition-colors mr-3",
1118
+ children: /* @__PURE__ */ jsx9(
1119
+ Chevron,
1120
+ {
1121
+ "aria-hidden": "true",
1122
+ className: cx7(
1123
+ "transition-transform duration-200 rotate-90",
1124
+ open && "rotate-270"
1125
+ )
1126
+ }
1127
+ )
1128
+ }
1129
+ )
1130
+ ]
1131
+ }
1132
+ ) : /* @__PURE__ */ jsx9(
1133
+ Input,
1134
+ {
1135
+ ref: mergedRef,
1136
+ placeholder,
1137
+ value: inputDisplayValue,
1138
+ disabled,
1139
+ readOnly: selectOnly,
1140
+ autoComplete: "off",
1141
+ onChange: handleInputChange,
1142
+ onFocus: handleFocus,
1143
+ onBlur: handleBlur,
1144
+ onKeyDown: handleKeyDown,
1145
+ onMouseDown: handleInputMouseDown,
1146
+ endIcon: /* @__PURE__ */ jsx9(
1147
+ "button",
1148
+ {
1149
+ type: "button",
1150
+ "aria-label": open ? "Collapse options" : "Expand options",
1151
+ onMouseDown: handleChevronMouseDown,
1152
+ className: "flex items-center justify-center rounded-full text-fg-subtle hover:text-fg-primary-base focus-visible:shadow-focus focus-visible:outline-none transition-colors",
1153
+ children: /* @__PURE__ */ jsx9(
1154
+ Chevron,
1155
+ {
1156
+ "aria-hidden": "true",
1157
+ className: cx7(
1158
+ "transition-transform duration-200 rotate-90",
1159
+ open && "rotate-270"
1160
+ )
1161
+ }
1162
+ )
1163
+ }
1164
+ ),
1165
+ ...comboboxInputProps,
1166
+ ...restInputProps
1167
+ }
1168
+ )
1169
+ }
1170
+ ) }),
1171
+ /* @__PURE__ */ jsx9(
1172
+ PopoverContent,
1173
+ {
1174
+ side: "bottom",
1175
+ align: "start",
1176
+ sideOffset: 4,
1177
+ avoidCollisions: true,
1178
+ forceMount: true,
1179
+ trapFocus: false,
1180
+ disableOutsidePointerEvents: false,
1181
+ onOpenAutoFocus: (event) => event.preventDefault(),
1182
+ className: cx7(
1183
+ "rounded-md border border-bg-base bg-white shadow-lg p-0 mt-1 max-h-60 overflow-auto",
1184
+ contentClassName
1185
+ ),
1186
+ style: { width: contentWidth || void 0 },
1187
+ "data-testid": "combobox-popover",
1188
+ children: /* @__PURE__ */ jsx9(
1189
+ "ul",
1190
+ {
1191
+ id: listboxId,
1192
+ role: "listbox",
1193
+ "aria-labelledby": labelHTMLForId,
1194
+ className: cx7("py-1", listClassName),
1195
+ ...listboxAriaProps,
1196
+ children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx9(
1197
+ "li",
1198
+ {
1199
+ role: "presentation",
1200
+ className: "px-32 py-8 text-sm text-fg-subtle",
1201
+ children: emptyState
1202
+ }
1203
+ ) : filteredOptions.map((option, index) => {
1204
+ const optionId = `${listboxId}-option-${index}`;
1205
+ const isActive = index === activeIndex;
1206
+ const isSelected = isMultiSelectEnabled ? multiSelectedValues.includes(option.value) : option.value === singleSelectedValue;
1207
+ const mergedClassName = cx7(
1208
+ MENU_ITEM_STYLES,
1209
+ isActive && "bg-bg-subtle text-fg-base",
1210
+ isSelected && "font-medium",
1211
+ option.disabled && "opacity-50 cursor-not-allowed",
1212
+ optionClassName,
1213
+ option.element.props.className
1214
+ );
1215
+ const originalOnClick = option.element.props.onClick;
1216
+ const originalOnMouseDown = option.element.props.onMouseDown;
1217
+ const optionContent = option.element.props.children ?? option.label;
1218
+ const checkboxIndicator = isMultiSelectEnabled && /* @__PURE__ */ jsx9(
1219
+ "span",
1220
+ {
1221
+ "data-slot": "combobox-option-checkbox",
1222
+ className: "pointer-events-none select-none shrink-0",
1223
+ children: /* @__PURE__ */ jsx9(
1224
+ Checkbox,
1225
+ {
1226
+ id: `${optionId}-checkbox`,
1227
+ checked: isSelected,
1228
+ readOnly: true,
1229
+ tabIndex: -1,
1230
+ "aria-hidden": "true",
1231
+ disabled: option.disabled
1232
+ }
1233
+ )
1234
+ }
1235
+ );
1236
+ return React9.cloneElement(option.element, {
1237
+ key: option.element.key ?? option.value ?? optionId,
1238
+ id: optionId,
1239
+ role: "option",
1240
+ "aria-selected": isSelected,
1241
+ "aria-disabled": option.disabled || void 0,
1242
+ tabIndex: -1,
1243
+ className: mergedClassName,
1244
+ onMouseDown: (event) => {
1245
+ event.preventDefault();
1246
+ originalOnMouseDown == null ? void 0 : originalOnMouseDown(event);
1247
+ },
1248
+ onClick: (event) => {
1249
+ originalOnClick == null ? void 0 : originalOnClick(event);
1250
+ if (event.defaultPrevented) return;
1251
+ selectOption(option);
1252
+ },
1253
+ children: /* @__PURE__ */ jsxs8("span", { className: "flex w-full items-center justify-between gap-8", children: [
1254
+ /* @__PURE__ */ jsx9("span", { className: "truncate", children: optionContent }),
1255
+ checkboxIndicator
1256
+ ] })
1257
+ });
1258
+ })
1259
+ }
1260
+ )
1261
+ }
1262
+ )
1263
+ ] });
1264
+ }
1265
+ );
1266
+ ComboboxRoot.displayName = "Combobox";
1267
+ var ComboboxOption = forwardRef5(function ComboboxOption2({ children, label, ...props }, ref) {
1268
+ return /* @__PURE__ */ jsx9("li", { ref, ...props, children: children ?? label });
1269
+ });
1270
+ ComboboxOption.displayName = OPTION_COMPONENT_NAME;
1271
+ ComboboxRoot.Option = ComboboxOption;
1272
+ var Option = ComboboxOption;
344
1273
  export {
1274
+ Checkbox,
1275
+ ComboboxRoot as Combobox,
1276
+ Option as ComboboxOption,
345
1277
  Feedback,
346
1278
  Field,
347
1279
  Input,
348
1280
  Label,
1281
+ Option,
349
1282
  Radio,
1283
+ Switch,
350
1284
  Textarea
351
1285
  };