@usefidel/design-system 0.15.0 → 0.16.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.d.ts CHANGED
@@ -2,6 +2,8 @@ import * as react from 'react';
2
2
  import { ButtonHTMLAttributes, ReactNode, HTMLAttributes, InputHTMLAttributes } from 'react';
3
3
 
4
4
  type ButtonVariant = 'primary' | 'outlined' | 'text' | 'neutral' | 'link'
5
+ /** High-contrast near-black, for a page's main create action ("New comparison"). */
6
+ | 'dark'
5
7
  /** Final confirm of a destructive action ("Delete my account"). */
6
8
  | 'danger'
7
9
  /** Starts a destructive action ("Delete account", "Remove"). */
@@ -421,4 +423,143 @@ interface SplitProps extends HTMLAttributes<HTMLDivElement> {
421
423
  */
422
424
  declare function Split({ aside, children, asideWidth, sticky, collapseBelow, className, style, ...rest }: SplitProps): react.JSX.Element;
423
425
 
424
- export { AppNav, type AppNavProps, Avatar, type AvatarProps, type AvatarSize, type Breakpoint, Button, type ButtonProps, type ButtonSize, type ButtonVariant, type DiffElement, type DiffPropertyLine, DiffPropertyList, type DiffPropertyListProps, DiffSection, type DiffSectionProps, MismatchCard, type MismatchCardProps, type MismatchProperty, Modal, type ModalProps, type ModalSize, type ModalTone, SectionHeader, type SectionHeaderProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectOption, type SelectProps, SettingsSection, type SettingsSectionProps, Split, type SplitProps, TabNav, type TabNavProps, Toggle, type ToggleProps, type ToggleSize, breakpoints };
426
+ /** Semantic colour roles shared by status components (Badge, Tag, MultiSelect dots). */
427
+ type Tone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
428
+
429
+ interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
430
+ /** Colour role. @default 'neutral' */
431
+ tone?: Tone;
432
+ /** Leading icon, drawn at 12px. */
433
+ icon?: ReactNode;
434
+ children: ReactNode;
435
+ }
436
+ /**
437
+ * Fidel design-system Badge: a tinted pill naming a state ("Checking",
438
+ * "Checked", "Failed to run"). Measured from Figma "✅Project" status-badge
439
+ * (node 231:59799).
440
+ */
441
+ declare function Badge({ tone, icon, className, children, ...rest }: BadgeProps): react.JSX.Element;
442
+
443
+ interface TagProps extends HTMLAttributes<HTMLSpanElement> {
444
+ /** Colour of the leading dot. Leave out for no dot. */
445
+ dot?: Tone;
446
+ children: ReactNode;
447
+ }
448
+ /**
449
+ * Fidel design-system Tag: a quiet bordered label for a category, with an
450
+ * optional coloured dot. Measured from Figma "✅Project" comparison-type chip
451
+ * (node 231:59808).
452
+ */
453
+ declare function Tag({ dot, className, children, ...rest }: TagProps): react.JSX.Element;
454
+
455
+ interface MultiSelectOption {
456
+ value: string;
457
+ label: string;
458
+ /** Coloured dot before the label. */
459
+ dot?: Tone;
460
+ /** Trailing count, e.g. how many results the option would show. */
461
+ count?: number;
462
+ disabled?: boolean;
463
+ }
464
+ interface MultiSelectProps {
465
+ /** Trigger text, e.g. "Status". Also the list's accessible name. */
466
+ label: string;
467
+ options: MultiSelectOption[];
468
+ /** Selected values. An empty list means nothing is ticked. */
469
+ value: string[];
470
+ onChange: (value: string[]) => void;
471
+ disabled?: boolean;
472
+ selectAllLabel?: string;
473
+ clearLabel?: string;
474
+ id?: string;
475
+ className?: string;
476
+ }
477
+ /**
478
+ * Fidel design-system MultiSelect: a filter menu of checkbox options, each
479
+ * with an optional dot and count, and Select all / Clear in the footer. The
480
+ * trigger shows "n/m" once anything is ticked. Keyboard: Enter/Space/↓ open;
481
+ * ↑/↓/Home/End move; Enter/Space toggle; Esc closes. Measured from Figma
482
+ * "✅Project" filter Select (node 231:60682).
483
+ */
484
+ declare function MultiSelect({ label, options, value, onChange, disabled, selectAllLabel, clearLabel, id, className, }: MultiSelectProps): react.JSX.Element;
485
+
486
+ interface SearchInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
487
+ /** Keyboard hint shown at the end while the field is empty, e.g. "/". */
488
+ shortcut?: string;
489
+ /** Class for the wrapper; `className` goes on the input. */
490
+ containerClassName?: string;
491
+ }
492
+ /**
493
+ * Fidel design-system SearchInput: a 44px search field with a leading
494
+ * magnifier. Measured from Figma "✅Project" toolbar search (node 231:59687).
495
+ */
496
+ declare const SearchInput: react.ForwardRefExoticComponent<SearchInputProps & react.RefAttributes<HTMLInputElement>>;
497
+
498
+ interface PaginationProps {
499
+ /** Current page, 1-based. */
500
+ page: number;
501
+ pageCount: number;
502
+ onPageChange: (page: number) => void;
503
+ /** With `pageSize`, adds "Showing 1–20 of 45" (full variant only). */
504
+ total?: number;
505
+ pageSize?: number;
506
+ /** full: range text + page numbers. compact: "Page 2 of 3" between arrows. @default 'full' */
507
+ variant?: 'full' | 'compact';
508
+ className?: string;
509
+ }
510
+ /**
511
+ * Fidel design-system Pagination. Renders nothing when there is one page.
512
+ */
513
+ declare function Pagination({ page, pageCount, onPageChange, total, pageSize, variant, className }: PaginationProps): react.JSX.Element | null;
514
+
515
+ interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
516
+ /** Picture above the title, e.g. <StackedCardsIllustration />. */
517
+ illustration?: ReactNode;
518
+ title: ReactNode;
519
+ description?: ReactNode;
520
+ /** Primary action, usually a Button. */
521
+ action?: ReactNode;
522
+ /** Secondary line under the action, e.g. a help link. */
523
+ footer?: ReactNode;
524
+ }
525
+ /**
526
+ * Fidel design-system EmptyState: a centred illustration, title, description,
527
+ * action and footer, for a list or page with nothing in it yet. Measured from
528
+ * Figma "✅Project" Empty State Container (node 231:59661).
529
+ */
530
+ declare function EmptyState({ illustration, title, description, action, footer, className, ...rest }: EmptyStateProps): react.JSX.Element;
531
+ /**
532
+ * Two tilted cards with a search badge: "nothing found yet". Drawn from the
533
+ * Figma "Stacked Cards" layers (node 80:49887), 240×160.
534
+ */
535
+ declare function StackedCardsIllustration({ className }: {
536
+ className?: string;
537
+ }): react.JSX.Element;
538
+
539
+ type SkeletonShape = 'rect' | 'text' | 'circle';
540
+ interface SkeletonProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
541
+ /** rect: a block. text: line(s) at text height. circle: an avatar or icon. @default 'rect' */
542
+ shape?: SkeletonShape;
543
+ /** CSS width; a number is px. Text defaults to 100%, circle to its height. */
544
+ width?: number | string;
545
+ /** CSS height; a number is px. Text defaults to one line (1em). @default 16 for rect and circle */
546
+ height?: number | string;
547
+ /** Text only: how many lines. The last of several is shorter, as text usually is. @default 1 */
548
+ lines?: number;
549
+ }
550
+ /**
551
+ * Fidel design-system Skeleton: a placeholder shape that pulses while content
552
+ * loads. Pages compose these into the shape of what's coming (rows, cards,
553
+ * headers). Purely visual and hidden from assistive tech; wrap a layout in
554
+ * SkeletonGroup so it's announced once as loading.
555
+ */
556
+ declare function Skeleton({ shape, width, height, lines, className, style, ...rest }: SkeletonProps): react.JSX.Element;
557
+ interface SkeletonGroupProps extends HTMLAttributes<HTMLDivElement> {
558
+ /** What's loading, for screen readers. @default 'Loading' */
559
+ label?: string;
560
+ children: ReactNode;
561
+ }
562
+ /** Wraps a skeleton layout and announces it once as loading (role="status"). */
563
+ declare function SkeletonGroup({ label, className, children, ...rest }: SkeletonGroupProps): react.JSX.Element;
564
+
565
+ export { AppNav, type AppNavProps, Avatar, type AvatarProps, type AvatarSize, Badge, type BadgeProps, type Breakpoint, Button, type ButtonProps, type ButtonSize, type ButtonVariant, type DiffElement, type DiffPropertyLine, DiffPropertyList, type DiffPropertyListProps, DiffSection, type DiffSectionProps, EmptyState, type EmptyStateProps, MismatchCard, type MismatchCardProps, type MismatchProperty, Modal, type ModalProps, type ModalSize, type ModalTone, MultiSelect, type MultiSelectOption, type MultiSelectProps, Pagination, type PaginationProps, SearchInput, type SearchInputProps, SectionHeader, type SectionHeaderProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectOption, type SelectProps, SettingsSection, type SettingsSectionProps, Skeleton, SkeletonGroup, type SkeletonGroupProps, type SkeletonProps, type SkeletonShape, Split, type SplitProps, StackedCardsIllustration, TabNav, type TabNavProps, Tag, type TagProps, Toggle, type ToggleProps, type ToggleSize, type Tone, breakpoints };
package/dist/index.js CHANGED
@@ -903,20 +903,337 @@ function Split({
903
903
  }
904
904
  );
905
905
  }
906
+
907
+ // src/components/Badge/Badge.tsx
908
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
909
+ function Badge({ tone = "neutral", icon, className, children, ...rest }) {
910
+ const classes = ["fds-badge", `fds-badge--${tone}`, className].filter(Boolean).join(" ");
911
+ return /* @__PURE__ */ jsxs12("span", { className: classes, ...rest, children: [
912
+ icon != null && /* @__PURE__ */ jsx14("span", { className: "fds-badge__icon", "aria-hidden": "true", children: icon }),
913
+ children
914
+ ] });
915
+ }
916
+
917
+ // src/components/Tag/Tag.tsx
918
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
919
+ function Tag({ dot, className, children, ...rest }) {
920
+ return /* @__PURE__ */ jsxs13("span", { className: ["fds-tag", className].filter(Boolean).join(" "), ...rest, children: [
921
+ dot && /* @__PURE__ */ jsx15("span", { className: `fds-dot fds-dot--${dot}`, "aria-hidden": "true" }),
922
+ children
923
+ ] });
924
+ }
925
+
926
+ // src/components/MultiSelect/MultiSelect.tsx
927
+ import { useEffect as useEffect4, useId as useId6, useRef as useRef5, useState as useState6 } from "react";
928
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
929
+ function ChevronIcon2() {
930
+ return /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 20 20", width: "20", height: "20", fill: "none", "aria-hidden": "true", className: "fds-multiselect__chevronIcon", children: /* @__PURE__ */ jsx16("path", { d: "M6 8l4 4 4-4", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }) });
931
+ }
932
+ function CheckIcon2() {
933
+ return /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 12 12", width: "12", height: "12", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx16("path", { d: "M10 3L4.5 8.5 2 6", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
934
+ }
935
+ function MultiSelect({
936
+ label,
937
+ options,
938
+ value,
939
+ onChange,
940
+ disabled = false,
941
+ selectAllLabel = "Select all",
942
+ clearLabel = "Clear",
943
+ id,
944
+ className
945
+ }) {
946
+ const genId = useId6();
947
+ const triggerId = id ?? genId;
948
+ const listId = `${genId}-list`;
949
+ const rootRef = useRef5(null);
950
+ const triggerRef = useRef5(null);
951
+ const listRef = useRef5(null);
952
+ const [open, setOpen] = useState6(false);
953
+ const [activeIndex, setActiveIndex] = useState6(0);
954
+ const selected = new Set(value);
955
+ const enabled = options.filter((o) => !o.disabled);
956
+ useEffect4(() => {
957
+ if (!open) return;
958
+ const onDown = (e) => {
959
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
960
+ };
961
+ document.addEventListener("mousedown", onDown);
962
+ const raf = requestAnimationFrame(() => listRef.current?.focus());
963
+ return () => {
964
+ document.removeEventListener("mousedown", onDown);
965
+ cancelAnimationFrame(raf);
966
+ };
967
+ }, [open]);
968
+ const close = () => {
969
+ setOpen(false);
970
+ triggerRef.current?.focus();
971
+ };
972
+ const toggle = (o) => {
973
+ if (!o || o.disabled) return;
974
+ const next = selected.has(o.value) ? value.filter((v) => v !== o.value) : options.filter((x) => x.value === o.value || selected.has(x.value)).map((x) => x.value);
975
+ onChange(next);
976
+ };
977
+ const onTriggerKeyDown = (e) => {
978
+ if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter" || e.key === " ") {
979
+ e.preventDefault();
980
+ setActiveIndex(0);
981
+ setOpen(true);
982
+ }
983
+ };
984
+ const onListKeyDown = (e) => {
985
+ const last = options.length - 1;
986
+ if (e.key === "ArrowDown") {
987
+ e.preventDefault();
988
+ setActiveIndex((i) => i >= last ? 0 : i + 1);
989
+ } else if (e.key === "ArrowUp") {
990
+ e.preventDefault();
991
+ setActiveIndex((i) => i <= 0 ? last : i - 1);
992
+ } else if (e.key === "Home") {
993
+ e.preventDefault();
994
+ setActiveIndex(0);
995
+ } else if (e.key === "End") {
996
+ e.preventDefault();
997
+ setActiveIndex(last);
998
+ } else if (e.key === "Enter" || e.key === " ") {
999
+ e.preventDefault();
1000
+ toggle(options[activeIndex]);
1001
+ } else if (e.key === "Escape" || e.key === "Tab") {
1002
+ if (e.key === "Escape") e.preventDefault();
1003
+ close();
1004
+ }
1005
+ };
1006
+ const triggerClasses = ["fds-multiselect__trigger", open ? "fds-multiselect__trigger--open" : ""].filter(Boolean).join(" ");
1007
+ return /* @__PURE__ */ jsxs14("div", { ref: rootRef, className: ["fds-multiselect", className].filter(Boolean).join(" "), children: [
1008
+ /* @__PURE__ */ jsxs14(
1009
+ "button",
1010
+ {
1011
+ type: "button",
1012
+ id: triggerId,
1013
+ ref: triggerRef,
1014
+ className: triggerClasses,
1015
+ disabled,
1016
+ "aria-haspopup": "listbox",
1017
+ "aria-expanded": open,
1018
+ "aria-controls": open ? listId : void 0,
1019
+ onClick: () => setOpen((o) => !o),
1020
+ onKeyDown: onTriggerKeyDown,
1021
+ children: [
1022
+ /* @__PURE__ */ jsx16("span", { className: "fds-multiselect__label", children: label }),
1023
+ value.length > 0 && /* @__PURE__ */ jsxs14("span", { className: "fds-multiselect__count", "aria-label": `${value.length} of ${options.length} selected`, children: [
1024
+ value.length,
1025
+ "/",
1026
+ options.length
1027
+ ] }),
1028
+ /* @__PURE__ */ jsx16("span", { className: `fds-multiselect__chevron${open ? " fds-multiselect__chevron--open" : ""}`, children: /* @__PURE__ */ jsx16(ChevronIcon2, {}) })
1029
+ ]
1030
+ }
1031
+ ),
1032
+ open && /* @__PURE__ */ jsxs14("div", { className: "fds-multiselect__popover", children: [
1033
+ /* @__PURE__ */ jsx16(
1034
+ "div",
1035
+ {
1036
+ ref: listRef,
1037
+ id: listId,
1038
+ role: "listbox",
1039
+ "aria-multiselectable": "true",
1040
+ "aria-label": label,
1041
+ tabIndex: 0,
1042
+ className: "fds-multiselect__list",
1043
+ "aria-activedescendant": `${listId}-opt-${activeIndex}`,
1044
+ onKeyDown: onListKeyDown,
1045
+ children: options.map((o, i) => {
1046
+ const checked = selected.has(o.value);
1047
+ return /* @__PURE__ */ jsxs14(
1048
+ "div",
1049
+ {
1050
+ id: `${listId}-opt-${i}`,
1051
+ role: "option",
1052
+ "aria-selected": checked,
1053
+ "aria-disabled": o.disabled || void 0,
1054
+ className: [
1055
+ "fds-multiselect__option",
1056
+ i === activeIndex ? "fds-multiselect__option--active" : "",
1057
+ o.disabled ? "fds-multiselect__option--disabled" : ""
1058
+ ].filter(Boolean).join(" "),
1059
+ onMouseEnter: () => setActiveIndex(i),
1060
+ onMouseDown: (e) => e.preventDefault(),
1061
+ onClick: () => toggle(o),
1062
+ children: [
1063
+ /* @__PURE__ */ jsx16("span", { className: `fds-multiselect__box${checked ? " fds-multiselect__box--checked" : ""}`, "aria-hidden": "true", children: checked && /* @__PURE__ */ jsx16(CheckIcon2, {}) }),
1064
+ o.dot && /* @__PURE__ */ jsx16("span", { className: `fds-dot fds-dot--${o.dot}`, "aria-hidden": "true" }),
1065
+ /* @__PURE__ */ jsx16("span", { className: "fds-multiselect__optLabel", children: o.label }),
1066
+ o.count != null && /* @__PURE__ */ jsx16("span", { className: "fds-multiselect__optCount", children: o.count })
1067
+ ]
1068
+ },
1069
+ o.value
1070
+ );
1071
+ })
1072
+ }
1073
+ ),
1074
+ /* @__PURE__ */ jsxs14("div", { className: "fds-multiselect__footer", children: [
1075
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "fds-multiselect__action", onClick: () => onChange(enabled.map((o) => o.value)), children: selectAllLabel }),
1076
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "fds-multiselect__action", onClick: () => onChange([]), children: clearLabel })
1077
+ ] })
1078
+ ] })
1079
+ ] });
1080
+ }
1081
+
1082
+ // src/components/SearchInput/SearchInput.tsx
1083
+ import { forwardRef as forwardRef3 } from "react";
1084
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
1085
+ var SearchInput = forwardRef3(function SearchInput2({ shortcut, containerClassName, className, value, ...rest }, ref) {
1086
+ const empty = value === void 0 || value === "";
1087
+ return /* @__PURE__ */ jsxs15("div", { className: ["fds-search", containerClassName].filter(Boolean).join(" "), children: [
1088
+ /* @__PURE__ */ jsxs15("svg", { className: "fds-search__icon", viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: [
1089
+ /* @__PURE__ */ jsx17("circle", { cx: "11", cy: "11", r: "8", stroke: "currentColor", strokeWidth: "2" }),
1090
+ /* @__PURE__ */ jsx17("path", { d: "m21 21-4.3-4.3", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
1091
+ ] }),
1092
+ /* @__PURE__ */ jsx17("input", { ref, type: "search", value, className: ["fds-search__input", className].filter(Boolean).join(" "), ...rest }),
1093
+ shortcut && empty && /* @__PURE__ */ jsx17("kbd", { className: "fds-search__kbd", "aria-hidden": "true", children: shortcut })
1094
+ ] });
1095
+ });
1096
+
1097
+ // src/components/Pagination/Pagination.tsx
1098
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
1099
+ function pageItems(page, pageCount) {
1100
+ if (pageCount <= 7) return Array.from({ length: pageCount }, (_, i) => i + 1);
1101
+ const start = Math.max(2, Math.min(page - 1, pageCount - 4));
1102
+ const end = Math.min(pageCount - 1, Math.max(page + 1, 5));
1103
+ const items = [1];
1104
+ if (start > 2) items.push(null);
1105
+ for (let p = start; p <= end; p++) items.push(p);
1106
+ if (end < pageCount - 1) items.push(null);
1107
+ items.push(pageCount);
1108
+ return items;
1109
+ }
1110
+ function Arrow2({ dir }) {
1111
+ return /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 20 20", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx18("path", { d: dir === "prev" ? "M12 15l-5-5 5-5" : "M8 5l5 5-5 5", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }) });
1112
+ }
1113
+ function Pagination({ page, pageCount, onPageChange, total, pageSize, variant = "full", className }) {
1114
+ if (pageCount <= 1) return null;
1115
+ const go = (p) => {
1116
+ if (p >= 1 && p <= pageCount && p !== page) onPageChange(p);
1117
+ };
1118
+ const showRange = variant === "full" && total != null && pageSize != null;
1119
+ const from = (page - 1) * (pageSize ?? 0) + 1;
1120
+ const to = Math.min(page * (pageSize ?? 0), total ?? 0);
1121
+ return /* @__PURE__ */ jsxs16("nav", { className: ["fds-pagination", `fds-pagination--${variant}`, className].filter(Boolean).join(" "), "aria-label": "Pagination", children: [
1122
+ showRange && /* @__PURE__ */ jsxs16("p", { className: "fds-pagination__range", children: [
1123
+ "Showing ",
1124
+ from,
1125
+ "\u2013",
1126
+ to,
1127
+ " of ",
1128
+ total
1129
+ ] }),
1130
+ /* @__PURE__ */ jsxs16("div", { className: "fds-pagination__controls", children: [
1131
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "fds-pagination__arrow", onClick: () => go(page - 1), disabled: page <= 1, "aria-label": "Previous page", children: /* @__PURE__ */ jsx18(Arrow2, { dir: "prev" }) }),
1132
+ variant === "compact" ? /* @__PURE__ */ jsxs16("span", { className: "fds-pagination__status", "aria-live": "polite", children: [
1133
+ "Page ",
1134
+ page,
1135
+ " of ",
1136
+ pageCount
1137
+ ] }) : pageItems(page, pageCount).map(
1138
+ (p, i) => p === null ? /* @__PURE__ */ jsx18("span", { className: "fds-pagination__gap", "aria-hidden": "true", children: "\u2026" }, `gap-${i}`) : /* @__PURE__ */ jsx18(
1139
+ "button",
1140
+ {
1141
+ type: "button",
1142
+ className: `fds-pagination__page${p === page ? " fds-pagination__page--current" : ""}`,
1143
+ "aria-current": p === page ? "page" : void 0,
1144
+ "aria-label": `Page ${p}`,
1145
+ onClick: () => go(p),
1146
+ children: p
1147
+ },
1148
+ p
1149
+ )
1150
+ ),
1151
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "fds-pagination__arrow", onClick: () => go(page + 1), disabled: page >= pageCount, "aria-label": "Next page", children: /* @__PURE__ */ jsx18(Arrow2, { dir: "next" }) })
1152
+ ] })
1153
+ ] });
1154
+ }
1155
+
1156
+ // src/components/EmptyState/EmptyState.tsx
1157
+ import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
1158
+ function EmptyState({ illustration, title, description, action, footer, className, ...rest }) {
1159
+ return /* @__PURE__ */ jsxs17("div", { className: ["fds-empty", className].filter(Boolean).join(" "), ...rest, children: [
1160
+ illustration != null && /* @__PURE__ */ jsx19("div", { className: "fds-empty__illustration", children: illustration }),
1161
+ /* @__PURE__ */ jsxs17("div", { className: "fds-empty__text", children: [
1162
+ /* @__PURE__ */ jsx19("h2", { className: "fds-empty__title", children: title }),
1163
+ description != null && /* @__PURE__ */ jsx19("p", { className: "fds-empty__description", children: description })
1164
+ ] }),
1165
+ (action != null || footer != null) && /* @__PURE__ */ jsxs17("div", { className: "fds-empty__actions", children: [
1166
+ action,
1167
+ footer != null && /* @__PURE__ */ jsx19("div", { className: "fds-empty__footer", children: footer })
1168
+ ] })
1169
+ ] });
1170
+ }
1171
+ function StackedCardsIllustration({ className }) {
1172
+ return /* @__PURE__ */ jsxs17("div", { className: ["fds-stacked-cards", className].filter(Boolean).join(" "), "aria-hidden": "true", children: [
1173
+ /* @__PURE__ */ jsx19("div", { className: "fds-stacked-cards__card fds-stacked-cards__card--back" }),
1174
+ /* @__PURE__ */ jsxs17("div", { className: "fds-stacked-cards__card fds-stacked-cards__card--front", children: [
1175
+ /* @__PURE__ */ jsxs17("div", { className: "fds-stacked-cards__lines", children: [
1176
+ /* @__PURE__ */ jsx19("span", {}),
1177
+ /* @__PURE__ */ jsx19("span", {}),
1178
+ /* @__PURE__ */ jsx19("span", {})
1179
+ ] }),
1180
+ /* @__PURE__ */ jsxs17("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", className: "fds-stacked-cards__status", children: [
1181
+ /* @__PURE__ */ jsx19("rect", { x: "0.75", y: "0.75", width: "16.5", height: "16.5", rx: "8.25", stroke: "currentColor", strokeWidth: "1.5" }),
1182
+ /* @__PURE__ */ jsx19("circle", { cx: "9", cy: "9", r: "3", className: "fds-stacked-cards__statusDot" })
1183
+ ] })
1184
+ ] }),
1185
+ /* @__PURE__ */ jsx19("div", { className: "fds-stacked-cards__badge", children: /* @__PURE__ */ jsxs17("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", children: [
1186
+ /* @__PURE__ */ jsx19("circle", { cx: "11", cy: "11", r: "8", stroke: "currentColor", strokeWidth: "2" }),
1187
+ /* @__PURE__ */ jsx19("path", { d: "m21 21-4.3-4.3", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
1188
+ ] }) })
1189
+ ] });
1190
+ }
1191
+
1192
+ // src/components/Skeleton/Skeleton.tsx
1193
+ import { jsx as jsx20 } from "react/jsx-runtime";
1194
+ var px = (v) => typeof v === "number" ? `${v}px` : v;
1195
+ function Skeleton({ shape = "rect", width, height, lines = 1, className, style, ...rest }) {
1196
+ const classes = ["fds-skeleton", `fds-skeleton--${shape}`, className].filter(Boolean).join(" ");
1197
+ if (shape === "text" && lines > 1) {
1198
+ return /* @__PURE__ */ jsx20("span", { className: "fds-skeleton-lines", style: { width: px(width) }, "aria-hidden": "true", children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx20(
1199
+ "span",
1200
+ {
1201
+ className: classes,
1202
+ style: { height: px(height), width: i === lines - 1 ? "60%" : void 0, ...style },
1203
+ ...rest
1204
+ },
1205
+ i
1206
+ )) });
1207
+ }
1208
+ const size = shape === "circle" ? { width: px(width ?? height ?? 16), height: px(height ?? width ?? 16) } : { width: px(width), height: px(height) };
1209
+ return /* @__PURE__ */ jsx20("span", { className: classes, style: { ...size, ...style }, "aria-hidden": "true", ...rest });
1210
+ }
1211
+ function SkeletonGroup({ label = "Loading", className, children, ...rest }) {
1212
+ return /* @__PURE__ */ jsx20("div", { role: "status", "aria-busy": "true", "aria-label": label, className, ...rest, children });
1213
+ }
906
1214
  export {
907
1215
  AppNav,
908
1216
  Avatar,
1217
+ Badge,
909
1218
  Button,
910
1219
  DiffPropertyList,
911
1220
  DiffSection,
1221
+ EmptyState,
912
1222
  MismatchCard,
913
1223
  Modal,
1224
+ MultiSelect,
1225
+ Pagination,
1226
+ SearchInput,
914
1227
  SectionHeader,
915
1228
  SegmentedControl,
916
1229
  Select,
917
1230
  SettingsSection,
1231
+ Skeleton,
1232
+ SkeletonGroup,
918
1233
  Split,
1234
+ StackedCardsIllustration,
919
1235
  TabNav,
1236
+ Tag,
920
1237
  Toggle,
921
1238
  breakpoints
922
1239
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefidel/design-system",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",