@usefidel/design-system 0.13.0 → 0.15.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
@@ -571,11 +571,352 @@ var Toggle = forwardRef2(function Toggle2({ size = "medium", label, description,
571
571
  ] })
572
572
  ] });
573
573
  });
574
+
575
+ // src/components/AppNav/AppNav.tsx
576
+ import { useEffect as useEffect2, useId as useId3, useRef as useRef2, useState as useState4 } from "react";
577
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
578
+ function AppNav({
579
+ logo,
580
+ actions,
581
+ children,
582
+ menuLabel = "Menu",
583
+ className,
584
+ ...rest
585
+ }) {
586
+ const [open, setOpen] = useState4(false);
587
+ const linksId = useId3();
588
+ const navRef = useRef2(null);
589
+ const toggleRef = useRef2(null);
590
+ useEffect2(() => {
591
+ if (!open) return;
592
+ const onKey = (e) => {
593
+ if (e.key === "Escape") {
594
+ setOpen(false);
595
+ toggleRef.current?.focus();
596
+ }
597
+ };
598
+ const onPointer = (e) => {
599
+ if (!navRef.current?.contains(e.target)) setOpen(false);
600
+ };
601
+ window.addEventListener("keydown", onKey);
602
+ document.addEventListener("mousedown", onPointer);
603
+ return () => {
604
+ window.removeEventListener("keydown", onKey);
605
+ document.removeEventListener("mousedown", onPointer);
606
+ };
607
+ }, [open]);
608
+ const classes = ["fds-app-nav", className].filter(Boolean).join(" ");
609
+ return /* @__PURE__ */ jsxs6("nav", { ref: navRef, className: classes, ...rest, children: [
610
+ /* @__PURE__ */ jsx6("div", { className: "fds-app-nav__logo", children: logo }),
611
+ /* @__PURE__ */ jsx6(
612
+ "div",
613
+ {
614
+ id: linksId,
615
+ className: "fds-app-nav__links",
616
+ "data-open": open ? "" : void 0,
617
+ onClick: (e) => {
618
+ if (e.target.closest("a, button")) setOpen(false);
619
+ },
620
+ children
621
+ }
622
+ ),
623
+ actions != null && /* @__PURE__ */ jsx6("div", { className: "fds-app-nav__actions", children: actions }),
624
+ /* @__PURE__ */ jsx6(
625
+ "button",
626
+ {
627
+ ref: toggleRef,
628
+ type: "button",
629
+ className: "fds-app-nav__toggle",
630
+ "aria-label": menuLabel,
631
+ "aria-expanded": open,
632
+ "aria-controls": linksId,
633
+ onClick: () => setOpen((v) => !v),
634
+ children: /* @__PURE__ */ jsx6("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: open ? /* @__PURE__ */ jsx6("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) : /* @__PURE__ */ jsx6("path", { d: "M3 6h14M3 10h14M3 14h14", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) })
635
+ }
636
+ )
637
+ ] });
638
+ }
639
+
640
+ // src/components/TabNav/TabNav.tsx
641
+ import { jsx as jsx7 } from "react/jsx-runtime";
642
+ function TabNav({ children, className, ...rest }) {
643
+ const classes = ["fds-tab-nav", className].filter(Boolean).join(" ");
644
+ return /* @__PURE__ */ jsx7("nav", { className: classes, ...rest, children });
645
+ }
646
+
647
+ // src/components/SettingsSection/SettingsSection.tsx
648
+ import { useId as useId4 } from "react";
649
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
650
+ function SettingsSection({
651
+ title,
652
+ description,
653
+ children,
654
+ className,
655
+ ...rest
656
+ }) {
657
+ const titleId = useId4();
658
+ const classes = ["fds-settings-section", className].filter(Boolean).join(" ");
659
+ return /* @__PURE__ */ jsxs7("section", { className: classes, "aria-labelledby": titleId, ...rest, children: [
660
+ /* @__PURE__ */ jsxs7("div", { className: "fds-settings-section__head", children: [
661
+ /* @__PURE__ */ jsx8("h2", { id: titleId, className: "fds-settings-section__title", children: title }),
662
+ description != null && /* @__PURE__ */ jsx8("p", { className: "fds-settings-section__description", children: description })
663
+ ] }),
664
+ /* @__PURE__ */ jsx8("div", { className: "fds-settings-section__body", children })
665
+ ] });
666
+ }
667
+
668
+ // src/components/Modal/Modal.tsx
669
+ import { useEffect as useEffect3, useId as useId5, useRef as useRef3 } from "react";
670
+ import { createPortal } from "react-dom";
671
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
672
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
673
+ function Modal({
674
+ open,
675
+ onClose,
676
+ title,
677
+ description,
678
+ children,
679
+ actions,
680
+ size = "small",
681
+ dismissible = true,
682
+ tone = "default",
683
+ className
684
+ }) {
685
+ const titleId = useId5();
686
+ const descriptionId = useId5();
687
+ const dialogRef = useRef3(null);
688
+ const onCloseRef = useRef3(onClose);
689
+ onCloseRef.current = onClose;
690
+ const dismissibleRef = useRef3(dismissible);
691
+ dismissibleRef.current = dismissible;
692
+ useEffect3(() => {
693
+ if (!open) return;
694
+ const dialog = dialogRef.current;
695
+ const previouslyFocused = document.activeElement;
696
+ const initial = dialog?.querySelector("[data-autofocus]") ?? dialog?.querySelector(`.fds-modal__content ${FOCUSABLE}, .fds-modal__actions ${FOCUSABLE}`) ?? dialog;
697
+ initial?.focus();
698
+ const previousOverflow = document.body.style.overflow;
699
+ document.body.style.overflow = "hidden";
700
+ const onKey = (e) => {
701
+ if (e.key === "Escape" && dismissibleRef.current) {
702
+ e.preventDefault();
703
+ onCloseRef.current();
704
+ return;
705
+ }
706
+ if (e.key === "Tab" && dialog) {
707
+ const focusable = Array.from(dialog.querySelectorAll(FOCUSABLE));
708
+ if (focusable.length === 0) {
709
+ e.preventDefault();
710
+ return;
711
+ }
712
+ const first = focusable[0];
713
+ const last = focusable[focusable.length - 1];
714
+ if (e.shiftKey && (document.activeElement === first || document.activeElement === dialog)) {
715
+ e.preventDefault();
716
+ last.focus();
717
+ } else if (!e.shiftKey && document.activeElement === last) {
718
+ e.preventDefault();
719
+ first.focus();
720
+ }
721
+ }
722
+ };
723
+ window.addEventListener("keydown", onKey);
724
+ return () => {
725
+ window.removeEventListener("keydown", onKey);
726
+ document.body.style.overflow = previousOverflow;
727
+ previouslyFocused?.focus?.();
728
+ };
729
+ }, [open]);
730
+ if (!open || typeof document === "undefined") return null;
731
+ const classes = ["fds-modal", `fds-modal--${size}`, tone === "danger" && "fds-modal--danger", className].filter(Boolean).join(" ");
732
+ return createPortal(
733
+ /* @__PURE__ */ jsx9(
734
+ "div",
735
+ {
736
+ className: "fds-modal__scrim",
737
+ onMouseDown: (e) => {
738
+ if (e.target === e.currentTarget && dismissible) onClose();
739
+ },
740
+ children: /* @__PURE__ */ jsxs8(
741
+ "div",
742
+ {
743
+ ref: dialogRef,
744
+ className: classes,
745
+ role: "dialog",
746
+ "aria-modal": "true",
747
+ "aria-labelledby": titleId,
748
+ "aria-describedby": description != null ? descriptionId : void 0,
749
+ tabIndex: -1,
750
+ children: [
751
+ /* @__PURE__ */ jsx9(
752
+ "button",
753
+ {
754
+ type: "button",
755
+ className: "fds-modal__close",
756
+ onClick: onClose,
757
+ disabled: !dismissible,
758
+ "aria-label": "Close",
759
+ children: /* @__PURE__ */ jsx9("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx9("path", { d: "M4 4l8 8M12 4l-8 8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) })
760
+ }
761
+ ),
762
+ tone === "danger" && /* @__PURE__ */ jsx9("div", { className: "fds-modal__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsxs8("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [
763
+ /* @__PURE__ */ jsx9("path", { d: "M10 3.5L17.5 16.5H2.5L10 3.5z", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round" }),
764
+ /* @__PURE__ */ jsx9("path", { d: "M10 8.5v3.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }),
765
+ /* @__PURE__ */ jsx9("circle", { cx: "10", cy: "14.25", r: "0.9", fill: "currentColor" })
766
+ ] }) }),
767
+ /* @__PURE__ */ jsx9("h2", { id: titleId, className: "fds-modal__title", children: title }),
768
+ description != null && /* @__PURE__ */ jsx9("p", { id: descriptionId, className: "fds-modal__description", children: description }),
769
+ children != null && /* @__PURE__ */ jsx9("div", { className: "fds-modal__content", children }),
770
+ actions != null && /* @__PURE__ */ jsx9("div", { className: "fds-modal__actions", children: actions })
771
+ ]
772
+ }
773
+ )
774
+ }
775
+ ),
776
+ document.body
777
+ );
778
+ }
779
+
780
+ // src/components/SegmentedControl/SegmentedControl.tsx
781
+ import { useRef as useRef4 } from "react";
782
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
783
+ function SegmentedControl({
784
+ options,
785
+ value,
786
+ onChange,
787
+ "aria-label": ariaLabel,
788
+ className
789
+ }) {
790
+ const refs = useRef4([]);
791
+ const enabled = options.map((o, i) => o.disabled ? -1 : i).filter((i) => i >= 0);
792
+ const onKeyDown = (e, index) => {
793
+ const at = enabled.indexOf(index);
794
+ let next;
795
+ if (e.key === "ArrowRight") next = enabled[(at + 1) % enabled.length];
796
+ else if (e.key === "ArrowLeft") next = enabled[(at - 1 + enabled.length) % enabled.length];
797
+ else if (e.key === "Home") next = enabled[0];
798
+ else if (e.key === "End") next = enabled[enabled.length - 1];
799
+ if (next === void 0) return;
800
+ e.preventDefault();
801
+ onChange(options[next].value);
802
+ refs.current[next]?.focus();
803
+ };
804
+ const classes = ["fds-segmented", className].filter(Boolean).join(" ");
805
+ return /* @__PURE__ */ jsx10("div", { role: "tablist", "aria-label": ariaLabel, className: classes, children: options.map((option, i) => {
806
+ const selected = option.value === value;
807
+ return /* @__PURE__ */ jsxs9(
808
+ "button",
809
+ {
810
+ ref: (el) => {
811
+ refs.current[i] = el;
812
+ },
813
+ type: "button",
814
+ role: "tab",
815
+ "aria-selected": selected,
816
+ "aria-controls": option.controls,
817
+ tabIndex: selected ? 0 : -1,
818
+ disabled: option.disabled,
819
+ className: "fds-segmented__option",
820
+ onClick: () => onChange(option.value),
821
+ onKeyDown: (e) => onKeyDown(e, i),
822
+ children: [
823
+ option.label,
824
+ option.count != null && /* @__PURE__ */ jsx10("span", { className: "fds-segmented__count", children: option.count })
825
+ ]
826
+ },
827
+ option.value
828
+ );
829
+ }) });
830
+ }
831
+
832
+ // src/breakpoints.ts
833
+ var breakpoints = {
834
+ /** Phones. */
835
+ sm: 480,
836
+ /** Large phones, small tablets. */
837
+ md: 640,
838
+ /** Tablets, narrow windows. */
839
+ lg: 900
840
+ };
841
+
842
+ // src/components/Avatar/Avatar.tsx
843
+ import { useState as useState5 } from "react";
844
+ import { jsx as jsx11 } from "react/jsx-runtime";
845
+ function initialsFor(name) {
846
+ const words = name.trim().split(/[\s._@-]+/).filter(Boolean);
847
+ if (words.length === 0) return "?";
848
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
849
+ return (words[0][0] + words[1][0]).toUpperCase();
850
+ }
851
+ function Avatar({ name, src, size = "medium", className, ...rest }) {
852
+ const [failedSrc, setFailedSrc] = useState5(null);
853
+ const showImage = !!src && src !== failedSrc;
854
+ const classes = ["fds-avatar", `fds-avatar--${size}`, className].filter(Boolean).join(" ");
855
+ return /* @__PURE__ */ jsx11("span", { className: classes, role: "img", "aria-label": name, ...rest, children: showImage ? /* @__PURE__ */ jsx11("img", { className: "fds-avatar__img", src, alt: "", onError: () => setFailedSrc(src) }) : /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: initialsFor(name) }) });
856
+ }
857
+
858
+ // src/components/SectionHeader/SectionHeader.tsx
859
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
860
+ function SectionHeader({
861
+ title,
862
+ description,
863
+ as: Heading = "h2",
864
+ titleId,
865
+ className,
866
+ ...rest
867
+ }) {
868
+ const classes = ["fds-section-header", className].filter(Boolean).join(" ");
869
+ return /* @__PURE__ */ jsxs10("div", { className: classes, ...rest, children: [
870
+ /* @__PURE__ */ jsx12(Heading, { id: titleId, className: "fds-section-header__title", children: title }),
871
+ description != null && /* @__PURE__ */ jsx12("p", { className: "fds-section-header__description", children: description })
872
+ ] });
873
+ }
874
+
875
+ // src/components/Split/Split.tsx
876
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
877
+ function Split({
878
+ aside,
879
+ children,
880
+ asideWidth = "280px",
881
+ sticky = false,
882
+ collapseBelow = "lg",
883
+ className,
884
+ style,
885
+ ...rest
886
+ }) {
887
+ const classes = [
888
+ "fds-split",
889
+ `fds-split--collapse-${collapseBelow}`,
890
+ sticky && "fds-split--sticky",
891
+ className
892
+ ].filter(Boolean).join(" ");
893
+ return /* @__PURE__ */ jsxs11(
894
+ "div",
895
+ {
896
+ className: classes,
897
+ style: { "--fds-split-aside": asideWidth, ...style },
898
+ ...rest,
899
+ children: [
900
+ /* @__PURE__ */ jsx13("div", { className: "fds-split__aside", children: aside }),
901
+ /* @__PURE__ */ jsx13("div", { className: "fds-split__main", children })
902
+ ]
903
+ }
904
+ );
905
+ }
574
906
  export {
907
+ AppNav,
908
+ Avatar,
575
909
  Button,
576
910
  DiffPropertyList,
577
911
  DiffSection,
578
912
  MismatchCard,
913
+ Modal,
914
+ SectionHeader,
915
+ SegmentedControl,
579
916
  Select,
580
- Toggle
917
+ SettingsSection,
918
+ Split,
919
+ TabNav,
920
+ Toggle,
921
+ breakpoints
581
922
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefidel/design-system",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Fidel brand design tokens + React component library — DTCG token source of truth, generated per-surface CSS + Tailwind-v4 parser-compatibility output, and on-brand React components (Button, MismatchCard, DiffSection).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",