@salemcobalt/ui 0.2.0-rc.7 → 0.2.0-rc.9

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
@@ -14,6 +14,8 @@ function Button({
14
14
  iconEnd,
15
15
  loading = false,
16
16
  fullWidth = false,
17
+ shortcut,
18
+ iconOnly = false,
17
19
  type = "button",
18
20
  className,
19
21
  children,
@@ -36,6 +38,7 @@ function Button({
36
38
  "data-size": size,
37
39
  "data-loading": loading || void 0,
38
40
  "data-full-width": fullWidth || void 0,
41
+ "data-icon-only": iconOnly || void 0,
39
42
  "aria-busy": loading || void 0,
40
43
  onClick: handleClick,
41
44
  ...rest,
@@ -43,6 +46,7 @@ function Button({
43
46
  iconStart ? /* @__PURE__ */ jsx("span", { className: "sc-button__icon", "aria-hidden": "true", children: iconStart }) : null,
44
47
  /* @__PURE__ */ jsx("span", { className: "sc-button__label", children }),
45
48
  iconEnd ? /* @__PURE__ */ jsx("span", { className: "sc-button__icon", "aria-hidden": "true", children: iconEnd }) : null,
49
+ shortcut ? /* @__PURE__ */ jsx("span", { className: "sc-button__shortcut", "aria-hidden": "true", children: shortcut }) : null,
46
50
  loading ? /* @__PURE__ */ jsx("span", { className: "sc-button__spinner", "aria-hidden": "true" }) : null
47
51
  ]
48
52
  }
@@ -596,6 +600,7 @@ function IconButton({
596
600
  tooltipSide = "top",
597
601
  round = false,
598
602
  loading = false,
603
+ dot,
599
604
  type,
600
605
  className,
601
606
  onClick,
@@ -628,7 +633,8 @@ function IconButton({
628
633
  ...rest,
629
634
  children: /* @__PURE__ */ jsxs7(Fragment2, { children: [
630
635
  /* @__PURE__ */ jsx8("span", { className: "sc-button__icon", "aria-hidden": "true", children }),
631
- loading ? /* @__PURE__ */ jsx8("span", { className: "sc-button__spinner", "aria-hidden": "true" }) : null
636
+ loading ? /* @__PURE__ */ jsx8("span", { className: "sc-button__spinner", "aria-hidden": "true" }) : null,
637
+ dot ? /* @__PURE__ */ jsx8("span", { className: "sc-icon-button__dot", "data-tone": dot, "aria-hidden": "true" }) : null
632
638
  ] })
633
639
  }
634
640
  });
@@ -1119,11 +1125,20 @@ function Root3({ className, onValueChange, ...rest }) {
1119
1125
  }
1120
1126
  );
1121
1127
  }
1122
- function List({ fill = false, className, children, ...rest }) {
1123
- return /* @__PURE__ */ jsxs14(BaseTabs.List, { className: cx("sc-tabs__list", className), "data-fill": fill || void 0, ...rest, children: [
1124
- children,
1125
- /* @__PURE__ */ jsx16(BaseTabs.Indicator, { className: "sc-tabs__indicator" })
1126
- ] });
1128
+ function List({ fill = false, variant = "line", className, children, ...rest }) {
1129
+ return /* @__PURE__ */ jsxs14(
1130
+ BaseTabs.List,
1131
+ {
1132
+ className: cx("sc-tabs__list", className),
1133
+ "data-fill": fill || void 0,
1134
+ "data-variant": variant,
1135
+ ...rest,
1136
+ children: [
1137
+ children,
1138
+ /* @__PURE__ */ jsx16(BaseTabs.Indicator, { className: "sc-tabs__indicator" })
1139
+ ]
1140
+ }
1141
+ );
1127
1142
  }
1128
1143
  function Tab({ icon, count, className, children, ...rest }) {
1129
1144
  return /* @__PURE__ */ jsxs14(BaseTabs.Tab, { className: cx("sc-tabs__tab", className), ...rest, children: [
@@ -1342,16 +1357,16 @@ function Text({
1342
1357
  style,
1343
1358
  ...rest
1344
1359
  }) {
1345
- const clamp = typeof truncate === "number" ? truncate : void 0;
1360
+ const clamp2 = typeof truncate === "number" ? truncate : void 0;
1346
1361
  return createElement4(as, {
1347
1362
  className: cx("sc-text", className),
1348
1363
  "data-size": size,
1349
1364
  "data-tone": tone,
1350
1365
  "data-weight": weight,
1351
1366
  "data-align": align,
1352
- "data-truncate": truncate === true ? "" : clamp !== void 0 ? "clamp" : void 0,
1367
+ "data-truncate": truncate === true ? "" : clamp2 !== void 0 ? "clamp" : void 0,
1353
1368
  "data-numeric": numeric || void 0,
1354
- style: clamp !== void 0 ? { ...style, "--sc-text-lines": clamp } : style,
1369
+ style: clamp2 !== void 0 ? { ...style, "--sc-text-lines": clamp2 } : style,
1355
1370
  ...rest
1356
1371
  });
1357
1372
  }
@@ -1734,6 +1749,790 @@ function Skeleton({
1734
1749
  );
1735
1750
  }
1736
1751
 
1752
+ // src/components/FileInput/FileInput.tsx
1753
+ import { useEffect as useEffect2, useId as useId3, useRef as useRef2, useState as useState4 } from "react";
1754
+
1755
+ // src/components/FileInput/accept.ts
1756
+ function parseAccept(accept) {
1757
+ if (accept == null) return [];
1758
+ const list = typeof accept === "string" ? accept.split(",") : accept;
1759
+ return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
1760
+ }
1761
+ function matchesAccept(file, accept) {
1762
+ if (accept.length === 0) return true;
1763
+ const name = file.name.toLowerCase();
1764
+ const type = file.type.toLowerCase();
1765
+ return accept.some((entry) => {
1766
+ if (entry.startsWith(".")) return name.endsWith(entry);
1767
+ if (entry.endsWith("/*")) return type.startsWith(entry.slice(0, -1));
1768
+ return type === entry;
1769
+ });
1770
+ }
1771
+ var UNITS = { b: 1, kb: 1e3, mb: 1e6, gb: 1e9 };
1772
+ function parseSize(size) {
1773
+ if (size == null) return void 0;
1774
+ if (typeof size === "number") return size;
1775
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)\s*$/i.exec(size);
1776
+ if (!match) throw new Error(`FileInput: maxFileSize "${size}" is not a size like "10MB".`);
1777
+ return Math.round(Number(match[1]) * UNITS[match[2].toLowerCase()]);
1778
+ }
1779
+ function formatSize(bytes) {
1780
+ const [unit, base] = bytes >= 1e9 ? ["GB", 1e9] : bytes >= 1e6 ? ["MB", 1e6] : bytes >= 1e3 ? ["KB", 1e3] : ["bytes", 1];
1781
+ const value = bytes / base;
1782
+ return `${Number.isInteger(value) ? value : Number(value.toFixed(1))} ${unit}`;
1783
+ }
1784
+ var MIME_NAMES = {
1785
+ "application/pdf": "PDF",
1786
+ "application/msword": "DOC",
1787
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
1788
+ "application/json": "JSON",
1789
+ "text/plain": "plain text",
1790
+ "text/markdown": "Markdown",
1791
+ "text/csv": "CSV",
1792
+ "image/png": "PNG",
1793
+ "image/jpeg": "JPEG"
1794
+ };
1795
+ function describeAccept(accept) {
1796
+ const names = accept.map((entry) => {
1797
+ if (entry.startsWith(".")) return entry.slice(1).toUpperCase();
1798
+ if (entry.endsWith("/*")) return `any ${entry.slice(0, -2)}`;
1799
+ return MIME_NAMES[entry] ?? entry.split("/").pop().toUpperCase();
1800
+ });
1801
+ const unique = [...new Set(names)];
1802
+ if (unique.length <= 1) return unique[0] ?? "";
1803
+ return `${unique.slice(0, -1).join(", ")} or ${unique.at(-1)}`;
1804
+ }
1805
+ function rejectionMessage(reason, detail) {
1806
+ switch (reason) {
1807
+ case "type":
1808
+ return `${detail.name} is not an accepted file type. Use ${detail.acceptLabel}.`;
1809
+ case "size":
1810
+ return `${detail.name} is ${formatSize(detail.size ?? 0)}, over the ${formatSize(detail.maxFileSize ?? 0)} limit.`;
1811
+ case "count":
1812
+ return detail.maxFiles === 1 ? "Choose one file." : `Up to ${detail.maxFiles} files. Remove one to add another.`;
1813
+ }
1814
+ }
1815
+ function escapeHtml(text) {
1816
+ return text.replace(
1817
+ /[&<>"']/g,
1818
+ (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch]
1819
+ );
1820
+ }
1821
+
1822
+ // src/components/FileInput/FileInput.tsx
1823
+ import { jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
1824
+ var REMOVE_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
1825
+ var sameFiles = (a, b) => a.length === b.length && a.every((file, i) => file === b[i]);
1826
+ function FileInput({
1827
+ label,
1828
+ hideLabel = false,
1829
+ description,
1830
+ error,
1831
+ invalid = false,
1832
+ required = false,
1833
+ disabled = false,
1834
+ name,
1835
+ size = "md",
1836
+ className,
1837
+ accept,
1838
+ acceptLabel,
1839
+ maxFileSize,
1840
+ multiple = false,
1841
+ maxFiles,
1842
+ files,
1843
+ defaultFiles,
1844
+ onFilesChange,
1845
+ idleLabel,
1846
+ browseLabel = "browse"
1847
+ }) {
1848
+ const id = useId3();
1849
+ const labelId = `${id}label`;
1850
+ const descriptionId = `${id}description`;
1851
+ const messagesId = `${id}messages`;
1852
+ const controlled = files !== void 0;
1853
+ const [uncontrolledFiles, setUncontrolledFiles] = useState4(defaultFiles ?? []);
1854
+ const current = controlled ? files : uncontrolledFiles;
1855
+ const [rejections, setRejections] = useState4([]);
1856
+ const [inputId, setInputId] = useState4();
1857
+ const [pondVersion, setPondVersion] = useState4(0);
1858
+ const hostRef = useRef2(null);
1859
+ const pondRef = useRef2(null);
1860
+ const moduleRef = useRef2(null);
1861
+ const patchRef = useRef2(() => {
1862
+ });
1863
+ const settledRef = useRef2(current);
1864
+ const targetRef = useRef2(null);
1865
+ const acceptList = parseAccept(accept);
1866
+ const acceptAttr = acceptList.join(",");
1867
+ const maxBytes = parseSize(maxFileSize);
1868
+ const messages = [...rejections, ...error != null ? [error] : []];
1869
+ const isInvalid = invalid || messages.length > 0;
1870
+ const idleText = idleLabel ?? (multiple ? "Drop files here or" : current.length > 0 ? "Drop another file to replace it, or" : "Drop a file here or");
1871
+ const live = {
1872
+ acceptList,
1873
+ acceptLabel: acceptLabel ?? describeAccept(acceptList),
1874
+ maxBytes,
1875
+ maxFiles: multiple ? maxFiles : 1,
1876
+ multiple,
1877
+ current,
1878
+ controlled,
1879
+ onFilesChange,
1880
+ labelId,
1881
+ describedBy: [description != null ? descriptionId : null, messages.length > 0 ? messagesId : null],
1882
+ isInvalid,
1883
+ acceptAttr
1884
+ };
1885
+ const liveRef = useRef2(live);
1886
+ useEffect2(() => {
1887
+ liveRef.current = live;
1888
+ });
1889
+ useEffect2(() => {
1890
+ let cancelled = false;
1891
+ let pond = null;
1892
+ let observer = null;
1893
+ const host = hostRef.current;
1894
+ if (!host) return void 0;
1895
+ const reject = (reason, file) => {
1896
+ const l = liveRef.current;
1897
+ const message = rejectionMessage(reason, {
1898
+ name: file?.name,
1899
+ size: file?.size,
1900
+ acceptLabel: l.acceptLabel,
1901
+ maxFileSize: l.maxBytes,
1902
+ maxFiles: l.maxFiles
1903
+ });
1904
+ setRejections((list) => list.includes(message) ? list : [...list, message]);
1905
+ if (file && targetRef.current) {
1906
+ targetRef.current = targetRef.current.filter((f) => f !== file);
1907
+ queueMicrotask(emit);
1908
+ }
1909
+ };
1910
+ const startAttempt = () => setRejections((list) => list.length ? [] : list);
1911
+ let removeIntent = false;
1912
+ const acceptedIds = /* @__PURE__ */ new Set();
1913
+ const noteRemove = (event) => {
1914
+ if (event.target?.closest?.(".filepond--action-remove-item")) removeIntent = true;
1915
+ };
1916
+ const emit = () => {
1917
+ const mod = moduleRef.current;
1918
+ if (!pond || !mod) return;
1919
+ const idle = pond.getFiles().filter((item) => item.status === mod.FileStatus.IDLE).map((item) => item.file);
1920
+ const accepted = liveRef.current.multiple ? idle : idle.slice(-1);
1921
+ const target = targetRef.current;
1922
+ if (target) {
1923
+ if (!sameFiles(accepted, target)) return;
1924
+ targetRef.current = null;
1925
+ settledRef.current = accepted;
1926
+ if (!sameFiles(accepted, liveRef.current.current)) {
1927
+ if (!liveRef.current.controlled) setUncontrolledFiles(accepted);
1928
+ liveRef.current.onFilesChange?.(accepted);
1929
+ }
1930
+ return;
1931
+ }
1932
+ if (sameFiles(accepted, settledRef.current)) return;
1933
+ settledRef.current = accepted;
1934
+ if (!liveRef.current.controlled) setUncontrolledFiles(accepted);
1935
+ liveRef.current.onFilesChange?.(accepted);
1936
+ };
1937
+ const patch = () => {
1938
+ if (!pond) return;
1939
+ const l = liveRef.current;
1940
+ const root = pond.element;
1941
+ if (!root) return;
1942
+ const set = (el, attr, value) => {
1943
+ if (!el || el.getAttribute(attr) === value) return;
1944
+ if (value == null) el.removeAttribute(attr);
1945
+ else el.setAttribute(attr, value);
1946
+ };
1947
+ const input = root.querySelector("input.filepond--browser");
1948
+ const dropLabel = root.querySelector(".filepond--drop-label label");
1949
+ set(input, "aria-labelledby", l.labelId);
1950
+ set(input, "aria-describedby", [dropLabel?.id, ...l.describedBy].filter(Boolean).join(" ") || null);
1951
+ set(input, "aria-invalid", l.isInvalid ? "true" : null);
1952
+ set(input, "accept", l.acceptAttr || null);
1953
+ set(input, "multiple", l.multiple ? "" : null);
1954
+ set(root.querySelector(".filepond--label-action"), "tabindex", null);
1955
+ for (const item of root.querySelectorAll(".filepond--item")) {
1956
+ const fileName = item.querySelector(".filepond--file-info-main")?.textContent?.trim();
1957
+ if (fileName)
1958
+ set(item.querySelector(".filepond--action-remove-item"), "aria-label", `Remove ${fileName}`);
1959
+ }
1960
+ setInputId(input?.id || void 0);
1961
+ };
1962
+ void import("filepond").then((mod) => {
1963
+ if (cancelled || !mod.supported()) return;
1964
+ moduleRef.current = mod;
1965
+ const options = {
1966
+ credits: false,
1967
+ allowPaste: false,
1968
+ allowReorder: false,
1969
+ allowProcess: false,
1970
+ allowRevert: false,
1971
+ instantUpload: false,
1972
+ dropOnPage: false,
1973
+ // Always multi: FilePond's own single mode replaces the current file *before* validating the new
1974
+ // one, so a rejected file would discard a good one. Replacement happens in `addfile` instead.
1975
+ allowMultiple: true,
1976
+ allowReplace: false,
1977
+ itemInsertLocation: "after",
1978
+ fileSizeBase: 1e3,
1979
+ iconRemove: REMOVE_ICON,
1980
+ labelButtonRemoveItem: "Remove",
1981
+ styleButtonRemoveItemPosition: "right",
1982
+ beforeAddFile: (item) => {
1983
+ const l = liveRef.current;
1984
+ const file = item.file;
1985
+ if (!matchesAccept(file, l.acceptList)) return reject("type", file), false;
1986
+ if (l.maxBytes != null && file.size > l.maxBytes) return reject("size", file), false;
1987
+ return true;
1988
+ }
1989
+ };
1990
+ pond = mod.create(options);
1991
+ const root = pond.element;
1992
+ if (!root) return;
1993
+ host.appendChild(root);
1994
+ pondRef.current = pond;
1995
+ patchRef.current = patch;
1996
+ pond.on("addfile", (error2, added) => {
1997
+ if (!error2) acceptedIds.add(added.id);
1998
+ if (!error2 && !liveRef.current.multiple) {
1999
+ for (const other of pond?.getFiles() ?? []) if (other.id !== added.id) pond?.removeFile(other.id);
2000
+ }
2001
+ emit();
2002
+ });
2003
+ pond.on("removefile", (_error, removed) => {
2004
+ const active = document.activeElement;
2005
+ const inRemovedRow = active?.closest(".filepond--item")?.id === `filepond--item-${removed.id}`;
2006
+ if (inRemovedRow || removeIntent && (active == null || active === document.body))
2007
+ root.querySelector("input.filepond--browser")?.focus();
2008
+ removeIntent = false;
2009
+ if (acceptedIds.delete(removed.id)) startAttempt();
2010
+ emit();
2011
+ });
2012
+ pond.on("updatefiles", emit);
2013
+ pond.on("warning", (warning) => {
2014
+ if (warning?.body === "Max files") reject("count");
2015
+ });
2016
+ observer = new MutationObserver(patch);
2017
+ observer.observe(root, {
2018
+ subtree: true,
2019
+ childList: true,
2020
+ attributes: true,
2021
+ attributeFilter: ["tabindex", "aria-labelledby", "aria-describedby", "accept", "id", "multiple"]
2022
+ });
2023
+ host.addEventListener("drop", startAttempt, true);
2024
+ host.addEventListener("change", startAttempt, true);
2025
+ host.addEventListener("click", noteRemove, true);
2026
+ settledRef.current = [];
2027
+ setPondVersion((v) => v + 1);
2028
+ });
2029
+ return () => {
2030
+ cancelled = true;
2031
+ host.removeEventListener("drop", startAttempt, true);
2032
+ host.removeEventListener("change", startAttempt, true);
2033
+ host.removeEventListener("click", noteRemove, true);
2034
+ observer?.disconnect();
2035
+ if (pond) {
2036
+ pond.element?.remove();
2037
+ pond.destroy();
2038
+ }
2039
+ pondRef.current = null;
2040
+ patchRef.current = () => {
2041
+ };
2042
+ };
2043
+ }, []);
2044
+ useEffect2(() => {
2045
+ const pond = pondRef.current;
2046
+ if (!pond) return;
2047
+ pond.setOptions({
2048
+ name: name ?? "",
2049
+ storeAsFile: name != null,
2050
+ disabled,
2051
+ required,
2052
+ maxFiles: multiple ? maxFiles ?? null : null,
2053
+ allowRemove: !disabled,
2054
+ labelIdle: `${escapeHtml(idleText)} <span class="filepond--label-action">${escapeHtml(browseLabel)}</span>`
2055
+ });
2056
+ patchRef.current();
2057
+ }, [
2058
+ pondVersion,
2059
+ name,
2060
+ disabled,
2061
+ required,
2062
+ multiple,
2063
+ maxFiles,
2064
+ idleText,
2065
+ browseLabel,
2066
+ labelId,
2067
+ acceptAttr,
2068
+ isInvalid,
2069
+ description,
2070
+ messages.length
2071
+ ]);
2072
+ useEffect2(() => {
2073
+ const pond = pondRef.current;
2074
+ const mod = moduleRef.current;
2075
+ if (!pond || !mod) return;
2076
+ const items = pond.getFiles();
2077
+ const idle = items.filter((item) => item.status === mod.FileStatus.IDLE);
2078
+ const inPond = idle.map((item) => item.file);
2079
+ if (sameFiles(inPond, current)) {
2080
+ settledRef.current = current;
2081
+ return;
2082
+ }
2083
+ targetRef.current = current;
2084
+ for (const item of idle) if (!current.includes(item.file)) pond.removeFile(item.id);
2085
+ const toAdd = current.filter((file) => !inPond.includes(file));
2086
+ if (toAdd.length > 0) void pond.addFiles(toAdd);
2087
+ }, [current, pondVersion]);
2088
+ const ready = pondVersion > 0;
2089
+ return /* @__PURE__ */ jsxs22(
2090
+ "div",
2091
+ {
2092
+ className: cx("sc-field", "sc-file-input-field", className),
2093
+ "data-size": size,
2094
+ "data-disabled": disabled ? "" : void 0,
2095
+ "data-invalid": isInvalid ? "" : void 0,
2096
+ children: [
2097
+ /* @__PURE__ */ jsxs22(
2098
+ "label",
2099
+ {
2100
+ id: labelId,
2101
+ htmlFor: inputId,
2102
+ className: cx("sc-field__label", hideLabel && "sc-visually-hidden"),
2103
+ children: [
2104
+ label,
2105
+ required ? /* @__PURE__ */ jsx27("span", { className: "sc-field__required", "aria-hidden": "true", children: "*" }) : null
2106
+ ]
2107
+ }
2108
+ ),
2109
+ /* @__PURE__ */ jsxs22(
2110
+ "div",
2111
+ {
2112
+ className: "sc-file-input",
2113
+ "data-ready": ready ? "" : void 0,
2114
+ "data-disabled": disabled ? "" : void 0,
2115
+ "data-invalid": isInvalid ? "" : void 0,
2116
+ "data-multiple": multiple ? "" : void 0,
2117
+ children: [
2118
+ ready ? null : (
2119
+ // Server render and first paint: the same drop zone, inert until FilePond takes over.
2120
+ /* @__PURE__ */ jsxs22("div", { className: "sc-file-input__placeholder", "aria-hidden": "true", children: [
2121
+ idleText,
2122
+ " ",
2123
+ /* @__PURE__ */ jsx27("span", { className: "sc-file-input__browse", children: browseLabel })
2124
+ ] })
2125
+ ),
2126
+ /* @__PURE__ */ jsx27("div", { ref: hostRef, className: "sc-file-input__pond" })
2127
+ ]
2128
+ }
2129
+ ),
2130
+ description != null ? /* @__PURE__ */ jsx27("p", { id: descriptionId, className: "sc-field__description", children: description }) : null,
2131
+ /* @__PURE__ */ jsx27("div", { id: messagesId, className: "sc-file-input__messages", "aria-live": "polite", children: messages.map((message, i) => /* @__PURE__ */ jsx27("p", { className: "sc-field__error", children: message }, i)) })
2132
+ ]
2133
+ }
2134
+ );
2135
+ }
2136
+
2137
+ // src/components/PriorityNav/PriorityNav.tsx
2138
+ import {
2139
+ useEffect as useEffect3,
2140
+ useId as useId4,
2141
+ useLayoutEffect,
2142
+ useRef as useRef3,
2143
+ useState as useState5
2144
+ } from "react";
2145
+
2146
+ // src/components/PriorityNav/fit.ts
2147
+ var EPSILON = 0.5;
2148
+ function rowWidth(widths, gap) {
2149
+ let total = 0;
2150
+ for (const w of widths) total += w;
2151
+ return widths.length > 1 ? total + gap * (widths.length - 1) : total;
2152
+ }
2153
+ function fitCount({ widths, current, available, more, gap }) {
2154
+ const n = widths.length;
2155
+ const hasCurrent = current >= 0 && current < n;
2156
+ const others = hasCurrent ? n - 1 : n;
2157
+ if (rowWidth(widths, gap) <= available + EPSILON) return others;
2158
+ let used = more + (hasCurrent ? widths[current] + gap : 0);
2159
+ let count = 0;
2160
+ for (let i = 0; i < n; i++) {
2161
+ if (i === current) continue;
2162
+ const next = used + widths[i] + gap;
2163
+ if (next > available + EPSILON) break;
2164
+ used = next;
2165
+ count++;
2166
+ }
2167
+ return count;
2168
+ }
2169
+ function inlineFlags(length, current, count) {
2170
+ const flags = [];
2171
+ let seen = 0;
2172
+ for (let i = 0; i < length; i++) {
2173
+ if (i === current) flags.push(true);
2174
+ else flags.push(seen++ < count);
2175
+ }
2176
+ return flags;
2177
+ }
2178
+ function isOverflowing({ widths, gap, available, was, hysteresis }) {
2179
+ const total = rowWidth(widths, gap);
2180
+ return was ? total + Math.max(0, hysteresis) > available + EPSILON : total > available + EPSILON;
2181
+ }
2182
+
2183
+ // src/components/PriorityNav/place.ts
2184
+ var clamp = (value, min, max) => Math.max(min, Math.min(value, max));
2185
+ function placePanel({ anchor, panel, viewport, gutter, offset, rtl = false }) {
2186
+ const endAligned = rtl ? anchor.left : anchor.right - panel.width;
2187
+ const startAligned = rtl ? anchor.right - panel.width : anchor.left;
2188
+ const fitsX = (x2) => x2 >= gutter && x2 + panel.width <= viewport.width - gutter;
2189
+ const x = fitsX(endAligned) ? endAligned : fitsX(startAligned) ? startAligned : endAligned;
2190
+ const left = clamp(x, gutter, Math.max(gutter, viewport.width - gutter - panel.width));
2191
+ const below = anchor.bottom + offset;
2192
+ const above = anchor.top - offset - panel.height;
2193
+ const fitsBelow = below + panel.height <= viewport.height - gutter;
2194
+ const top = fitsBelow || above < gutter ? below : above;
2195
+ return { left, top };
2196
+ }
2197
+
2198
+ // src/components/PriorityNav/PriorityNav.tsx
2199
+ import { jsx as jsx28, jsxs as jsxs23 } from "react/jsx-runtime";
2200
+ var defaultCountLabel = (n) => n === 1 ? "1 link" : `${n} links`;
2201
+ var defaultLink = ({ children, ...props }) => /* @__PURE__ */ jsx28("a", { ...props, children });
2202
+ var keyOf = (item) => item.id ?? item.href;
2203
+ var supportsAnchor = () => typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("anchor-name: --a");
2204
+ var supportsPopover = (el) => typeof el.showPopover === "function";
2205
+ function signatureOf(items) {
2206
+ return items.map((item) => {
2207
+ const text = typeof item.label === "string" || typeof item.label === "number" ? item.label : "";
2208
+ return `${keyOf(item)}${text}`;
2209
+ }).join("");
2210
+ }
2211
+ function PriorityNav({
2212
+ items,
2213
+ moreLabel = "More",
2214
+ moreCountLabel = defaultCountLabel,
2215
+ renderLink = defaultLink,
2216
+ onOverflowChange,
2217
+ overflowHysteresis = 0,
2218
+ className,
2219
+ style,
2220
+ ...rest
2221
+ }) {
2222
+ const signature = signatureOf(items);
2223
+ const current = items.findIndex((item) => item.current);
2224
+ const [fit, setFit] = useState5({ signature: null, current: -1, count: 0, overflowing: false });
2225
+ const [open, setOpen] = useState5(false);
2226
+ const panelId = useId4();
2227
+ const listRef = useRef3(null);
2228
+ const moreRef = useRef3(null);
2229
+ const toggleRef = useRef3(null);
2230
+ const panelRef = useRef3(null);
2231
+ const measured = useRef3(null);
2232
+ const currentRef = useRef3(current);
2233
+ const fitRef = useRef3(fit);
2234
+ const hysteresisRef = useRef3(overflowHysteresis);
2235
+ const onOverflowRef = useRef3(onOverflowChange);
2236
+ const solve = (m, cur, was) => ({
2237
+ count: fitCount({ ...m, current: cur }),
2238
+ overflowing: isOverflowing({ ...m, was, hysteresis: hysteresisRef.current })
2239
+ });
2240
+ const measuring = fit.signature !== signature;
2241
+ useLayoutEffect(() => {
2242
+ currentRef.current = current;
2243
+ fitRef.current = fit;
2244
+ hysteresisRef.current = overflowHysteresis;
2245
+ onOverflowRef.current = onOverflowChange;
2246
+ });
2247
+ useLayoutEffect(() => {
2248
+ const list = listRef.current;
2249
+ const more = moreRef.current;
2250
+ if (!list || !more) return;
2251
+ if (measuring) {
2252
+ const widths = Array.from(
2253
+ list.querySelectorAll(":scope > .sc-priority-nav__item"),
2254
+ (el) => el.getBoundingClientRect().width
2255
+ );
2256
+ measured.current = {
2257
+ widths,
2258
+ more: more.getBoundingClientRect().width,
2259
+ gap: parseFloat(getComputedStyle(list).columnGap) || 0,
2260
+ available: list.clientWidth
2261
+ };
2262
+ } else if (fit.current === current) return;
2263
+ const m = measured.current;
2264
+ if (!m) return;
2265
+ setFit({ signature, current, ...solve(m, current, fit.overflowing) });
2266
+ }, [measuring, signature, current, fit]);
2267
+ useEffect3(() => {
2268
+ const list = listRef.current;
2269
+ const more = moreRef.current;
2270
+ if (!list || !more || typeof ResizeObserver === "undefined") return;
2271
+ const observer = new ResizeObserver((entries) => {
2272
+ const m = measured.current;
2273
+ if (!m) return;
2274
+ for (const entry of entries) {
2275
+ if (entry.target === list)
2276
+ m.available = entry.contentBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
2277
+ else {
2278
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? 0;
2279
+ if (width > 0) m.more = width;
2280
+ }
2281
+ }
2282
+ const prev = fitRef.current;
2283
+ if (prev.signature === null) return;
2284
+ const next = solve(m, currentRef.current, prev.overflowing);
2285
+ if (prev.count !== next.count || prev.overflowing !== next.overflowing) setFit({ ...prev, ...next });
2286
+ });
2287
+ observer.observe(list);
2288
+ observer.observe(more);
2289
+ return () => observer.disconnect();
2290
+ }, []);
2291
+ const reported = useRef3(false);
2292
+ useEffect3(() => {
2293
+ if (reported.current === fit.overflowing) return;
2294
+ reported.current = fit.overflowing;
2295
+ onOverflowRef.current?.(fit.overflowing);
2296
+ }, [fit.overflowing]);
2297
+ useEffect3(() => {
2298
+ const fonts = typeof document === "undefined" ? void 0 : document.fonts;
2299
+ if (!fonts || fonts.status === "loaded") return;
2300
+ let live = true;
2301
+ void fonts.ready.then(() => {
2302
+ if (live) setFit((prev) => ({ ...prev, signature: null }));
2303
+ });
2304
+ return () => {
2305
+ live = false;
2306
+ };
2307
+ }, []);
2308
+ const flags = measuring ? null : inlineFlags(items.length, current, fit.count);
2309
+ const overflow = flags ? items.filter((_, i) => !flags[i]) : [];
2310
+ const expanded = open && overflow.length > 0;
2311
+ useLayoutEffect(() => {
2312
+ const panel = panelRef.current;
2313
+ if (!panel || !supportsPopover(panel)) return;
2314
+ if (!panel.hasAttribute("popover")) panel.setAttribute("popover", "manual");
2315
+ const shown = panel.matches(":popover-open");
2316
+ if (expanded && !shown) panel.showPopover();
2317
+ else if (!expanded && shown) panel.hidePopover();
2318
+ }, [expanded]);
2319
+ useLayoutEffect(() => {
2320
+ const panel = panelRef.current;
2321
+ const toggle = toggleRef.current;
2322
+ if (!expanded || !panel || !toggle || supportsAnchor()) return;
2323
+ const place = () => {
2324
+ const anchor = toggle.getBoundingClientRect();
2325
+ const box = panel.getBoundingClientRect();
2326
+ const css = getComputedStyle(panel);
2327
+ const { left, top } = placePanel({
2328
+ anchor,
2329
+ panel: { width: box.width, height: box.height },
2330
+ viewport: { width: document.documentElement.clientWidth, height: window.innerHeight },
2331
+ gutter: parseFloat(css.getPropertyValue("--sc-spacing-md")) || 16,
2332
+ offset: parseFloat(css.getPropertyValue("--sc-spacing-2xs")) || 4,
2333
+ rtl: css.direction === "rtl"
2334
+ });
2335
+ panel.style.left = `${left}px`;
2336
+ panel.style.top = `${top}px`;
2337
+ };
2338
+ place();
2339
+ window.addEventListener("resize", place, { passive: true });
2340
+ window.addEventListener("scroll", place, { passive: true, capture: true });
2341
+ return () => {
2342
+ window.removeEventListener("resize", place);
2343
+ window.removeEventListener("scroll", place, { capture: true });
2344
+ panel.style.left = "";
2345
+ panel.style.top = "";
2346
+ };
2347
+ }, [expanded]);
2348
+ useEffect3(() => {
2349
+ if (!expanded) return;
2350
+ const onPointerDown = (event) => {
2351
+ if (!moreRef.current?.contains(event.target)) setOpen(false);
2352
+ };
2353
+ const onKeyDown = (event) => {
2354
+ if (event.key !== "Escape") return;
2355
+ const active = document.activeElement;
2356
+ setOpen(false);
2357
+ if (!active || active === document.body || moreRef.current?.contains(active))
2358
+ toggleRef.current?.focus();
2359
+ };
2360
+ document.addEventListener("pointerdown", onPointerDown);
2361
+ document.addEventListener("keydown", onKeyDown);
2362
+ return () => {
2363
+ document.removeEventListener("pointerdown", onPointerDown);
2364
+ document.removeEventListener("keydown", onKeyDown);
2365
+ };
2366
+ }, [expanded]);
2367
+ const onBlur = (event) => {
2368
+ if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2369
+ };
2370
+ const link = (item, inPanel) => renderLink(
2371
+ {
2372
+ href: item.href,
2373
+ className: "sc-priority-nav__link",
2374
+ "aria-current": item.current ? "page" : void 0,
2375
+ onClick: inPanel ? () => setOpen(false) : void 0,
2376
+ children: /* @__PURE__ */ jsx28("span", { className: "sc-priority-nav__label", children: item.label })
2377
+ },
2378
+ item
2379
+ );
2380
+ return /* @__PURE__ */ jsx28(
2381
+ "nav",
2382
+ {
2383
+ className: cx("sc-priority-nav", className),
2384
+ "data-measuring": measuring || void 0,
2385
+ "data-overflowing": fit.overflowing || void 0,
2386
+ style: { ...style, "--_anchor": `--sc-priority-nav${panelId.replace(/[^\w-]/g, "")}` },
2387
+ ...rest,
2388
+ children: /* @__PURE__ */ jsxs23("ul", { ref: listRef, className: "sc-priority-nav__list", children: [
2389
+ items.map(
2390
+ (item, i) => flags && !flags[i] ? null : /* @__PURE__ */ jsx28("li", { className: "sc-priority-nav__item", "data-current": item.current || void 0, children: link(item, false) }, keyOf(item))
2391
+ ),
2392
+ /* @__PURE__ */ jsxs23(
2393
+ "li",
2394
+ {
2395
+ ref: moreRef,
2396
+ className: "sc-priority-nav__more",
2397
+ hidden: measuring || overflow.length === 0,
2398
+ onBlur,
2399
+ children: [
2400
+ /* @__PURE__ */ jsxs23(
2401
+ "button",
2402
+ {
2403
+ ref: toggleRef,
2404
+ type: "button",
2405
+ className: "sc-priority-nav__toggle",
2406
+ "aria-expanded": expanded,
2407
+ "aria-controls": panelId,
2408
+ onClick: () => setOpen((o) => !o),
2409
+ children: [
2410
+ /* @__PURE__ */ jsx28(MoreHorizontalIcon, { className: "sc-priority-nav__ellipsis" }),
2411
+ /* @__PURE__ */ jsx28("span", { className: "sc-priority-nav__toggle-label", children: moreLabel }),
2412
+ /* @__PURE__ */ jsxs23("span", { className: "sc-visually-hidden", children: [
2413
+ ", ",
2414
+ moreCountLabel(overflow.length)
2415
+ ] }),
2416
+ /* @__PURE__ */ jsx28(ChevronDownIcon, { size: "sm", className: "sc-priority-nav__chevron" })
2417
+ ]
2418
+ }
2419
+ ),
2420
+ /* @__PURE__ */ jsx28("ul", { ref: panelRef, id: panelId, className: "sc-priority-nav__panel", hidden: !expanded, children: overflow.map((item) => /* @__PURE__ */ jsx28("li", { children: link(item, true) }, keyOf(item))) })
2421
+ ]
2422
+ }
2423
+ )
2424
+ ] })
2425
+ }
2426
+ );
2427
+ }
2428
+
2429
+ // src/components/Popover/Popover.tsx
2430
+ import { Popover as BasePopover } from "@base-ui/react/popover";
2431
+ import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
2432
+ function Root6({ open, defaultOpen, onOpenChange, modal = false, children }) {
2433
+ return /* @__PURE__ */ jsx29(
2434
+ BasePopover.Root,
2435
+ {
2436
+ open,
2437
+ defaultOpen,
2438
+ onOpenChange: onOpenChange ? (next) => onOpenChange(next) : void 0,
2439
+ modal,
2440
+ children
2441
+ }
2442
+ );
2443
+ }
2444
+ function Trigger3({ render, className, children, ...rest }) {
2445
+ return /* @__PURE__ */ jsx29(
2446
+ BasePopover.Trigger,
2447
+ {
2448
+ render: render ?? /* @__PURE__ */ jsx29(Button, { variant: "secondary", className }),
2449
+ className: render ? className : void 0,
2450
+ ...rest,
2451
+ children
2452
+ }
2453
+ );
2454
+ }
2455
+ function Content3({
2456
+ side = "bottom",
2457
+ align = "center",
2458
+ sideOffset,
2459
+ arrow = false,
2460
+ className,
2461
+ children,
2462
+ ...rest
2463
+ }) {
2464
+ return /* @__PURE__ */ jsx29(BasePopover.Portal, { children: /* @__PURE__ */ jsx29(
2465
+ BasePopover.Positioner,
2466
+ {
2467
+ side,
2468
+ align,
2469
+ sideOffset: sideOffset ?? (arrow ? 8 : 4),
2470
+ className: "sc-popover__positioner",
2471
+ children: /* @__PURE__ */ jsxs24(BasePopover.Popup, { className: cx("sc-popover", className), ...rest, children: [
2472
+ arrow ? /* @__PURE__ */ jsx29(BasePopover.Arrow, { className: "sc-popover__arrow" }) : null,
2473
+ children
2474
+ ] })
2475
+ }
2476
+ ) });
2477
+ }
2478
+ function Title2({ className, ...rest }) {
2479
+ return /* @__PURE__ */ jsx29(BasePopover.Title, { className: cx("sc-popover__title", className), ...rest });
2480
+ }
2481
+ function Description2({ className, ...rest }) {
2482
+ return /* @__PURE__ */ jsx29(BasePopover.Description, { className: cx("sc-popover__description", className), ...rest });
2483
+ }
2484
+ function Close2({ className, children, render, ...rest }) {
2485
+ if (children === void 0 && render === void 0) {
2486
+ return /* @__PURE__ */ jsx29(BasePopover.Close, { className: cx("sc-popover__close", className), "aria-label": "Close", ...rest, children: /* @__PURE__ */ jsx29(XIcon, {}) });
2487
+ }
2488
+ return /* @__PURE__ */ jsx29(BasePopover.Close, { className, render, ...rest, children });
2489
+ }
2490
+ var Popover = { Root: Root6, Trigger: Trigger3, Content: Content3, Title: Title2, Description: Description2, Close: Close2 };
2491
+
2492
+ // src/components/BroomIcon/BroomIcon.tsx
2493
+ import { jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
2494
+ function BroomIcon({ sweeping = false, size = "lg", title, className, ...rest }) {
2495
+ return /* @__PURE__ */ jsxs25(
2496
+ "svg",
2497
+ {
2498
+ className: cx("sc-icon", "sc-broom", className),
2499
+ "data-size": size,
2500
+ "data-sweeping": sweeping || void 0,
2501
+ viewBox: "0 0 24 24",
2502
+ fill: "none",
2503
+ focusable: "false",
2504
+ "aria-hidden": title ? void 0 : true,
2505
+ role: title ? "img" : void 0,
2506
+ "aria-label": title,
2507
+ ...rest,
2508
+ children: [
2509
+ /* @__PURE__ */ jsxs25(
2510
+ "g",
2511
+ {
2512
+ className: "sc-broom__body",
2513
+ fill: "none",
2514
+ stroke: "currentColor",
2515
+ strokeLinecap: "round",
2516
+ strokeLinejoin: "round",
2517
+ children: [
2518
+ /* @__PURE__ */ jsx30("path", { d: "M16.1 10.48 L19.36 1.86 A0.87 0.87 0 0 1 20.98 2.47 L17.84 10.78" }),
2519
+ /* @__PURE__ */ jsx30("path", { d: "M13.33 12 C14.23 10.67 16.73 9.87 18.17 11 C19.07 12.67 18.93 18.33 18.4 21.4 C16.07 22.4 11.07 22.07 6.4 18.07 C9.07 16.67 11.8 15 13.33 12 Z" }),
2520
+ /* @__PURE__ */ jsx30("path", { className: "sc-broom__band", d: "M13.47 12.13 L18.4 13.5 L18.33 14.1 L13.27 12.77 Z" }),
2521
+ /* @__PURE__ */ jsx30("path", { d: "M12.87 13.73 L14.4 14.47 M16.2 15.27 L18.47 15.87" }),
2522
+ /* @__PURE__ */ jsx30("path", { d: "M12.8 16.87 C11.86 18.24 10.75 19.38 9.57 20.23 M15.6 17.4 C15.22 18.96 14.6 20.44 13.77 21.71" })
2523
+ ]
2524
+ }
2525
+ ),
2526
+ /* @__PURE__ */ jsxs25("g", { className: "sc-broom__dust", fill: "none", stroke: "currentColor", children: [
2527
+ /* @__PURE__ */ jsx30("circle", { cx: "4.77", cy: "13.53", r: "0.5" }),
2528
+ /* @__PURE__ */ jsx30("circle", { cx: "2.7", cy: "15.53", r: "0.87" }),
2529
+ /* @__PURE__ */ jsx30("circle", { cx: "4.1", cy: "18.27", r: "0.5" })
2530
+ ] })
2531
+ ]
2532
+ }
2533
+ );
2534
+ }
2535
+
1737
2536
  // src/index.ts
1738
2537
  var version = "0.1.1";
1739
2538
  export {
@@ -1746,6 +2545,7 @@ export {
1746
2545
  Banner,
1747
2546
  BellIcon,
1748
2547
  BriefcaseIcon,
2548
+ BroomIcon,
1749
2549
  Button,
1750
2550
  CalendarIcon,
1751
2551
  Card,
@@ -1768,6 +2568,7 @@ export {
1768
2568
  EmptyState,
1769
2569
  ExternalLinkIcon,
1770
2570
  EyeIcon,
2571
+ FileInput,
1771
2572
  FilterIcon,
1772
2573
  FlagIcon,
1773
2574
  Heading,
@@ -1790,6 +2591,8 @@ export {
1790
2591
  PauseIcon,
1791
2592
  PlayIcon,
1792
2593
  PlusIcon,
2594
+ Popover,
2595
+ PriorityNav,
1793
2596
  Progress,
1794
2597
  RefreshIcon,
1795
2598
  SearchIcon,