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