@madecki/ui 1.3.0 → 1.4.0

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.cjs CHANGED
@@ -372,6 +372,375 @@ var Input = ({
372
372
  ] })
373
373
  ] }) });
374
374
  };
375
+ function optionTestSlug(value) {
376
+ return value.replace(/[^a-zA-Z0-9_-]/g, "_");
377
+ }
378
+ function ChevronDown({ className = "" }) {
379
+ return /* @__PURE__ */ jsxRuntime.jsx(
380
+ "svg",
381
+ {
382
+ width: 20,
383
+ height: 20,
384
+ viewBox: "0 0 20 20",
385
+ fill: "none",
386
+ xmlns: "http://www.w3.org/2000/svg",
387
+ className,
388
+ "aria-hidden": true,
389
+ children: /* @__PURE__ */ jsxRuntime.jsx(
390
+ "path",
391
+ {
392
+ d: "M5 7.5L10 12.5L15 7.5",
393
+ stroke: "currentColor",
394
+ strokeWidth: "1.5",
395
+ strokeLinecap: "round",
396
+ strokeLinejoin: "round"
397
+ }
398
+ )
399
+ }
400
+ );
401
+ }
402
+ function buildVariantClasses(variant, isFocused, disabled) {
403
+ const inputClassNames = [
404
+ "min-w-0 flex-1 rounded-none border-0 font-sans",
405
+ "py-4 pl-5 pr-2",
406
+ "outline-hidden",
407
+ "bg-transparent shadow-none"
408
+ ];
409
+ const inputWrapperClassNames = [
410
+ "flex min-w-0 w-full items-stretch rounded-smb p-px"
411
+ ];
412
+ if (isFocused) {
413
+ inputWrapperClassNames.push("bg-gradient");
414
+ } else if (variant === "primary" || variant === "tertiary") {
415
+ inputWrapperClassNames.push("bg-lightgray");
416
+ }
417
+ const innerFieldClassNames = [
418
+ "flex min-h-0 min-w-0 flex-1 overflow-hidden rounded-sm"
419
+ ];
420
+ const chevronClassNames = [
421
+ "pointer-events-none flex shrink-0 items-center justify-center self-stretch pl-1 pr-4"
422
+ ];
423
+ switch (variant) {
424
+ case "primary":
425
+ inputClassNames.push("text-primary placeholder:text-lightgray");
426
+ innerFieldClassNames.push("bg-neutral");
427
+ chevronClassNames.push("text-primary");
428
+ break;
429
+ case "secondary":
430
+ inputClassNames.push(
431
+ "text-neutral placeholder:text-lightgray dark:placeholder:text-icongray"
432
+ );
433
+ innerFieldClassNames.push("bg-neutral dark:bg-gray");
434
+ chevronClassNames.push("text-neutral");
435
+ break;
436
+ case "tertiary":
437
+ inputClassNames.push(
438
+ "text-neutral placeholder:text-lightgray dark:placeholder:text-icongray"
439
+ );
440
+ innerFieldClassNames.push("bg-neutral dark:bg-primary");
441
+ chevronClassNames.push("text-neutral dark:text-icongray");
442
+ break;
443
+ }
444
+ if (disabled) {
445
+ inputClassNames.push("cursor-not-allowed opacity-50");
446
+ chevronClassNames.push("opacity-50");
447
+ }
448
+ return {
449
+ inputClassNames,
450
+ inputWrapperClassNames,
451
+ innerFieldClassNames,
452
+ chevronClassNames
453
+ };
454
+ }
455
+ function Select(props) {
456
+ const {
457
+ name,
458
+ label,
459
+ options,
460
+ placeholder = "Select\u2026",
461
+ variant = "primary",
462
+ disabled = false,
463
+ className = "",
464
+ testId: testIdProp
465
+ } = props;
466
+ const isMulti = props.multi === true;
467
+ const singleValueProp = !isMulti ? props.value : void 0;
468
+ const multiValueProp = isMulti ? props.value : void 0;
469
+ const isControlled = isMulti ? multiValueProp !== void 0 : singleValueProp !== void 0;
470
+ const reactId = react.useId();
471
+ const listboxId = `${name}-listbox-${reactId.replace(/:/g, "")}`;
472
+ const baseTestId = testIdProp ?? `select-${name}`;
473
+ const [internalSingle, setInternalSingle] = react.useState(
474
+ !isMulti && !isControlled ? props.defaultValue ?? "" : ""
475
+ );
476
+ const [internalMulti, setInternalMulti] = react.useState(
477
+ isMulti && !isControlled ? [...props.defaultValue ?? []] : []
478
+ );
479
+ const selectedSingle = react.useMemo(() => {
480
+ if (isMulti) return "";
481
+ if (singleValueProp !== void 0) return singleValueProp;
482
+ return internalSingle;
483
+ }, [isMulti, singleValueProp, internalSingle]);
484
+ const selectedMulti = react.useMemo(() => {
485
+ if (!isMulti) return [];
486
+ if (multiValueProp !== void 0) return multiValueProp;
487
+ return internalMulti;
488
+ }, [isMulti, multiValueProp, internalMulti]);
489
+ const [open, setOpen] = react.useState(false);
490
+ const [filter, setFilter] = react.useState("");
491
+ const [highlightIndex, setHighlightIndex] = react.useState(0);
492
+ const [isFocused, setIsFocused] = react.useState(false);
493
+ const containerRef = react.useRef(null);
494
+ const listRef = react.useRef(null);
495
+ const inputRef = react.useRef(null);
496
+ const optionByValue = react.useMemo(() => {
497
+ const m = /* @__PURE__ */ new Map();
498
+ for (const o of options) m.set(o.value, o);
499
+ return m;
500
+ }, [options]);
501
+ const filteredOptions = react.useMemo(() => {
502
+ const q = filter.trim().toLowerCase();
503
+ if (!q) return options;
504
+ return options.filter(
505
+ (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q)
506
+ );
507
+ }, [options, filter]);
508
+ const closedDisplay = react.useMemo(() => {
509
+ if (isMulti) {
510
+ if (selectedMulti.length === 0) return "";
511
+ return selectedMulti.map((v) => optionByValue.get(v)?.label ?? v).join(", ");
512
+ }
513
+ if (!selectedSingle) return "";
514
+ return optionByValue.get(selectedSingle)?.label ?? selectedSingle;
515
+ }, [isMulti, selectedMulti, selectedSingle, optionByValue]);
516
+ const inputValue = open ? filter : closedDisplay;
517
+ const onSingleChange = !isMulti ? props.onChange : void 0;
518
+ const onMultiChange = isMulti ? props.onChange : void 0;
519
+ const setSingle = react.useCallback(
520
+ (next) => {
521
+ if (!isMulti) {
522
+ if (!isControlled) setInternalSingle(next);
523
+ onSingleChange?.(next);
524
+ }
525
+ },
526
+ [isMulti, isControlled, onSingleChange]
527
+ );
528
+ const setMulti = react.useCallback(
529
+ (next) => {
530
+ if (isMulti) {
531
+ if (!isControlled) setInternalMulti(next);
532
+ onMultiChange?.(next);
533
+ }
534
+ },
535
+ [isMulti, isControlled, onMultiChange]
536
+ );
537
+ const close = react.useCallback(() => {
538
+ setOpen(false);
539
+ setFilter("");
540
+ setHighlightIndex(0);
541
+ }, []);
542
+ const openMenu = react.useCallback(() => {
543
+ if (disabled) return;
544
+ setOpen(true);
545
+ setFilter("");
546
+ setHighlightIndex(0);
547
+ }, [disabled]);
548
+ react.useEffect(() => {
549
+ if (!open) return;
550
+ const max = Math.max(0, filteredOptions.length - 1);
551
+ setHighlightIndex((i) => Math.min(i, max));
552
+ }, [open, filteredOptions.length]);
553
+ react.useEffect(() => {
554
+ if (!open) return;
555
+ const onDocMouseDown = (e) => {
556
+ const el = containerRef.current;
557
+ if (el && !el.contains(e.target)) close();
558
+ };
559
+ document.addEventListener("mousedown", onDocMouseDown);
560
+ return () => document.removeEventListener("mousedown", onDocMouseDown);
561
+ }, [open, close]);
562
+ const commitHighlight = react.useCallback(() => {
563
+ const opt = filteredOptions[highlightIndex];
564
+ if (!opt) return;
565
+ if (isMulti) {
566
+ const set = new Set(selectedMulti);
567
+ if (set.has(opt.value)) set.delete(opt.value);
568
+ else set.add(opt.value);
569
+ setMulti([...set]);
570
+ } else {
571
+ setSingle(opt.value);
572
+ close();
573
+ inputRef.current?.blur();
574
+ }
575
+ }, [
576
+ filteredOptions,
577
+ highlightIndex,
578
+ isMulti,
579
+ selectedMulti,
580
+ setMulti,
581
+ setSingle,
582
+ close
583
+ ]);
584
+ const onOptionMouseDown = (e) => {
585
+ e.preventDefault();
586
+ };
587
+ const onOptionClick = (opt) => {
588
+ if (isMulti) {
589
+ const set = new Set(selectedMulti);
590
+ if (set.has(opt.value)) set.delete(opt.value);
591
+ else set.add(opt.value);
592
+ setMulti([...set]);
593
+ } else {
594
+ setSingle(opt.value);
595
+ close();
596
+ }
597
+ };
598
+ const onInputFocus = () => {
599
+ setIsFocused(true);
600
+ if (!disabled) setOpen(true);
601
+ };
602
+ const onInputBlur = (e) => {
603
+ const next = e.relatedTarget;
604
+ if (listRef.current?.contains(next)) return;
605
+ setIsFocused(false);
606
+ close();
607
+ };
608
+ const onInputChange = (v) => {
609
+ if (!open) setOpen(true);
610
+ setFilter(v);
611
+ setHighlightIndex(0);
612
+ };
613
+ const onKeyDown = (e) => {
614
+ if (disabled) return;
615
+ if (e.key === "Escape") {
616
+ e.preventDefault();
617
+ close();
618
+ inputRef.current?.blur();
619
+ return;
620
+ }
621
+ if (e.key === "ArrowDown") {
622
+ e.preventDefault();
623
+ if (!open) openMenu();
624
+ else
625
+ setHighlightIndex(
626
+ (i) => Math.min(i + 1, Math.max(0, filteredOptions.length - 1))
627
+ );
628
+ return;
629
+ }
630
+ if (e.key === "ArrowUp") {
631
+ e.preventDefault();
632
+ if (!open) openMenu();
633
+ else setHighlightIndex((i) => Math.max(0, i - 1));
634
+ return;
635
+ }
636
+ if (e.key === "Enter" && open) {
637
+ e.preventDefault();
638
+ commitHighlight();
639
+ return;
640
+ }
641
+ if (e.key === "Home" && open) {
642
+ e.preventDefault();
643
+ setHighlightIndex(0);
644
+ return;
645
+ }
646
+ if (e.key === "End" && open) {
647
+ e.preventDefault();
648
+ setHighlightIndex(Math.max(0, filteredOptions.length - 1));
649
+ return;
650
+ }
651
+ };
652
+ const {
653
+ inputClassNames,
654
+ inputWrapperClassNames,
655
+ innerFieldClassNames,
656
+ chevronClassNames
657
+ } = buildVariantClasses(variant, isFocused, disabled);
658
+ const activeDescendant = open && filteredOptions[highlightIndex] ? `${name}-option-${optionTestSlug(filteredOptions[highlightIndex].value)}` : void 0;
659
+ const listboxClass = "absolute left-0 right-0 top-full z-50 mt-1 max-h-60 overflow-auto rounded-sm border border-lightgray bg-neutral py-1 shadow-lg dark:border-gray dark:bg-gray dark:text-white";
660
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: `relative ${className}`.trim(), children: [
661
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, children: [
662
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: label }),
663
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: inputWrapperClassNames.join(" "), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: innerFieldClassNames.join(" "), children: [
664
+ /* @__PURE__ */ jsxRuntime.jsx(
665
+ "input",
666
+ {
667
+ ref: inputRef,
668
+ id: name,
669
+ name,
670
+ type: "text",
671
+ autoComplete: "off",
672
+ spellCheck: false,
673
+ disabled,
674
+ placeholder,
675
+ value: inputValue,
676
+ "aria-label": label,
677
+ "aria-expanded": open,
678
+ "aria-haspopup": "listbox",
679
+ "aria-controls": listboxId,
680
+ "aria-autocomplete": "list",
681
+ "aria-activedescendant": activeDescendant,
682
+ role: "combobox",
683
+ "data-testid": baseTestId,
684
+ className: inputClassNames.join(" "),
685
+ onChange: (e) => onInputChange(e.target.value),
686
+ onFocus: onInputFocus,
687
+ onBlur: onInputBlur,
688
+ onKeyDown
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: chevronClassNames.join(" "), "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, { className: "block shrink-0" }) })
692
+ ] }) })
693
+ ] }),
694
+ open && /* @__PURE__ */ jsxRuntime.jsx(
695
+ "ul",
696
+ {
697
+ ref: listRef,
698
+ id: listboxId,
699
+ role: "listbox",
700
+ "aria-multiselectable": isMulti,
701
+ "aria-label": label,
702
+ "data-testid": `${baseTestId}-listbox`,
703
+ tabIndex: -1,
704
+ className: listboxClass,
705
+ children: filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
706
+ "li",
707
+ {
708
+ className: "px-5 py-3 text-sm text-lightgray dark:text-icongray",
709
+ role: "presentation",
710
+ children: "No matches"
711
+ }
712
+ ) : filteredOptions.map((opt, index) => {
713
+ const selected = isMulti ? selectedMulti.includes(opt.value) : selectedSingle === opt.value;
714
+ const highlighted = index === highlightIndex;
715
+ const oid = `${name}-option-${optionTestSlug(opt.value)}`;
716
+ return /* @__PURE__ */ jsxRuntime.jsxs(
717
+ "li",
718
+ {
719
+ id: oid,
720
+ role: "option",
721
+ "aria-selected": selected,
722
+ "data-testid": `${baseTestId}-option-${optionTestSlug(opt.value)}`,
723
+ "data-option-value": opt.value,
724
+ className: [
725
+ "cursor-pointer px-5 py-3 text-sm text-primary dark:text-white",
726
+ highlighted ? "bg-lightgray/50 dark:bg-white/10" : "",
727
+ selected ? "font-semibold" : ""
728
+ ].filter(Boolean).join(" "),
729
+ onMouseDown: onOptionMouseDown,
730
+ onMouseEnter: () => setHighlightIndex(index),
731
+ onClick: () => onOptionClick(opt),
732
+ children: [
733
+ isMulti && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2 inline-block w-4 text-center", "aria-hidden": true, children: selected ? "\u2713" : "" }),
734
+ opt.label
735
+ ]
736
+ },
737
+ opt.value
738
+ );
739
+ })
740
+ }
741
+ )
742
+ ] });
743
+ }
375
744
  var Tabs = ({ tabs, onTabClick, className = "" }) => {
376
745
  const [activeTab, setActiveTab] = react.useState(
377
746
  tabs.find((tab) => tab.isActive)?.value
@@ -1066,6 +1435,7 @@ exports.InstagramIcon = InstagramIcon;
1066
1435
  exports.LinkedInIcon = LinkedInIcon;
1067
1436
  exports.RadioButtons = RadioButtons;
1068
1437
  exports.Search = Search;
1438
+ exports.Select = Select;
1069
1439
  exports.Share = Share;
1070
1440
  exports.Spinner = Spinner;
1071
1441
  exports.SpinnerOverlay = SpinnerOverlay;