@madecki/ui 1.2.1 → 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.js CHANGED
@@ -1,21 +1,15 @@
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
- var Button = ({
4
+ // src/components/Tag/tagSurfaceClassNames.ts
5
+ function getTagSurfaceClassNames({
5
6
  variant,
6
7
  size = "md",
7
- children,
8
- onClick,
9
- isActive,
10
- id,
11
- label,
12
- disabled,
13
8
  className = "",
14
- type = "button"
15
- }) => {
16
- if (typeof isActive === "boolean" && id === void 0) {
17
- throw Error("If button has isActive props, it must have id props too");
18
- }
9
+ filled = false,
10
+ muted = false,
11
+ interactive = false
12
+ }) {
19
13
  const classNames = [
20
14
  "relative flex gap-2 items-center rounded-sm font-sans border-2 transition-colors leading-none"
21
15
  ];
@@ -29,42 +23,42 @@ var Button = ({
29
23
  info: "bg-info",
30
24
  blue: "bg-blue"
31
25
  };
32
- const bg = isActive === true ? activeBgMap[variant] : "bg-neutral";
26
+ const bg = filled === true ? activeBgMap[variant] : "bg-neutral";
33
27
  switch (variant) {
34
28
  case "primary":
35
29
  classNames.push(
36
30
  `border-primary text-primary ${bg} focus:bg-primary`,
37
- isActive === true ? "dark:bg-neutral dark:text-primary" : "dark:text-white dark:bg-gray dark:hover:bg-neutral dark:hover:text-primary"
31
+ filled === true ? "dark:bg-neutral dark:text-primary" : interactive ? "dark:text-white dark:bg-gray dark:hover:bg-neutral dark:hover:text-primary" : "dark:text-white dark:bg-gray"
38
32
  );
39
33
  break;
40
34
  case "success":
41
35
  classNames.push(
42
36
  `border-success text-primary ${bg} focus:bg-primary focus:text-success`,
43
- isActive === true ? "dark:bg-success" : "dark:text-white dark:bg-gray dark:hover:bg-success"
37
+ filled === true ? "dark:bg-success" : interactive ? "dark:text-white dark:bg-gray dark:hover:bg-success" : "dark:text-white dark:bg-gray"
44
38
  );
45
39
  break;
46
40
  case "warning":
47
41
  classNames.push(
48
42
  `border-warning text-primary ${bg} focus:bg-primary focus:text-warning`,
49
- isActive === true ? "dark:bg-warning" : "dark:text-white dark:bg-gray dark:hover:bg-warning"
43
+ filled === true ? "dark:bg-warning" : interactive ? "dark:text-white dark:bg-gray dark:hover:bg-warning" : "dark:text-white dark:bg-gray"
50
44
  );
51
45
  break;
52
46
  case "danger":
53
47
  classNames.push(
54
48
  `border-danger text-primary ${bg} focus:bg-primary focus:text-danger`,
55
- isActive === true ? "dark:bg-danger" : "dark:text-white dark:bg-gray dark:hover:bg-danger"
49
+ filled === true ? "dark:bg-danger" : interactive ? "dark:text-white dark:bg-gray dark:hover:bg-danger" : "dark:text-white dark:bg-gray"
56
50
  );
57
51
  break;
58
52
  case "info":
59
53
  classNames.push(
60
54
  `border-info text-primary ${bg} focus:bg-primary focus:text-info`,
61
- isActive === true ? "dark:bg-info" : "dark:text-white dark:bg-gray dark:hover:bg-info"
55
+ filled === true ? "dark:bg-info" : interactive ? "dark:text-white dark:bg-gray dark:hover:bg-info" : "dark:text-white dark:bg-gray"
62
56
  );
63
57
  break;
64
58
  case "blue":
65
59
  classNames.push(
66
60
  `border-blue text-primary ${bg} focus:bg-primary focus:text-blue`,
67
- isActive === true ? "dark:bg-blue" : "dark:text-white dark:bg-gray dark:hover:bg-blue"
61
+ filled === true ? "dark:bg-blue" : interactive ? "dark:text-white dark:bg-gray dark:hover:bg-blue" : "dark:text-white dark:bg-gray"
68
62
  );
69
63
  break;
70
64
  }
@@ -82,19 +76,47 @@ var Button = ({
82
76
  classNames.push("text-lg py-5 px-8");
83
77
  break;
84
78
  }
85
- if (disabled) {
79
+ if (muted) {
86
80
  classNames.push(
87
- "cursor-not-allowed hover:bg-neutral dark:hover:bg-neutral opacity-50"
81
+ interactive ? "cursor-not-allowed hover:bg-neutral dark:hover:bg-neutral opacity-50" : "cursor-default opacity-50"
88
82
  );
89
83
  }
84
+ if (interactive && !muted) {
85
+ classNames.push("cursor-pointer");
86
+ }
90
87
  if (className) {
91
88
  classNames.push(className);
92
89
  }
90
+ return classNames.join(" ");
91
+ }
92
+ var Button = ({
93
+ variant,
94
+ size = "md",
95
+ children,
96
+ onClick,
97
+ isActive,
98
+ id,
99
+ label,
100
+ disabled,
101
+ className = "",
102
+ type = "button"
103
+ }) => {
104
+ if (typeof isActive === "boolean" && id === void 0) {
105
+ throw Error("If button has isActive props, it must have id props too");
106
+ }
107
+ const surfaceClassName = getTagSurfaceClassNames({
108
+ variant,
109
+ size,
110
+ className,
111
+ filled: isActive === true,
112
+ muted: Boolean(disabled),
113
+ interactive: true
114
+ });
93
115
  return /* @__PURE__ */ jsxs(
94
116
  "button",
95
117
  {
96
118
  type,
97
- className: classNames.join(" "),
119
+ className: surfaceClassName,
98
120
  onClick: () => {
99
121
  if (isActive === true) {
100
122
  onClick?.();
@@ -252,6 +274,29 @@ var RadioButtons = ({
252
274
  }
253
275
  )) });
254
276
  };
277
+ var Tag = ({
278
+ variant,
279
+ size = "md",
280
+ children,
281
+ label,
282
+ className = "",
283
+ filled = false,
284
+ muted = false
285
+ }) => {
286
+ return /* @__PURE__ */ jsx(
287
+ "span",
288
+ {
289
+ className: getTagSurfaceClassNames({
290
+ variant,
291
+ size,
292
+ className,
293
+ filled,
294
+ muted
295
+ }),
296
+ children: children || label
297
+ }
298
+ );
299
+ };
255
300
  var Input = ({
256
301
  name,
257
302
  onChange,
@@ -325,6 +370,375 @@ var Input = ({
325
370
  ] })
326
371
  ] }) });
327
372
  };
373
+ function optionTestSlug(value) {
374
+ return value.replace(/[^a-zA-Z0-9_-]/g, "_");
375
+ }
376
+ function ChevronDown({ className = "" }) {
377
+ return /* @__PURE__ */ jsx(
378
+ "svg",
379
+ {
380
+ width: 20,
381
+ height: 20,
382
+ viewBox: "0 0 20 20",
383
+ fill: "none",
384
+ xmlns: "http://www.w3.org/2000/svg",
385
+ className,
386
+ "aria-hidden": true,
387
+ children: /* @__PURE__ */ jsx(
388
+ "path",
389
+ {
390
+ d: "M5 7.5L10 12.5L15 7.5",
391
+ stroke: "currentColor",
392
+ strokeWidth: "1.5",
393
+ strokeLinecap: "round",
394
+ strokeLinejoin: "round"
395
+ }
396
+ )
397
+ }
398
+ );
399
+ }
400
+ function buildVariantClasses(variant, isFocused, disabled) {
401
+ const inputClassNames = [
402
+ "min-w-0 flex-1 rounded-none border-0 font-sans",
403
+ "py-4 pl-5 pr-2",
404
+ "outline-hidden",
405
+ "bg-transparent shadow-none"
406
+ ];
407
+ const inputWrapperClassNames = [
408
+ "flex min-w-0 w-full items-stretch rounded-smb p-px"
409
+ ];
410
+ if (isFocused) {
411
+ inputWrapperClassNames.push("bg-gradient");
412
+ } else if (variant === "primary" || variant === "tertiary") {
413
+ inputWrapperClassNames.push("bg-lightgray");
414
+ }
415
+ const innerFieldClassNames = [
416
+ "flex min-h-0 min-w-0 flex-1 overflow-hidden rounded-sm"
417
+ ];
418
+ const chevronClassNames = [
419
+ "pointer-events-none flex shrink-0 items-center justify-center self-stretch pl-1 pr-4"
420
+ ];
421
+ switch (variant) {
422
+ case "primary":
423
+ inputClassNames.push("text-primary placeholder:text-lightgray");
424
+ innerFieldClassNames.push("bg-neutral");
425
+ chevronClassNames.push("text-primary");
426
+ break;
427
+ case "secondary":
428
+ inputClassNames.push(
429
+ "text-neutral placeholder:text-lightgray dark:placeholder:text-icongray"
430
+ );
431
+ innerFieldClassNames.push("bg-neutral dark:bg-gray");
432
+ chevronClassNames.push("text-neutral");
433
+ break;
434
+ case "tertiary":
435
+ inputClassNames.push(
436
+ "text-neutral placeholder:text-lightgray dark:placeholder:text-icongray"
437
+ );
438
+ innerFieldClassNames.push("bg-neutral dark:bg-primary");
439
+ chevronClassNames.push("text-neutral dark:text-icongray");
440
+ break;
441
+ }
442
+ if (disabled) {
443
+ inputClassNames.push("cursor-not-allowed opacity-50");
444
+ chevronClassNames.push("opacity-50");
445
+ }
446
+ return {
447
+ inputClassNames,
448
+ inputWrapperClassNames,
449
+ innerFieldClassNames,
450
+ chevronClassNames
451
+ };
452
+ }
453
+ function Select(props) {
454
+ const {
455
+ name,
456
+ label,
457
+ options,
458
+ placeholder = "Select\u2026",
459
+ variant = "primary",
460
+ disabled = false,
461
+ className = "",
462
+ testId: testIdProp
463
+ } = props;
464
+ const isMulti = props.multi === true;
465
+ const singleValueProp = !isMulti ? props.value : void 0;
466
+ const multiValueProp = isMulti ? props.value : void 0;
467
+ const isControlled = isMulti ? multiValueProp !== void 0 : singleValueProp !== void 0;
468
+ const reactId = useId();
469
+ const listboxId = `${name}-listbox-${reactId.replace(/:/g, "")}`;
470
+ const baseTestId = testIdProp ?? `select-${name}`;
471
+ const [internalSingle, setInternalSingle] = useState(
472
+ !isMulti && !isControlled ? props.defaultValue ?? "" : ""
473
+ );
474
+ const [internalMulti, setInternalMulti] = useState(
475
+ isMulti && !isControlled ? [...props.defaultValue ?? []] : []
476
+ );
477
+ const selectedSingle = useMemo(() => {
478
+ if (isMulti) return "";
479
+ if (singleValueProp !== void 0) return singleValueProp;
480
+ return internalSingle;
481
+ }, [isMulti, singleValueProp, internalSingle]);
482
+ const selectedMulti = useMemo(() => {
483
+ if (!isMulti) return [];
484
+ if (multiValueProp !== void 0) return multiValueProp;
485
+ return internalMulti;
486
+ }, [isMulti, multiValueProp, internalMulti]);
487
+ const [open, setOpen] = useState(false);
488
+ const [filter, setFilter] = useState("");
489
+ const [highlightIndex, setHighlightIndex] = useState(0);
490
+ const [isFocused, setIsFocused] = useState(false);
491
+ const containerRef = useRef(null);
492
+ const listRef = useRef(null);
493
+ const inputRef = useRef(null);
494
+ const optionByValue = useMemo(() => {
495
+ const m = /* @__PURE__ */ new Map();
496
+ for (const o of options) m.set(o.value, o);
497
+ return m;
498
+ }, [options]);
499
+ const filteredOptions = useMemo(() => {
500
+ const q = filter.trim().toLowerCase();
501
+ if (!q) return options;
502
+ return options.filter(
503
+ (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q)
504
+ );
505
+ }, [options, filter]);
506
+ const closedDisplay = useMemo(() => {
507
+ if (isMulti) {
508
+ if (selectedMulti.length === 0) return "";
509
+ return selectedMulti.map((v) => optionByValue.get(v)?.label ?? v).join(", ");
510
+ }
511
+ if (!selectedSingle) return "";
512
+ return optionByValue.get(selectedSingle)?.label ?? selectedSingle;
513
+ }, [isMulti, selectedMulti, selectedSingle, optionByValue]);
514
+ const inputValue = open ? filter : closedDisplay;
515
+ const onSingleChange = !isMulti ? props.onChange : void 0;
516
+ const onMultiChange = isMulti ? props.onChange : void 0;
517
+ const setSingle = useCallback(
518
+ (next) => {
519
+ if (!isMulti) {
520
+ if (!isControlled) setInternalSingle(next);
521
+ onSingleChange?.(next);
522
+ }
523
+ },
524
+ [isMulti, isControlled, onSingleChange]
525
+ );
526
+ const setMulti = useCallback(
527
+ (next) => {
528
+ if (isMulti) {
529
+ if (!isControlled) setInternalMulti(next);
530
+ onMultiChange?.(next);
531
+ }
532
+ },
533
+ [isMulti, isControlled, onMultiChange]
534
+ );
535
+ const close = useCallback(() => {
536
+ setOpen(false);
537
+ setFilter("");
538
+ setHighlightIndex(0);
539
+ }, []);
540
+ const openMenu = useCallback(() => {
541
+ if (disabled) return;
542
+ setOpen(true);
543
+ setFilter("");
544
+ setHighlightIndex(0);
545
+ }, [disabled]);
546
+ useEffect(() => {
547
+ if (!open) return;
548
+ const max = Math.max(0, filteredOptions.length - 1);
549
+ setHighlightIndex((i) => Math.min(i, max));
550
+ }, [open, filteredOptions.length]);
551
+ useEffect(() => {
552
+ if (!open) return;
553
+ const onDocMouseDown = (e) => {
554
+ const el = containerRef.current;
555
+ if (el && !el.contains(e.target)) close();
556
+ };
557
+ document.addEventListener("mousedown", onDocMouseDown);
558
+ return () => document.removeEventListener("mousedown", onDocMouseDown);
559
+ }, [open, close]);
560
+ const commitHighlight = useCallback(() => {
561
+ const opt = filteredOptions[highlightIndex];
562
+ if (!opt) return;
563
+ if (isMulti) {
564
+ const set = new Set(selectedMulti);
565
+ if (set.has(opt.value)) set.delete(opt.value);
566
+ else set.add(opt.value);
567
+ setMulti([...set]);
568
+ } else {
569
+ setSingle(opt.value);
570
+ close();
571
+ inputRef.current?.blur();
572
+ }
573
+ }, [
574
+ filteredOptions,
575
+ highlightIndex,
576
+ isMulti,
577
+ selectedMulti,
578
+ setMulti,
579
+ setSingle,
580
+ close
581
+ ]);
582
+ const onOptionMouseDown = (e) => {
583
+ e.preventDefault();
584
+ };
585
+ const onOptionClick = (opt) => {
586
+ if (isMulti) {
587
+ const set = new Set(selectedMulti);
588
+ if (set.has(opt.value)) set.delete(opt.value);
589
+ else set.add(opt.value);
590
+ setMulti([...set]);
591
+ } else {
592
+ setSingle(opt.value);
593
+ close();
594
+ }
595
+ };
596
+ const onInputFocus = () => {
597
+ setIsFocused(true);
598
+ if (!disabled) setOpen(true);
599
+ };
600
+ const onInputBlur = (e) => {
601
+ const next = e.relatedTarget;
602
+ if (listRef.current?.contains(next)) return;
603
+ setIsFocused(false);
604
+ close();
605
+ };
606
+ const onInputChange = (v) => {
607
+ if (!open) setOpen(true);
608
+ setFilter(v);
609
+ setHighlightIndex(0);
610
+ };
611
+ const onKeyDown = (e) => {
612
+ if (disabled) return;
613
+ if (e.key === "Escape") {
614
+ e.preventDefault();
615
+ close();
616
+ inputRef.current?.blur();
617
+ return;
618
+ }
619
+ if (e.key === "ArrowDown") {
620
+ e.preventDefault();
621
+ if (!open) openMenu();
622
+ else
623
+ setHighlightIndex(
624
+ (i) => Math.min(i + 1, Math.max(0, filteredOptions.length - 1))
625
+ );
626
+ return;
627
+ }
628
+ if (e.key === "ArrowUp") {
629
+ e.preventDefault();
630
+ if (!open) openMenu();
631
+ else setHighlightIndex((i) => Math.max(0, i - 1));
632
+ return;
633
+ }
634
+ if (e.key === "Enter" && open) {
635
+ e.preventDefault();
636
+ commitHighlight();
637
+ return;
638
+ }
639
+ if (e.key === "Home" && open) {
640
+ e.preventDefault();
641
+ setHighlightIndex(0);
642
+ return;
643
+ }
644
+ if (e.key === "End" && open) {
645
+ e.preventDefault();
646
+ setHighlightIndex(Math.max(0, filteredOptions.length - 1));
647
+ return;
648
+ }
649
+ };
650
+ const {
651
+ inputClassNames,
652
+ inputWrapperClassNames,
653
+ innerFieldClassNames,
654
+ chevronClassNames
655
+ } = buildVariantClasses(variant, isFocused, disabled);
656
+ const activeDescendant = open && filteredOptions[highlightIndex] ? `${name}-option-${optionTestSlug(filteredOptions[highlightIndex].value)}` : void 0;
657
+ 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";
658
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `relative ${className}`.trim(), children: [
659
+ /* @__PURE__ */ jsxs("label", { htmlFor: name, children: [
660
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
661
+ /* @__PURE__ */ jsx("div", { className: inputWrapperClassNames.join(" "), children: /* @__PURE__ */ jsxs("div", { className: innerFieldClassNames.join(" "), children: [
662
+ /* @__PURE__ */ jsx(
663
+ "input",
664
+ {
665
+ ref: inputRef,
666
+ id: name,
667
+ name,
668
+ type: "text",
669
+ autoComplete: "off",
670
+ spellCheck: false,
671
+ disabled,
672
+ placeholder,
673
+ value: inputValue,
674
+ "aria-label": label,
675
+ "aria-expanded": open,
676
+ "aria-haspopup": "listbox",
677
+ "aria-controls": listboxId,
678
+ "aria-autocomplete": "list",
679
+ "aria-activedescendant": activeDescendant,
680
+ role: "combobox",
681
+ "data-testid": baseTestId,
682
+ className: inputClassNames.join(" "),
683
+ onChange: (e) => onInputChange(e.target.value),
684
+ onFocus: onInputFocus,
685
+ onBlur: onInputBlur,
686
+ onKeyDown
687
+ }
688
+ ),
689
+ /* @__PURE__ */ jsx("div", { className: chevronClassNames.join(" "), "aria-hidden": true, children: /* @__PURE__ */ jsx(ChevronDown, { className: "block shrink-0" }) })
690
+ ] }) })
691
+ ] }),
692
+ open && /* @__PURE__ */ jsx(
693
+ "ul",
694
+ {
695
+ ref: listRef,
696
+ id: listboxId,
697
+ role: "listbox",
698
+ "aria-multiselectable": isMulti,
699
+ "aria-label": label,
700
+ "data-testid": `${baseTestId}-listbox`,
701
+ tabIndex: -1,
702
+ className: listboxClass,
703
+ children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx(
704
+ "li",
705
+ {
706
+ className: "px-5 py-3 text-sm text-lightgray dark:text-icongray",
707
+ role: "presentation",
708
+ children: "No matches"
709
+ }
710
+ ) : filteredOptions.map((opt, index) => {
711
+ const selected = isMulti ? selectedMulti.includes(opt.value) : selectedSingle === opt.value;
712
+ const highlighted = index === highlightIndex;
713
+ const oid = `${name}-option-${optionTestSlug(opt.value)}`;
714
+ return /* @__PURE__ */ jsxs(
715
+ "li",
716
+ {
717
+ id: oid,
718
+ role: "option",
719
+ "aria-selected": selected,
720
+ "data-testid": `${baseTestId}-option-${optionTestSlug(opt.value)}`,
721
+ "data-option-value": opt.value,
722
+ className: [
723
+ "cursor-pointer px-5 py-3 text-sm text-primary dark:text-white",
724
+ highlighted ? "bg-lightgray/50 dark:bg-white/10" : "",
725
+ selected ? "font-semibold" : ""
726
+ ].filter(Boolean).join(" "),
727
+ onMouseDown: onOptionMouseDown,
728
+ onMouseEnter: () => setHighlightIndex(index),
729
+ onClick: () => onOptionClick(opt),
730
+ children: [
731
+ isMulti && /* @__PURE__ */ jsx("span", { className: "mr-2 inline-block w-4 text-center", "aria-hidden": true, children: selected ? "\u2713" : "" }),
732
+ opt.label
733
+ ]
734
+ },
735
+ opt.value
736
+ );
737
+ })
738
+ }
739
+ )
740
+ ] });
741
+ }
328
742
  var Tabs = ({ tabs, onTabClick, className = "" }) => {
329
743
  const [activeTab, setActiveTab] = useState(
330
744
  tabs.find((tab) => tab.isActive)?.value
@@ -621,11 +1035,11 @@ var Text = ({
621
1035
  size = "md",
622
1036
  weight = "normal",
623
1037
  color = "default",
624
- as: Tag = "p",
1038
+ as: Tag2 = "p",
625
1039
  className = ""
626
1040
  }) => {
627
1041
  return /* @__PURE__ */ jsx(
628
- Tag,
1042
+ Tag2,
629
1043
  {
630
1044
  className: `${sizeStyles3[size]} ${weightStyles2[weight]} ${colorStyles2[color]} ${className}`,
631
1045
  children
@@ -1002,6 +1416,6 @@ var InstagramIcon = ({
1002
1416
  return icon;
1003
1417
  };
1004
1418
 
1005
- export { BlockQuote, Button, ButtonTransparent, Container, ContentBox, GradientButton, Grid, GridItem, Heading, Heart, Hr, Info, Input, InstagramIcon, LinkedInIcon, RadioButtons, Search, Share, Spinner, SpinnerOverlay, Stack, Tabs, Text, TwitterIcon, Warning };
1419
+ 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 };
1006
1420
  //# sourceMappingURL=index.js.map
1007
1421
  //# sourceMappingURL=index.js.map