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