@apliteni/apliteni-ui 0.58.2 → 0.59.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.58.2",
3
+ "version": "0.59.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -18,9 +18,17 @@
18
18
  "access": "public"
19
19
  },
20
20
  "exports": {
21
- ".": "./src/index.js",
21
+ ".": {
22
+ "types": "./src/index.d.ts",
23
+ "import": "./src/index.js",
24
+ "default": "./src/index.js"
25
+ },
22
26
  "./guidelines/*": "./guidelines/*",
23
- "./motion": "./src/motion.js",
27
+ "./motion": {
28
+ "types": "./src/motion.d.ts",
29
+ "import": "./src/motion.js",
30
+ "default": "./src/motion.js"
31
+ },
24
32
  "./inline": "./src/inline.js",
25
33
  "./css": "./src/index.css",
26
34
  "./tokens": "./src/tokens/tokens.css",
@@ -465,7 +465,7 @@ type FilterBarProps = {
465
465
  onClear: () => void;
466
466
  onChange: (id: string, value: string | undefined) => void;
467
467
  };
468
- declare function FilterBar(props: FilterBarProps): react.JSX.Element;
468
+ declare function FilterBar({ filters, label, clearLabel, disabled, busy, onRemove, onClear, onChange }: FilterBarProps): react.JSX.Element;
469
469
 
470
470
  type KeyValueRow = {
471
471
  label: string;
@@ -1796,13 +1796,18 @@ function BackLink({
1796
1796
  }
1797
1797
 
1798
1798
  // src/TableValues.tsx
1799
- import { numericValue, deltaValue } from "@apliteni/apliteni-ui";
1799
+ import { formatNumericValue, formatDeltaValue } from "@apliteni/apliteni-ui";
1800
1800
  import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
1801
1801
  function NumericValue(props) {
1802
- return /* @__PURE__ */ jsx17("span", { dangerouslySetInnerHTML: { __html: numericValue(props) } });
1802
+ const value = formatNumericValue(props);
1803
+ return /* @__PURE__ */ jsx17("span", { children: /* @__PURE__ */ jsxs15("span", { className: "ui-value", "aria-label": value.missing, children: [
1804
+ value.text,
1805
+ value.unit && /* @__PURE__ */ jsx17("span", { className: "ui-value__unit", children: value.unit })
1806
+ ] }) });
1803
1807
  }
1804
1808
  function DeltaValue(props) {
1805
- return /* @__PURE__ */ jsx17("span", { dangerouslySetInnerHTML: { __html: deltaValue(props) } });
1809
+ const value = formatDeltaValue(props);
1810
+ return /* @__PURE__ */ jsx17("span", { children: /* @__PURE__ */ jsx17("span", { className: value.className, "aria-describedby": value.basisId || void 0, children: value.text }) });
1806
1811
  }
1807
1812
  function RowIdentity({ symbol, name, logo, href }) {
1808
1813
  const Tag = href ? "a" : "span";
@@ -1852,44 +1857,95 @@ function Segmented({ options, value, onChange, label, appearance = "pill", disab
1852
1857
  }
1853
1858
 
1854
1859
  // src/FilterBar.tsx
1855
- import { useLayoutEffect as useLayoutEffect4, useRef as useRef11 } from "react";
1856
- import { filterBar, initFilterBar } from "@apliteni/apliteni-ui";
1857
- import { jsx as jsx19 } from "react/jsx-runtime";
1858
- function FilterBar(props) {
1859
- const host = useRef11(null);
1860
- const latest = useRef11(props);
1861
- latest.current = props;
1862
- const controller = useRef11(null);
1863
- const initial = useRef11({ __html: filterBar(props) });
1864
- useLayoutEffect4(() => {
1865
- const el = host.current;
1866
- controller.current = initFilterBar(el, latest.current);
1867
- const remove = (e) => latest.current.onRemove(e.detail.id);
1868
- const clear = () => latest.current.onClear();
1869
- const change = (e) => {
1870
- const { id, value } = e.detail;
1871
- latest.current.onChange(id, value);
1872
- };
1873
- el.addEventListener("ui-filter-remove", remove);
1874
- el.addEventListener("ui-filter-clear", clear);
1875
- el.addEventListener("ui-filter-change", change);
1876
- return () => {
1877
- controller.current?.destroy();
1878
- el.removeEventListener("ui-filter-remove", remove);
1879
- el.removeEventListener("ui-filter-clear", clear);
1880
- el.removeEventListener("ui-filter-change", change);
1881
- };
1882
- }, []);
1883
- useLayoutEffect4(() => {
1884
- controller.current?.update(props);
1885
- }, [props.filters, props.label, props.clearLabel, props.disabled, props.busy]);
1886
- return /* @__PURE__ */ jsx19("div", { ref: host, dangerouslySetInnerHTML: initial.current });
1860
+ import { useRef as useRef11, useState as useState7 } from "react";
1861
+ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1862
+ function FilterBar({ filters, label = "Filters", clearLabel = "Clear all filters", disabled = false, busy = false, onRemove, onClear, onChange }) {
1863
+ const bar = useRef11(null);
1864
+ const focused = useRef11(null);
1865
+ const [opened, setOpened] = useState7({});
1866
+ const blocked = disabled || busy;
1867
+ useIsoLayoutEffect(() => {
1868
+ const stale = Object.keys(opened).filter((id) => !filters.some((filter) => filter.id === id && filter.open === opened[id].against));
1869
+ if (stale.length) setOpened((previous) => Object.fromEntries(Object.entries(previous).filter(([id]) => !stale.includes(id))));
1870
+ const prior = focused.current;
1871
+ if (!prior || prior.id === null && filters.length > 0 || filters.some((f) => f.id === prior.id)) return;
1872
+ const chips = Array.from(bar.current?.querySelectorAll("[data-filter-id]") ?? []);
1873
+ const next = chips.slice(prior.index).find((chip) => !chip.disabled) || chips.slice(0, prior.index).reverse().find((chip) => !chip.disabled);
1874
+ const target = !blocked && next?.querySelector("[data-dropdown-trigger]");
1875
+ focused.current = null;
1876
+ (target || bar.current)?.focus();
1877
+ });
1878
+ return /* @__PURE__ */ jsx19("div", { children: /* @__PURE__ */ jsxs16(
1879
+ "fieldset",
1880
+ {
1881
+ ref: bar,
1882
+ className: "ui-filter-bar",
1883
+ "data-filter-bar": "",
1884
+ disabled: blocked,
1885
+ "aria-busy": busy || void 0,
1886
+ tabIndex: -1,
1887
+ onBlur: (event) => {
1888
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) focused.current = null;
1889
+ },
1890
+ children: [
1891
+ /* @__PURE__ */ jsx19("legend", { className: "ui-filter-bar__legend", children: label }),
1892
+ filters.map((filter, index) => /* @__PURE__ */ jsxs16(
1893
+ "fieldset",
1894
+ {
1895
+ className: "ui-filter-bar__chip",
1896
+ "data-filter-id": filter.id,
1897
+ disabled: filter.disabled,
1898
+ onFocus: () => {
1899
+ focused.current = { id: filter.id, index };
1900
+ },
1901
+ children: [
1902
+ /* @__PURE__ */ jsx19("legend", { className: "ui-filter-bar__legend", children: filter.label }),
1903
+ /* @__PURE__ */ jsx19(
1904
+ Dropdown,
1905
+ {
1906
+ label: filter.label,
1907
+ value: filter.value,
1908
+ items: filter.items,
1909
+ variant: "select",
1910
+ ariaLabel: `${filter.label}: ${filter.value}`,
1911
+ open: !blocked && !filter.disabled && (opened[filter.id] && opened[filter.id].against === filter.open ? opened[filter.id].open : !!filter.open),
1912
+ onOpenChange: (open) => setOpened((previous) => ({ ...previous, [filter.id]: { open, against: filter.open } })),
1913
+ onSelect: (value) => {
1914
+ if (!blocked && !filter.disabled) onChange(filter.id, value == null ? void 0 : String(value));
1915
+ }
1916
+ }
1917
+ ),
1918
+ /* @__PURE__ */ jsx19(
1919
+ "button",
1920
+ {
1921
+ type: "button",
1922
+ className: "ui-filter-bar__remove",
1923
+ "data-filter-remove": "",
1924
+ "aria-label": `Remove ${filter.label} filter`,
1925
+ onClick: () => {
1926
+ if (!blocked && !filter.disabled) onRemove(filter.id);
1927
+ },
1928
+ children: "\xD7"
1929
+ }
1930
+ )
1931
+ ]
1932
+ },
1933
+ filter.id
1934
+ )),
1935
+ /* @__PURE__ */ jsx19("span", { "data-filter-clear": "", onFocus: () => {
1936
+ focused.current = { id: null, index: 0 };
1937
+ }, children: /* @__PURE__ */ jsx19(Button, { size: "sm", variant: "ghost", disabled: !filters.length, onClick: () => {
1938
+ if (!blocked) onClear();
1939
+ }, children: clearLabel }) })
1940
+ ]
1941
+ }
1942
+ ) });
1887
1943
  }
1888
1944
 
1889
1945
  // src/KeyValueList.tsx
1890
- import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
1946
+ import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
1891
1947
  function detail({ value, redacted }) {
1892
- if (redacted) return /* @__PURE__ */ jsxs16("span", { className: "ui-kv__hidden", children: [
1948
+ if (redacted) return /* @__PURE__ */ jsxs17("span", { className: "ui-kv__hidden", children: [
1893
1949
  /* @__PURE__ */ jsx20(Icon, { name: "eyeOff" }),
1894
1950
  "Hidden"
1895
1951
  ] });
@@ -1897,24 +1953,24 @@ function detail({ value, redacted }) {
1897
1953
  return typeof value === "boolean" ? String(value) : value;
1898
1954
  }
1899
1955
  function KeyValueList({ rows, columns = 1, className, ...rest }) {
1900
- return /* @__PURE__ */ jsx20("dl", { ...rest, className: ["ui-kv", columns === 2 && "ui-kv--two-columns", className].filter(Boolean).join(" "), children: rows.map((row, index) => /* @__PURE__ */ jsxs16("div", { className: "ui-drawer__row", children: [
1956
+ return /* @__PURE__ */ jsx20("dl", { ...rest, className: ["ui-kv", columns === 2 && "ui-kv--two-columns", className].filter(Boolean).join(" "), children: rows.map((row, index) => /* @__PURE__ */ jsxs17("div", { className: "ui-drawer__row", children: [
1901
1957
  /* @__PURE__ */ jsx20("dt", { children: row.label }),
1902
1958
  /* @__PURE__ */ jsx20("dd", { children: detail(row) })
1903
1959
  ] }, index)) });
1904
1960
  }
1905
1961
  function DrawerSection({ title, headingLevel = 3, className, children, ...rest }) {
1906
1962
  const Heading = `h${headingLevel}`;
1907
- return /* @__PURE__ */ jsxs16("section", { ...rest, className: ["ui-drawer__section", className].filter(Boolean).join(" "), children: [
1963
+ return /* @__PURE__ */ jsxs17("section", { ...rest, className: ["ui-drawer__section", className].filter(Boolean).join(" "), children: [
1908
1964
  title != null && /* @__PURE__ */ jsx20(Heading, { className: "ui-drawer__section-title", children: title }),
1909
1965
  children
1910
1966
  ] });
1911
1967
  }
1912
1968
 
1913
1969
  // src/Snippet.tsx
1914
- import { useEffect as useEffect7, useRef as useRef12, useState as useState7 } from "react";
1915
- import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
1970
+ import { useEffect as useEffect7, useRef as useRef12, useState as useState8 } from "react";
1971
+ import { jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
1916
1972
  function Snippet({ label = "shell", code = "", reveal = false, copyLabel = "Copy" }) {
1917
- const [status, setStatus] = useState7("idle");
1973
+ const [status, setStatus] = useState8("idle");
1918
1974
  const request = useRef12(0);
1919
1975
  const timer = useRef12(void 0);
1920
1976
  useEffect7(() => {
@@ -1937,10 +1993,10 @@ function Snippet({ label = "shell", code = "", reveal = false, copyLabel = "Copy
1937
1993
  if (current === request.current) setStatus("failed");
1938
1994
  }
1939
1995
  }
1940
- return /* @__PURE__ */ jsxs17("div", { className: reveal ? "ui-snippet ui-snippet--reveal" : "ui-snippet", children: [
1941
- /* @__PURE__ */ jsxs17("div", { className: "ui-snippet__bar", children: [
1996
+ return /* @__PURE__ */ jsxs18("div", { className: reveal ? "ui-snippet ui-snippet--reveal" : "ui-snippet", children: [
1997
+ /* @__PURE__ */ jsxs18("div", { className: "ui-snippet__bar", children: [
1942
1998
  /* @__PURE__ */ jsx21("span", { children: label }),
1943
- /* @__PURE__ */ jsxs17("button", { type: "button", className: "ui-snippet__copy", "aria-live": "polite", "aria-atomic": "true", onClick: copy, children: [
1999
+ /* @__PURE__ */ jsxs18("button", { type: "button", className: "ui-snippet__copy", "aria-live": "polite", "aria-atomic": "true", onClick: copy, children: [
1944
2000
  /* @__PURE__ */ jsx21(Icon, { name: status === "copied" ? "check" : "copy" }),
1945
2001
  status === "copied" ? "Copied" : status === "failed" ? "Copy failed" : copyLabel
1946
2002
  ] })
@@ -1952,7 +2008,7 @@ function Snippet({ label = "shell", code = "", reveal = false, copyLabel = "Copy
1952
2008
  // src/Tabs.tsx
1953
2009
  import { useEffect as useEffect8, useId as useId9, useRef as useRef13 } from "react";
1954
2010
  import { playEntrance } from "@apliteni/apliteni-ui/motion";
1955
- import { Fragment as Fragment6, jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
2011
+ import { Fragment as Fragment6, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
1956
2012
  function Tabs({ items, value, onChange, label }) {
1957
2013
  const id = useId9();
1958
2014
  const previous = useRef13(value);
@@ -1962,8 +2018,8 @@ function Tabs({ items, value, onChange, label }) {
1962
2018
  if (previous.current !== value) playEntrance(activePanel.current);
1963
2019
  previous.current = value;
1964
2020
  }, [value]);
1965
- return /* @__PURE__ */ jsxs18("div", { className: "ui-tabs", children: [
1966
- /* @__PURE__ */ jsx22("div", { className: "ui-tabs__list", role: "tablist", "aria-label": label, children: items.map((item, i) => /* @__PURE__ */ jsxs18(
2021
+ return /* @__PURE__ */ jsxs19("div", { className: "ui-tabs", children: [
2022
+ /* @__PURE__ */ jsx22("div", { className: "ui-tabs__list", role: "tablist", "aria-label": label, children: items.map((item, i) => /* @__PURE__ */ jsxs19(
1967
2023
  "button",
1968
2024
  {
1969
2025
  type: "button",
@@ -2001,7 +2057,7 @@ function Tabs({ items, value, onChange, label }) {
2001
2057
  },
2002
2058
  children: [
2003
2059
  item.label,
2004
- item.count != null && /* @__PURE__ */ jsxs18(Fragment6, { children: [
2060
+ item.count != null && /* @__PURE__ */ jsxs19(Fragment6, { children: [
2005
2061
  " ",
2006
2062
  item.count
2007
2063
  ] })
@@ -2029,7 +2085,7 @@ function Tabs({ items, value, onChange, label }) {
2029
2085
  // src/Timeline.tsx
2030
2086
  import { useEffect as useEffect9, useRef as useRef14 } from "react";
2031
2087
  import { playEntrance as playEntrance2, prefersReducedMotion } from "@apliteni/apliteni-ui/motion";
2032
- import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
2088
+ import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
2033
2089
  var kindIcons = { person: "user", rule: "bolt", reversal: "refresh" };
2034
2090
  function Timeline({ events, className, ...rest }) {
2035
2091
  const previousIds = useRef14(new Set(events.map((event) => event.id)));
@@ -2046,7 +2102,7 @@ function Timeline({ events, className, ...rest }) {
2046
2102
  }
2047
2103
  previousIds.current = new Set(events.map((event) => event.id));
2048
2104
  }, [events]);
2049
- return /* @__PURE__ */ jsx23("ol", { ...rest, role: "list", className: ["ui-timeline", events.some((event) => event.kind) && "ui-timeline--kinds", className].filter(Boolean).join(" "), children: events.map((event, index) => /* @__PURE__ */ jsxs19(
2105
+ return /* @__PURE__ */ jsx23("ol", { ...rest, role: "list", className: ["ui-timeline", events.some((event) => event.kind) && "ui-timeline--kinds", className].filter(Boolean).join(" "), children: events.map((event, index) => /* @__PURE__ */ jsxs20(
2050
2106
  "li",
2051
2107
  {
2052
2108
  className: "ui-timeline__event",
@@ -2060,13 +2116,13 @@ function Timeline({ events, className, ...rest }) {
2060
2116
  event.kind && "ui-timeline__marker--kind",
2061
2117
  event.kind === "reversal" && "ui-timeline__marker--reversal"
2062
2118
  ].filter(Boolean).join(" "), children: event.kind && /* @__PURE__ */ jsx23(Icon, { name: kindIcons[event.kind] }) }),
2063
- /* @__PURE__ */ jsxs19("div", { className: "ui-timeline__head", children: [
2119
+ /* @__PURE__ */ jsxs20("div", { className: "ui-timeline__head", children: [
2064
2120
  /* @__PURE__ */ jsx23("span", { className: "ui-timeline__actor", children: event.actor }),
2065
2121
  /* @__PURE__ */ jsx23("time", { dateTime: event.dateTime, children: event.timestamp }),
2066
2122
  index === events.length - 1 && event.relativeTimestamp && /* @__PURE__ */ jsx23("span", { className: "ui-timeline__relative", children: event.relativeTimestamp })
2067
2123
  ] }),
2068
2124
  /* @__PURE__ */ jsx23("p", { className: "ui-timeline__description", children: event.description }),
2069
- (event.meta != null || event.undo) && /* @__PURE__ */ jsxs19("div", { className: "ui-timeline__meta", children: [
2125
+ (event.meta != null || event.undo) && /* @__PURE__ */ jsxs20("div", { className: "ui-timeline__meta", children: [
2070
2126
  event.meta != null && /* @__PURE__ */ jsx23("span", { children: event.meta }),
2071
2127
  event.undo && /* @__PURE__ */ jsx23(Button, { variant: "ghost", size: "xs", className: "ui-btn ui-btn--ghost ui-btn--xs ui-timeline__undo", "aria-label": event.undo.label, onClick: event.undo.onUndo, children: "Undo" })
2072
2128
  ] })
@@ -2077,15 +2133,15 @@ function Timeline({ events, className, ...rest }) {
2077
2133
  }
2078
2134
 
2079
2135
  // src/Field.tsx
2080
- import { useEffect as useEffect10, useId as useId10, useRef as useRef15, useState as useState8 } from "react";
2136
+ import { useEffect as useEffect10, useId as useId10, useRef as useRef15, useState as useState9 } from "react";
2081
2137
  import { esc, icon as icon4 } from "@apliteni/apliteni-ui";
2082
- import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
2138
+ import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
2083
2139
  function messages(id, hint, error) {
2084
2140
  return [error && `${id}-error`, hint && `${id}-hint`].filter(Boolean).join(" ") || void 0;
2085
2141
  }
2086
2142
  function Frame({ id, label, hint, error, required, children }) {
2087
- return /* @__PURE__ */ jsxs20("div", { className: "ui-field", children: [
2088
- /* @__PURE__ */ jsxs20("label", { className: "ui-field__label", htmlFor: id, children: [
2143
+ return /* @__PURE__ */ jsxs21("div", { className: "ui-field", children: [
2144
+ /* @__PURE__ */ jsxs21("label", { className: "ui-field__label", htmlFor: id, children: [
2089
2145
  label,
2090
2146
  required && /* @__PURE__ */ jsx24("span", { className: "ui-field__req", "aria-hidden": "true", children: "*" })
2091
2147
  ] }),
@@ -2121,7 +2177,7 @@ function TextField({ label, hint, error, required, type = "text", unit, classNam
2121
2177
  className: `ui-input${error ? " is-invalid" : ""} ${className}`
2122
2178
  }
2123
2179
  );
2124
- return /* @__PURE__ */ jsx24(Frame, { ...{ id, label, hint, error, required }, children: hasUnit ? /* @__PURE__ */ jsxs20("div", { className: "ui-field-number", children: [
2180
+ return /* @__PURE__ */ jsx24(Frame, { ...{ id, label, hint, error, required }, children: hasUnit ? /* @__PURE__ */ jsxs21("div", { className: "ui-field-number", children: [
2125
2181
  control,
2126
2182
  /* @__PURE__ */ jsx24("span", { id: `${id}-unit`, className: "ui-field-number__unit", children: unit })
2127
2183
  ] }) : control });
@@ -2155,8 +2211,8 @@ function SelectField({ label, hint, error, required, className = "", children, .
2155
2211
  function FileField({ label, hint, error, required, disabled, onFileChange, className = "", ...props }) {
2156
2212
  const id = useId10();
2157
2213
  const input = useRef15(null);
2158
- const [file, setFile] = useState8(null);
2159
- const [dragging, setDragging] = useState8(false);
2214
+ const [file, setFile] = useState9(null);
2215
+ const [dragging, setDragging] = useState9(false);
2160
2216
  useEffect10(() => {
2161
2217
  const form = input.current?.form;
2162
2218
  const reset = () => {
@@ -2170,7 +2226,7 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
2170
2226
  setFile(next);
2171
2227
  onFileChange?.(next);
2172
2228
  };
2173
- return /* @__PURE__ */ jsx24(Frame, { ...{ id, label, hint, error, required }, children: /* @__PURE__ */ jsxs20(
2229
+ return /* @__PURE__ */ jsx24(Frame, { ...{ id, label, hint, error, required }, children: /* @__PURE__ */ jsxs21(
2174
2230
  "div",
2175
2231
  {
2176
2232
  className: `ui-file${dragging && !disabled ? " is-dragging" : ""}${file ? " has-file" : ""}${error ? " is-invalid" : ""}${disabled ? " is-disabled" : ""}`,
@@ -2194,7 +2250,7 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
2194
2250
  /* @__PURE__ */ jsx24("span", { className: "ui-file__glyph", children: /* @__PURE__ */ jsx24(Icon, { name: "upload" }) }),
2195
2251
  /* @__PURE__ */ jsx24("div", { "aria-live": "polite", children: file ? file.name : "Drop a file here" }),
2196
2252
  /* @__PURE__ */ jsx24("div", { children: "or choose one from your device" }),
2197
- /* @__PURE__ */ jsxs20("span", { className: "ui-file__choose", children: [
2253
+ /* @__PURE__ */ jsxs21("span", { className: "ui-file__choose", children: [
2198
2254
  /* @__PURE__ */ jsx24("span", { className: "ui-btn ui-btn--secondary ui-btn--sm", "aria-hidden": "true", "aria-disabled": disabled || void 0, children: file ? "Replace file" : "Choose file" }),
2199
2255
  /* @__PURE__ */ jsx24(
2200
2256
  "input",
@@ -2216,9 +2272,9 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
2216
2272
  }
2217
2273
 
2218
2274
  // src/Toast.tsx
2219
- import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect11, useLayoutEffect as useLayoutEffect5, useRef as useRef16, useState as useState9 } from "react";
2275
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect11, useLayoutEffect as useLayoutEffect4, useRef as useRef16, useState as useState10 } from "react";
2220
2276
  import { createPortal as createPortal4 } from "react-dom";
2221
- import { jsx as jsx25, jsxs as jsxs21 } from "react/jsx-runtime";
2277
+ import { jsx as jsx25, jsxs as jsxs22 } from "react/jsx-runtime";
2222
2278
  var Context = createContext2(null);
2223
2279
  var glyphs = { success: "circleCheck", danger: "circleX", warn: "circleAlert", info: "info", neutral: "bolt" };
2224
2280
  function useToast() {
@@ -2228,11 +2284,11 @@ function useToast() {
2228
2284
  }
2229
2285
  function Notice({ notice, remove }) {
2230
2286
  const root = useRef16(null);
2231
- const [leaving2, setLeaving] = useState9(false);
2287
+ const [leaving2, setLeaving] = useState10(false);
2232
2288
  const dismissed = useRef16(false);
2233
2289
  const remaining = useRef16(5e3);
2234
- const [hovered, setHovered] = useState9(false);
2235
- const [focused, setFocused] = useState9(false);
2290
+ const [hovered, setHovered] = useState10(false);
2291
+ const [focused, setFocused] = useState10(false);
2236
2292
  const paused = hovered || focused;
2237
2293
  const tone = notice.tone ?? "success";
2238
2294
  const dismiss = useCallback3(() => {
@@ -2264,7 +2320,7 @@ function Notice({ notice, remove }) {
2264
2320
  remaining.current = Math.max(0, remaining.current - (Date.now() - started));
2265
2321
  };
2266
2322
  }, [dismiss, leaving2, notice.action, paused]);
2267
- return /* @__PURE__ */ jsxs21(
2323
+ return /* @__PURE__ */ jsxs22(
2268
2324
  "div",
2269
2325
  {
2270
2326
  ref: root,
@@ -2279,7 +2335,7 @@ function Notice({ notice, remove }) {
2279
2335
  },
2280
2336
  children: [
2281
2337
  /* @__PURE__ */ jsx25("span", { className: "ui-toast__icon", children: /* @__PURE__ */ jsx25(Icon, { name: glyphs[tone] }) }),
2282
- /* @__PURE__ */ jsxs21("div", { className: "ui-toast__body", children: [
2338
+ /* @__PURE__ */ jsxs22("div", { className: "ui-toast__body", children: [
2283
2339
  /* @__PURE__ */ jsx25("div", { className: "ui-toast__title", children: notice.title }),
2284
2340
  notice.text && /* @__PURE__ */ jsx25("div", { className: "ui-toast__text", children: notice.text })
2285
2341
  ] }),
@@ -2313,7 +2369,7 @@ function Notice({ notice, remove }) {
2313
2369
  }
2314
2370
  var REACH = "--rx-toast-stack";
2315
2371
  function usePublishedReach(stack2, count) {
2316
- useLayoutEffect5(() => {
2372
+ useLayoutEffect4(() => {
2317
2373
  const element = stack2.current;
2318
2374
  const view = element?.ownerDocument.defaultView;
2319
2375
  const root = element?.ownerDocument.documentElement;
@@ -2332,7 +2388,7 @@ function usePublishedReach(stack2, count) {
2332
2388
  }, [stack2, count]);
2333
2389
  }
2334
2390
  function Toast({ children }) {
2335
- const [entries, setEntries] = useState9([]);
2391
+ const [entries, setEntries] = useState10([]);
2336
2392
  const nextId = useRef16(0);
2337
2393
  const stack2 = useRef16(null);
2338
2394
  const push = useCallback3((notice) => {
@@ -2341,7 +2397,7 @@ function Toast({ children }) {
2341
2397
  setEntries((current) => [...current, { id, notice, remove }]);
2342
2398
  }, []);
2343
2399
  usePublishedReach(stack2, entries.length);
2344
- return /* @__PURE__ */ jsxs21(Context.Provider, { value: push, children: [
2400
+ return /* @__PURE__ */ jsxs22(Context.Provider, { value: push, children: [
2345
2401
  children,
2346
2402
  typeof document !== "undefined" && createPortal4(
2347
2403
  /* @__PURE__ */ jsx25("div", { className: "ui-toast-stack rx-toast-stack", ref: stack2, children: entries.map((entry) => /* @__PURE__ */ jsx25(Notice, { notice: entry.notice, remove: entry.remove }, entry.id)) }),
@@ -2351,9 +2407,9 @@ function Toast({ children }) {
2351
2407
  }
2352
2408
 
2353
2409
  // src/AppShell.tsx
2354
- import { useEffect as useEffect12, useId as useId11, useState as useState10 } from "react";
2410
+ import { useEffect as useEffect12, useId as useId11, useState as useState11 } from "react";
2355
2411
  import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName } from "@apliteni/apliteni-ui";
2356
- import { Fragment as Fragment7, jsx as jsx26, jsxs as jsxs22 } from "react/jsx-runtime";
2412
+ import { Fragment as Fragment7, jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
2357
2413
  var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
2358
2414
  function AppShell({
2359
2415
  sections,
@@ -2375,10 +2431,10 @@ function AppShell({
2375
2431
  renderLink
2376
2432
  }) {
2377
2433
  const uid = useId11();
2378
- const [collapsed, setCollapsed] = useState10(defaultCollapsed);
2379
- const [search, setSearch] = useState10(false);
2380
- const [more, setMore] = useState10(false);
2381
- const [theme, setTheme] = useState10("dark");
2434
+ const [collapsed, setCollapsed] = useState11(defaultCollapsed);
2435
+ const [search, setSearch] = useState11(false);
2436
+ const [more, setMore] = useState11(false);
2437
+ const [theme, setTheme] = useState11("dark");
2382
2438
  const current = path(pathname);
2383
2439
  const active = sections.filter((section) => current === path(section.href) || path(section.href) !== "/" && current.startsWith(`${path(section.href)}/`)).sort((a, b) => path(b.href).length - path(a.href).length)[0];
2384
2440
  useEffect12(() => {
@@ -2424,7 +2480,7 @@ function AppShell({
2424
2480
  onClick: (event) => {
2425
2481
  if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
2426
2482
  },
2427
- children: /* @__PURE__ */ jsxs22(Fragment7, { children: [
2483
+ children: /* @__PURE__ */ jsxs23(Fragment7, { children: [
2428
2484
  /* @__PURE__ */ jsx26("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx26(Icon, { name: section.icon }) }),
2429
2485
  /* @__PURE__ */ jsx26("span", { className: "ui-nav__label", children: section.label }),
2430
2486
  section.count != null && /* @__PURE__ */ jsx26("span", { className: "ui-nav__badge", children: section.count })
@@ -2436,24 +2492,24 @@ function AppShell({
2436
2492
  const visible = sections.length > 4 ? sections.slice(0, 3) : sections;
2437
2493
  const remaining = sections.slice(visible.length);
2438
2494
  const initials = account.name.trim().split(/\s+/).map((part) => part[0]).slice(0, 2).join("");
2439
- return /* @__PURE__ */ jsxs22("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
2440
- /* @__PURE__ */ jsxs22("div", { className: "ui-app__rail", children: [
2441
- /* @__PURE__ */ jsx26("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs22("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
2495
+ return /* @__PURE__ */ jsxs23("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
2496
+ /* @__PURE__ */ jsxs23("div", { className: "ui-app__rail", children: [
2497
+ /* @__PURE__ */ jsx26("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs23("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
2442
2498
  brand ?? /* @__PURE__ */ jsx26("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
2443
2499
  /* @__PURE__ */ jsx26("span", { children: word })
2444
2500
  ] }) }),
2445
2501
  /* @__PURE__ */ jsx26("nav", { className: "ui-nav ui-nav--side", "aria-label": "Sections", children: /* @__PURE__ */ jsx26("ul", { className: "ui-nav__list", children: links(sections) }) }),
2446
- /* @__PURE__ */ jsx26("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx26("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs22("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
2447
- /* @__PURE__ */ jsx26("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs22("svg", { className: "ui-icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2502
+ /* @__PURE__ */ jsx26("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx26("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs23("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
2503
+ /* @__PURE__ */ jsx26("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs23("svg", { className: "ui-icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2448
2504
  /* @__PURE__ */ jsx26("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
2449
2505
  /* @__PURE__ */ jsx26("path", { className: "ui-app__fold-seam", d: "M9 3v18" })
2450
2506
  ] }) }),
2451
2507
  /* @__PURE__ */ jsx26("span", { className: "ui-nav__label", children: foldLabel })
2452
2508
  ] }) }) })
2453
2509
  ] }),
2454
- /* @__PURE__ */ jsxs22("div", { className: "ui-app__well", children: [
2455
- /* @__PURE__ */ jsxs22("header", { className: "ui-app__bar", children: [
2456
- /* @__PURE__ */ jsxs22("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
2510
+ /* @__PURE__ */ jsxs23("div", { className: "ui-app__well", children: [
2511
+ /* @__PURE__ */ jsxs23("header", { className: "ui-app__bar", children: [
2512
+ /* @__PURE__ */ jsxs23("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
2457
2513
  /* @__PURE__ */ jsx26("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx26(Icon, { name: "search" }) }),
2458
2514
  /* @__PURE__ */ jsx26("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
2459
2515
  /* @__PURE__ */ jsx26("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
@@ -2471,7 +2527,7 @@ function AppShell({
2471
2527
  triggerClass: "ui-app__user-trigger",
2472
2528
  panelClass: "ui-app__user-panel",
2473
2529
  triggerContent: /* @__PURE__ */ jsx26("span", { className: "ui-app__av", "aria-hidden": "true", children: initials }),
2474
- header: /* @__PURE__ */ jsxs22("div", { className: "ui-dropdown__head", children: [
2530
+ header: /* @__PURE__ */ jsxs23("div", { className: "ui-dropdown__head", children: [
2475
2531
  /* @__PURE__ */ jsx26("b", { children: account.name }),
2476
2532
  /* @__PURE__ */ jsx26("span", { children: account.email })
2477
2533
  ] }),
@@ -2480,7 +2536,7 @@ function AppShell({
2480
2536
  }
2481
2537
  ) })
2482
2538
  ] }),
2483
- /* @__PURE__ */ jsxs22("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
2539
+ /* @__PURE__ */ jsxs23("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
2484
2540
  back && /* @__PURE__ */ jsx26(BackLink, { ...back }),
2485
2541
  /* @__PURE__ */ jsx26("h1", { children: title }),
2486
2542
  lede && /* @__PURE__ */ jsx26("p", { className: "ui-app__sub", children: lede }),
@@ -2488,9 +2544,9 @@ function AppShell({
2488
2544
  /* @__PURE__ */ jsx26("div", { className: "ui-app__body", children })
2489
2545
  ] })
2490
2546
  ] }),
2491
- /* @__PURE__ */ jsx26("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs22("ul", { className: "ui-nav__list", children: [
2547
+ /* @__PURE__ */ jsx26("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs23("ul", { className: "ui-nav__list", children: [
2492
2548
  links(visible),
2493
- remaining.length > 0 && /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsxs22(
2549
+ remaining.length > 0 && /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsxs23(
2494
2550
  "button",
2495
2551
  {
2496
2552
  type: "button",
@@ -2512,7 +2568,7 @@ function AppShell({
2512
2568
 
2513
2569
  // src/EmptyState.tsx
2514
2570
  import { icon as kitIcon2, illo } from "@apliteni/apliteni-ui";
2515
- import { jsx as jsx27, jsxs as jsxs23 } from "react/jsx-runtime";
2571
+ import { jsx as jsx27, jsxs as jsxs24 } from "react/jsx-runtime";
2516
2572
  var cases = {
2517
2573
  "first-run": { title: "Nothing needs you yet", sub: "New items will appear here when they need your attention.", icon: "check" },
2518
2574
  "no-matches": { title: "No results in this view", sub: "Try another search or clear your filters.", icon: "search" },
@@ -2524,7 +2580,7 @@ function EmptyState({ variant = "first-run", title, sub, icon: icon6, art, actio
2524
2580
  const heading = title ?? preset.title;
2525
2581
  const guidance = sub ?? preset.sub;
2526
2582
  const glyph = icon6 ?? preset.icon;
2527
- return /* @__PURE__ */ jsxs23("div", { className: "ui-empty", children: [
2583
+ return /* @__PURE__ */ jsxs24("div", { className: "ui-empty", children: [
2528
2584
  art ? typeof art === "string" ? /* @__PURE__ */ jsx27("div", { className: "ui-empty__art", dangerouslySetInnerHTML: { __html: /<svg/.test(art) ? art : illo(art) } }) : /* @__PURE__ */ jsx27("div", { className: "ui-empty__art", "aria-hidden": "true", children: art }) : glyph ? /* @__PURE__ */ jsx27("div", { className: "ui-empty__icon", dangerouslySetInnerHTML: { __html: kitIcon2(glyph) } }) : null,
2529
2585
  heading && /* @__PURE__ */ jsx27("div", { className: "ui-empty__title", children: heading }),
2530
2586
  guidance && /* @__PURE__ */ jsx27("div", { className: "ui-empty__sub", children: guidance }),
@@ -2535,7 +2591,7 @@ function EmptyState({ variant = "first-run", title, sub, icon: icon6, art, actio
2535
2591
  // src/ThemeToggle.tsx
2536
2592
  import { useSyncExternalStore } from "react";
2537
2593
  import { icon as icon5 } from "@apliteni/apliteni-ui";
2538
- import { jsx as jsx28, jsxs as jsxs24 } from "react/jsx-runtime";
2594
+ import { jsx as jsx28, jsxs as jsxs25 } from "react/jsx-runtime";
2539
2595
  var STORAGE_KEY = "apliteni-strategy-theme";
2540
2596
  var CHOICE_EVENT = "apliteni-theme-choice";
2541
2597
  var THEME_INIT_SCRIPT = `(()=>{let t;try{t=localStorage.getItem('apliteni-strategy-theme')}catch{}document.documentElement.setAttribute('data-theme',t==='dark'||t==='light'?t:window.matchMedia?.('(prefers-color-scheme: light)').matches?'light':'dark')})()`;
@@ -2594,9 +2650,9 @@ function ThemeToggle({ labelled = false }) {
2594
2650
  window.dispatchEvent(new Event(CHOICE_EVENT));
2595
2651
  };
2596
2652
  const button = /* @__PURE__ */ jsx28("button", { type: "button", className: "toggle", "aria-label": name, title: name, onClick: toggle, children: /* @__PURE__ */ jsx28("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: icon5(theme === "auto" ? "monitor" : theme === "light" ? "sun" : "moon") } }) });
2597
- return labelled ? /* @__PURE__ */ jsxs24("span", { className: "ui-theme-toggle", children: [
2653
+ return labelled ? /* @__PURE__ */ jsxs25("span", { className: "ui-theme-toggle", children: [
2598
2654
  button,
2599
- /* @__PURE__ */ jsxs24("span", { children: [
2655
+ /* @__PURE__ */ jsxs25("span", { children: [
2600
2656
  label,
2601
2657
  " theme"
2602
2658
  ] })
@@ -1,3 +1,5 @@
1
+ import { SCORE, scoreCommand, rankCommands, rankGroups, paletteHotkey, norm } from '../logic/command-palette.js';
2
+ export { SCORE, scoreCommand, rankCommands, rankGroups, paletteHotkey } from '../logic/command-palette.js';
1
3
  // Command palette — one overlay, a text box and a ranked list of things to run
2
4
  // or go to.
3
5
  //
@@ -16,134 +18,6 @@ const cx = (...a) => a.filter(Boolean).join(' ');
16
18
  let _uid = 0;
17
19
  const nextId = (p = 'cmdk') => `${p}-${++_uid}`;
18
20
 
19
- /**
20
- * How a match is worth more than another match.
21
- *
22
- * Six ways a query can meet an item, scored so that the whole beats the part
23
- * and the name beats the note. The numbers are a ladder and not a measurement —
24
- * what matters is the order, which is why src/components/command-palette.test.js
25
- * asserts the ORDER of the results rather than any of these values.
26
- *
27
- * cmdk's command-score is the other published answer: one continuous score
28
- * built out of SCORE_CONTINUE_MATCH 1, SCORE_SPACE_WORD_JUMP 0.9,
29
- * SCORE_NON_SPACE_WORD_JUMP 0.8 and SCORE_CHARACTER_JUMP 0.17. It ranks
30
- * beautifully and cannot be explained to a reader who asks why their item is
31
- * third. A ladder can: the kit's palettes hold tens of items, not thousands.
32
- */
33
- export const SCORE = {
34
- exact: 100,
35
- prefix: 90,
36
- wordStart: 80,
37
- contains: 70,
38
- keyword: 60,
39
- description: 40,
40
- subsequence: 20,
41
- };
42
-
43
- const norm = (s) => String(s == null ? '' : s).toLowerCase().replace(/\s+/g, ' ').trim();
44
-
45
- // Every character of `q`, in order, somewhere in `s` — "nc" finding "New
46
- // campaign". The weakest match the kit accepts, and the only one that can pair
47
- // a two-letter query with a twenty-letter label.
48
- function subsequence(s, q) {
49
- let at = 0;
50
- for (const ch of q) {
51
- at = s.indexOf(ch, at);
52
- if (at === -1) return false;
53
- at += 1;
54
- }
55
- return true;
56
- }
57
-
58
- // A word here starts after a space or one of the separators a product name uses,
59
- // so "camp" reaches the second word of "New campaign" and the "utm" in
60
- // "url-utm_source".
61
- const wordStarts = (s) => s.split(/[\s/\-_.:,]+/).filter(Boolean);
62
-
63
- function scoreField({ label, description, keywords }, q) {
64
- const name = norm(label);
65
- if (!name && !description && !(keywords || []).length) return 0;
66
- if (name === q) return SCORE.exact;
67
- if (name.startsWith(q)) return SCORE.prefix;
68
- if (wordStarts(name).some((w) => w.startsWith(q))) return SCORE.wordStart;
69
- if (name.includes(q)) return SCORE.contains;
70
- for (const k of keywords || []) {
71
- const key = norm(k);
72
- if (key === q || key.startsWith(q)) return SCORE.keyword;
73
- }
74
- if (norm(description).includes(q)) return SCORE.description;
75
- if (subsequence(name, q)) return SCORE.subsequence;
76
- return 0;
77
- }
78
-
79
- /**
80
- * What one item is worth against one query. 0 means it is not a result.
81
- *
82
- * An empty query scores everything the same, so a palette nobody has typed into
83
- * shows what the caller passed, in the caller's order — which is where a
84
- * product puts the four things somebody actually does here.
85
- *
86
- * A query with a space in it has to match as a whole OR token by token, every
87
- * token landing somewhere: "new camp" reaches "New campaign" as a whole, and
88
- * "camp new" reaches it one token at a time. The weakest token decides, because
89
- * a result is only as good as the part of the query it answers worst.
90
- */
91
- export function scoreCommand(item, query) {
92
- const q = norm(query);
93
- if (!q) return 1;
94
- const whole = scoreField(item, q);
95
- const tokens = q.split(' ');
96
- if (tokens.length < 2) return whole;
97
- let weakest = Infinity;
98
- for (const t of tokens) {
99
- const s = scoreField(item, t);
100
- if (!s) { weakest = 0; break; }
101
- weakest = Math.min(weakest, s);
102
- }
103
- return Math.max(whole, weakest === Infinity ? 0 : weakest);
104
- }
105
-
106
- /**
107
- * The items that answer `query`, best first.
108
- *
109
- * Ties keep the caller's order — that is the whole of the kit's ordering
110
- * opinion, and it is what lets a product put its four most-used commands at the
111
- * top of the list it passes and have them stay there.
112
- */
113
- export function rankCommands(items, query) {
114
- return (items || [])
115
- .map((item, i) => ({ item, i, score: scoreCommand(item, query) }))
116
- .filter((r) => r.score > 0)
117
- .sort((a, b) => b.score - a.score || a.i - b.i)
118
- .map((r) => r.item);
119
- }
120
-
121
- /**
122
- * The groups that answer `query`, best group first, each holding its own ranked
123
- * rows and nothing that scored zero.
124
- *
125
- * A group is carried by its best row, and the caller's order breaks the tie at
126
- * both levels — so a palette nobody has typed into is exactly the list that was
127
- * passed in. cmdk ranks groups the same way; the tie-break is the kit's, and it
128
- * is what keeps a heading from moving under a reader who has typed nothing.
129
- */
130
- export function rankGroups(groups, query) {
131
- return (groups || [])
132
- .map((g, i) => {
133
- const items = rankCommands(g.items || [], query);
134
- const best = items.length ? scoreCommand(items[0], query) : 0;
135
- return { group: { ...g, items }, i, best };
136
- })
137
- .filter((g) => g.group.items.length > 0)
138
- .sort((a, b) => b.best - a.best || a.i - b.i)
139
- .map((g) => g.group);
140
- }
141
-
142
- /** The label for the key that opens it, on the platform the caller is on. */
143
- export function paletteHotkey(platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
144
- return /mac|iphone|ipad|ipod/i.test(String(platform || '')) ? '⌘K' : 'Ctrl K';
145
- }
146
-
147
21
  // The three fields a query is matched against, written onto the row so the
148
22
  // wiring can re-rank what is already rendered without being handed the data a
149
23
  // second time. Three attributes and not one blob: the ranking weighs a name
@@ -1,3 +1,5 @@
1
+ import { dropdownMatch, dropdownFiltering } from '../logic/dropdown.js';
2
+ export { dropdownMatch, dropdownFiltering } from '../logic/dropdown.js';
1
3
  // Dropdown — the kit's one popover-list primitive. A trigger opens a panel of
2
4
  // item rows; two flavours share the same panel and the same open/close JS:
3
5
  //
@@ -79,22 +81,6 @@ function ddBody({ items, sections }, listbox, sx) {
79
81
  // why: docs/specification.md#a-dropdown-with-a-search-field
80
82
  // NFD takes the mark off é or ö; ł, ø, đ and the rest are letters of their own
81
83
  // with nothing to take off, so they are mapped by hand.
82
- const FOLD = { ł: 'l', ø: 'o', đ: 'd', ð: 'd', ß: 'ss', æ: 'ae', œ: 'oe', ı: 'i', þ: 'th' };
83
- const fold = (s) => String(s == null ? '' : s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
84
- .replace(/[łøđðßæœıþ]/g, (c) => FOLD[c]);
85
-
86
- /**
87
- * Is this query narrowing anything? A query of spaces, or of marks with no letter
88
- * under them, is not — it leaves every row showing, and the separators with them.
89
- *
90
- * Public because a second implementation of this dropdown cannot write its own: the
91
- * React <Dropdown> asks these two, the way <CommandPalette> asks rankGroups(), so a
92
- * list a server rendered and the same list after a keystroke hide the same rows.
93
- */
94
- export const dropdownFiltering = (query) => fold(query).trim() !== '';
95
-
96
- /** Does `label` match `query`? A substring, anywhere, ignoring case and accents. */
97
- export const dropdownMatch = (label, query) => fold(label).includes(fold(query).trim());
98
84
  const ddIsRow = (it) => it && it !== '---' && !it.separator;
99
85
 
100
86
  let _ddSeq = 0;
@@ -1,3 +1,5 @@
1
+ import { calloutIcons } from '../logic/callout.js';
2
+ export { calloutIcons } from '../logic/callout.js';
1
3
  // apliteni-ui component factories — each returns an HTML string, matching the
2
4
  // viz/ server-render idiom so the portal can adopt them with no framework.
3
5
  import { icon } from '../assets/icons.js';
@@ -218,7 +220,7 @@ export function switchToggle({ checked = false, disabled = false, name, label =
218
220
  }
219
221
 
220
222
  // ---- Callout / toast / success ------------------------------------------
221
- export const calloutIcons = { info: 'info', success: 'check', warn: 'alert', danger: 'alert', neutral: 'info' };
223
+
222
224
 
223
225
  export function callout({ variant, icon: ic = calloutIcons[variant] || 'info', body, actions } = {}) {
224
226
  return `<div class="${esc(cx('ui-callout', variant && variant !== 'neutral' && `ui-callout--${variant}`))}"><span class="ui-callout__icon">${icon(ic)}</span><div class="ui-callout__body">${body}${actions != null ? `<div class="ui-toolbar ui-callout__actions">${actions}</div>` : ''}</div></div>`;
@@ -1,3 +1,5 @@
1
+ import { PAGE_SIZES, DEFAULT_PAGE_SIZE } from '../logic/pagination.js';
2
+ export { PAGE_SIZES, DEFAULT_PAGE_SIZE } from '../logic/pagination.js';
1
3
  // Pagination — the strip under a table or a list, as an HTML string.
2
4
  //
3
5
  // It renders a page the CALLER computed. Rows never come in here: `page`,
@@ -15,8 +17,7 @@ import { safeUrl } from '../html.js';
15
17
  // AG Grid defaults to 100, and the finance portal's two data-heavy surfaces
16
18
  // already page at 100. A 250 step was in the first draft of this component and
17
19
  // no surveyed kit offers one, so it went rather than being invented.
18
- export const PAGE_SIZES = [25, 50, 100];
19
- export const DEFAULT_PAGE_SIZE = 100;
20
+
20
21
 
21
22
  // The classes button({ variant: 'ghost', size: 'sm' }) emits, written out because
22
23
  // button() takes no extra class and every control here needs one of its own for
@@ -1,12 +1,5 @@
1
- export function segmentedNextIndex(key, index, length) {
2
- if (!length) return null;
3
- if (key === 'ArrowRight' || key === 'ArrowDown') return (index + 1) % length;
4
- if (key === 'ArrowLeft' || key === 'ArrowUp') return (index - 1 + length) % length;
5
- if (key === 'Home') return 0;
6
- if (key === 'End') return length - 1;
7
- return null;
8
- }
9
-
1
+ import { segmentedNextIndex } from '../logic/segmented.js';
2
+ export { segmentedNextIndex } from '../logic/segmented.js';
10
3
  export function initSegmented(root = document) {
11
4
  const cleanups = [];
12
5
  root.querySelectorAll('[data-seg]').forEach(group => {
@@ -1,14 +1,14 @@
1
+ import { formatNumericValue, formatDeltaValue } from '../logic/table-values.js';
1
2
  import { esc } from './index.js';
2
3
  import { safeUrl } from '../html.js';
3
- export function numericValue({ value, unit = '', missing = 'Not available' } = {}) {
4
- if (value == null || value === '') return `<span class="ui-value" aria-label="${esc(missing)}">—</span>`;
5
- return `<span class="ui-value">${esc(value)}${unit ? `<span class="ui-value__unit">${esc(unit)}</span>` : ''}</span>`;
4
+ export function numericValue(options = {}) {
5
+ const value = formatNumericValue(options);
6
+ return `<span class="ui-value"${value.missing !== undefined ? ` aria-label="${esc(value.missing)}"` : ''}>${esc(value.text)}${value.unit ? `<span class="ui-value__unit">${esc(value.unit)}</span>` : ''}</span>`;
6
7
  }
7
8
 
8
- export function deltaValue({ value, tone = 'neutral', basisId, missing = 'No earlier figure' } = {}) {
9
- const present = value != null && value !== '';
10
- const judged = present && ['success', 'danger'].includes(tone) && !/^[+−-]?0+(?:[.,]0+)?(?:[^\d.,].*)?$/.test(String(value).replace(/\s/g, ''));
11
- return `<span class="ui-delta${judged ? ` ui-delta--${tone}` : ''}"${basisId ? ` aria-describedby="${esc(basisId)}"` : ''}>${esc(present ? value : missing)}</span>`;
9
+ export function deltaValue(options = {}) {
10
+ const value = formatDeltaValue(options);
11
+ return `<span class="${value.className}"${value.basisId ? ` aria-describedby="${esc(value.basisId)}"` : ''}>${esc(value.text)}</span>`;
12
12
  }
13
13
 
14
14
  export function rowIdentity({ symbol = '', name = '', logo, href } = {}) {
package/src/index.d.ts ADDED
@@ -0,0 +1,128 @@
1
+ export declare function esc(value: unknown): string;
2
+ export declare function field(opts?: Record<string, unknown>): string;
3
+ export declare function input(opts?: Record<string, unknown>): string;
4
+ export declare function textarea(opts?: Record<string, unknown>): string;
5
+ export declare function select(opts?: Record<string, unknown>): string;
6
+ export declare function numericValue(opts?: Record<string, unknown>): string;
7
+ export declare function deltaValue(opts?: Record<string, unknown>): string;
8
+ export declare function filterBar(opts?: Record<string, unknown>): string;
9
+ export declare function initFilterBar(host: Element, opts?: Record<string, unknown>): { update: (opts: Record<string, unknown>) => void; destroy: () => void };
10
+ export declare function segmentedNextIndex(key: string, index: number, length: number): number | null;
11
+ export declare function icon(name: string, className?: string): string;
12
+ export declare function snippet(opts?: Record<string, unknown>): string;
13
+ export declare function button(opts?: Record<string, unknown>): string;
14
+ export declare function badge(label: string, variant?: string): string;
15
+ export declare const calloutIcons: Record<'neutral' | 'info' | 'success' | 'warn' | 'danger', string>;
16
+ export declare function callout(opts?: Record<string, unknown>): string;
17
+ export declare function card(opts?: Record<string, unknown>): string;
18
+ export declare function pagination(opts?: Record<string, unknown>): string;
19
+ export declare function drawerSection(opts?: Record<string, unknown>): string;
20
+ export declare function drawer(opts?: Record<string, unknown>): string;
21
+ export declare function dropdown(opts?: Record<string, unknown>): string;
22
+ export declare function wireDropdown(root?: Document | Element): void;
23
+ /** The kit's own match, asked rather than re-implemented by <Dropdown>. */
24
+ export declare function dropdownMatch(label: unknown, query: unknown): boolean;
25
+ export declare function dropdownFiltering(query: unknown): boolean;
26
+ export declare function backLink(opts?: Record<string, unknown>): string;
27
+ export declare function statBand(opts?: Record<string, unknown>): string;
28
+ export declare function commandPalette(opts?: Record<string, unknown>): string;
29
+ export declare function rankGroups<G>(groups: readonly G[], query: string): G[];
30
+ export declare function rankCommands<I>(items: readonly I[], query: string): I[];
31
+ export declare function scoreCommand(item: Record<string, unknown>, query: string): number;
32
+ export declare function paletteHotkey(platform?: string): string;
33
+ export declare const PAGE_SIZES: number[];
34
+ export declare const DEFAULT_PAGE_SIZE: number;
35
+ export type NumericValueOptions = { value?: string | number | null; unit?: string; missing?: string };
36
+ export type DeltaValueOptions = { value?: string | null; tone?: 'success' | 'danger' | 'neutral'; basisId?: string; missing?: string };
37
+ export declare function formatNumericValue(options?: NumericValueOptions): { text: string; unit: string; missing: string | undefined };
38
+ export declare function formatDeltaValue(options?: DeltaValueOptions): { text: string; className: string; basisId: string | undefined };
39
+ export declare const SCORE: Record<'exact' | 'prefix' | 'wordStart' | 'contains' | 'keyword' | 'description' | 'subsequence', number>;
40
+ export declare const iconNames: string[];
41
+ export declare const iconCategories: { name: string; names: string[] }[];
42
+ export declare const iconOnlyAllowed: Record<string, string>;
43
+ export declare const iconMeanings: Record<string, string>;
44
+ export declare const sun: string;
45
+ export declare const moon: string;
46
+ export declare function prism(prefix?: string, size?: number): string;
47
+ export declare function seedling(prefix?: string, size?: number): string;
48
+ export declare function brand(options?: { p?: string; word?: string; size?: number; href?: string }): string;
49
+ export declare function illo(name: string): string;
50
+ export * from './motion.js';
51
+ export declare const illoNames: string[];
52
+
53
+ // Vanilla factories accept their existing option bags and return HTML.
54
+ export declare function accentPicker(opts?: Record<string, unknown>): string;
55
+ export declare function accountMenu(opts?: Record<string, unknown>): string;
56
+ export declare function accountShell(opts?: Record<string, unknown>): string;
57
+ export declare function appShell(opts?: Record<string, unknown>): string;
58
+ export declare function breadcrumbs(opts?: Record<string, unknown>): string;
59
+ export declare function busyRegion(opts?: Record<string, unknown>): string;
60
+ export declare function checkbox(opts?: Record<string, unknown>): string;
61
+ export declare function confirm(opts?: Record<string, unknown>): string;
62
+ export declare function deniedState(opts?: Record<string, unknown>): string;
63
+ export declare function emptyState(opts?: Record<string, unknown>): string;
64
+ export declare function feedbackWidget(opts?: Record<string, unknown>): string;
65
+ export declare function footer(opts?: Record<string, unknown>): string;
66
+ export declare function nav(opts?: Record<string, unknown>): string;
67
+ export declare function navTabs(opts?: Record<string, unknown>): string;
68
+ export declare function rowIdentity(opts?: Record<string, unknown>): string;
69
+ export declare function segmented(opts?: Record<string, unknown>): string;
70
+ export declare function sidebarNav(opts?: Record<string, unknown>): string;
71
+ export declare function skeleton(opts?: Record<string, unknown>): string;
72
+ export declare function skeletonTable(opts?: Record<string, unknown>): string;
73
+ export declare function success(opts?: Record<string, unknown>): string;
74
+ export declare function successPanel(opts?: Record<string, unknown>): string;
75
+ export declare function switchToggle(opts?: Record<string, unknown>): string;
76
+ export declare function tabs(opts?: Record<string, unknown>): string;
77
+ export declare function toast(opts?: Record<string, unknown>): string;
78
+ export declare function tooltip(opts?: Record<string, unknown>): string;
79
+ export declare function topbar(opts?: Record<string, unknown>): string;
80
+ export declare function commandPaletteList(groups?: readonly Record<string, unknown>[], opts?: { uid?: string; from?: number }): string;
81
+ export declare function deckTextSwitch(active?: string): string;
82
+ export declare function hlShell(raw: string): string;
83
+ export declare function pill(label: string, variant?: string): string;
84
+ export declare function statusDot(live?: boolean): string;
85
+ export declare function successCheck(): string;
86
+ export declare function themeIcon(theme: string): string;
87
+ export declare function themeName(theme: string): string;
88
+ export declare function themeToggle(theme?: string): string;
89
+ export declare function versionSwitcher(versions?: { label: string; meta?: string; badge?: string }[], activeIdx?: number): string;
90
+
91
+ export declare const ACCENTS: string[];
92
+ export declare const ACCOUNT_NAV: { id: string; icon: string; label: string }[];
93
+ export declare const RAIL_COOKIE: 'apliteni-ui-rail';
94
+ export declare const STAT_TONES: string[];
95
+ export declare const STAT_VARIANTS: string[];
96
+
97
+ export declare function applyAccent(name: string, root?: HTMLElement): void;
98
+ export declare function applyTheme(theme: string, root?: HTMLElement): void;
99
+ export declare function railCollapsed(cookies?: string): boolean | null;
100
+ export declare function closeCommandPalette(root: Element | null): void;
101
+ export declare function closeConfirm(root: Element | null): void;
102
+ export declare function closeDrawer(root: Element | null): void;
103
+ export declare function openCommandPalette(root: Element | null, returnFocusTo?: HTMLElement | null): void;
104
+ export declare function openConfirm(root: Element | null, returnFocusTo?: HTMLElement | null): void;
105
+ export declare function openDrawer(root: Element | null, returnFocusTo?: HTMLElement | null): void;
106
+ export declare function setPaletteResults(root: HTMLElement, groups: readonly Record<string, unknown>[]): void;
107
+ export declare function hideTooltip(host: HTMLElement): void;
108
+ export declare function showTooltip(host: HTMLElement, mark: Element): void;
109
+ export declare function initRowIdentity(root?: Document | Element): void;
110
+ export declare function initSegmented(root?: Document | Element): () => void;
111
+ export declare function initTabs(root?: Document | Element): void;
112
+ export declare function wireCommandPalette(root?: Document | Element): void;
113
+ export declare function wireConfirm(root?: Document | Element): void;
114
+ export declare function wireDrawer(root?: Document | Element): void;
115
+ export declare function wireFeedback(opts?: Record<string, unknown>): void;
116
+ export declare function wireNav(root?: Document | Element): void;
117
+ export declare function wireShell(root?: Document | Element, opts?: { persist?: boolean }): void;
118
+ export declare function wireTooltip(root?: Document | Element): void;
119
+ export declare function wireTopbar(root?: Document | Element): void;
120
+ export declare function wireSuccess(root: Element, opts?: { onDone?: () => void }): () => void;
121
+ export declare function wirePagination(root?: Document | Element | string | null, opts?: { onPage?: (page: number) => void; onPageSize?: (size: number) => void }): () => void;
122
+ export declare function nearestSection(node: Node | null, root?: Element | null): Element | null;
123
+ export declare function dismissToast(element: HTMLElement): void;
124
+ export declare function wireToastStack(container: Element | string | null): Element | null;
125
+ export declare function pushToast(container: Element | string | null, opts?: Record<string, unknown>): Element | null;
126
+ export declare function setBusy(root: Element | string | null, opts?: { busy?: boolean; message?: string; body?: string }): Element | null;
127
+ export declare function setButtonBusy(element: HTMLElement, opts?: { busy?: boolean }): void;
128
+ export declare function setPagerStatus(root: Element | string | null, text: string): Element | null;
package/src/index.js CHANGED
@@ -26,3 +26,10 @@ export { setButtonBusy } from './components/button-busy.js';
26
26
  export * from './components/table-values.js';
27
27
  export * from './components/segmented.js';
28
28
  export * from './components/filter-bar.js';
29
+
30
+ export { dropdownMatch, dropdownFiltering } from './logic/dropdown.js';
31
+ export { SCORE, rankGroups, rankCommands, scoreCommand, paletteHotkey } from './logic/command-palette.js';
32
+ export { segmentedNextIndex } from './logic/segmented.js';
33
+ export { PAGE_SIZES, DEFAULT_PAGE_SIZE } from './logic/pagination.js';
34
+ export { calloutIcons } from './logic/callout.js';
35
+ export { formatNumericValue, formatDeltaValue } from './logic/table-values.js';
@@ -0,0 +1 @@
1
+ export const calloutIcons = { info: 'info', success: 'check', warn: 'alert', danger: 'alert', neutral: 'info' };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * How a match is worth more than another match.
3
+ *
4
+ * Six ways a query can meet an item, scored so that the whole beats the part
5
+ * and the name beats the note. The numbers are a ladder and not a measurement —
6
+ * what matters is the order, which is why src/components/command-palette.test.js
7
+ * asserts the ORDER of the results rather than any of these values.
8
+ *
9
+ * cmdk's command-score is the other published answer: one continuous score
10
+ * built out of SCORE_CONTINUE_MATCH 1, SCORE_SPACE_WORD_JUMP 0.9,
11
+ * SCORE_NON_SPACE_WORD_JUMP 0.8 and SCORE_CHARACTER_JUMP 0.17. It ranks
12
+ * beautifully and cannot be explained to a reader who asks why their item is
13
+ * third. A ladder can: the kit's palettes hold tens of items, not thousands.
14
+ */
15
+ export const SCORE = {
16
+ exact: 100,
17
+ prefix: 90,
18
+ wordStart: 80,
19
+ contains: 70,
20
+ keyword: 60,
21
+ description: 40,
22
+ subsequence: 20,
23
+ };
24
+
25
+ export const norm = (s) => String(s == null ? '' : s).toLowerCase().replace(/\s+/g, ' ').trim();
26
+
27
+ // Every character of `q`, in order, somewhere in `s` — "nc" finding "New
28
+ // campaign". The weakest match the kit accepts, and the only one that can pair
29
+ // a two-letter query with a twenty-letter label.
30
+ function subsequence(s, q) {
31
+ let at = 0;
32
+ for (const ch of q) {
33
+ at = s.indexOf(ch, at);
34
+ if (at === -1) return false;
35
+ at += 1;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ // A word here starts after a space or one of the separators a product name uses,
41
+ // so "camp" reaches the second word of "New campaign" and the "utm" in
42
+ // "url-utm_source".
43
+ const wordStarts = (s) => s.split(/[\s/\-_.:,]+/).filter(Boolean);
44
+
45
+ function scoreField({ label, description, keywords }, q) {
46
+ const name = norm(label);
47
+ if (!name && !description && !(keywords || []).length) return 0;
48
+ if (name === q) return SCORE.exact;
49
+ if (name.startsWith(q)) return SCORE.prefix;
50
+ if (wordStarts(name).some((w) => w.startsWith(q))) return SCORE.wordStart;
51
+ if (name.includes(q)) return SCORE.contains;
52
+ for (const k of keywords || []) {
53
+ const key = norm(k);
54
+ if (key === q || key.startsWith(q)) return SCORE.keyword;
55
+ }
56
+ if (norm(description).includes(q)) return SCORE.description;
57
+ if (subsequence(name, q)) return SCORE.subsequence;
58
+ return 0;
59
+ }
60
+
61
+ /**
62
+ * What one item is worth against one query. 0 means it is not a result.
63
+ *
64
+ * An empty query scores everything the same, so a palette nobody has typed into
65
+ * shows what the caller passed, in the caller's order — which is where a
66
+ * product puts the four things somebody actually does here.
67
+ *
68
+ * A query with a space in it has to match as a whole OR token by token, every
69
+ * token landing somewhere: "new camp" reaches "New campaign" as a whole, and
70
+ * "camp new" reaches it one token at a time. The weakest token decides, because
71
+ * a result is only as good as the part of the query it answers worst.
72
+ */
73
+ export function scoreCommand(item, query) {
74
+ const q = norm(query);
75
+ if (!q) return 1;
76
+ const whole = scoreField(item, q);
77
+ const tokens = q.split(' ');
78
+ if (tokens.length < 2) return whole;
79
+ let weakest = Infinity;
80
+ for (const t of tokens) {
81
+ const s = scoreField(item, t);
82
+ if (!s) { weakest = 0; break; }
83
+ weakest = Math.min(weakest, s);
84
+ }
85
+ return Math.max(whole, weakest === Infinity ? 0 : weakest);
86
+ }
87
+
88
+ /**
89
+ * The items that answer `query`, best first.
90
+ *
91
+ * Ties keep the caller's order — that is the whole of the kit's ordering
92
+ * opinion, and it is what lets a product put its four most-used commands at the
93
+ * top of the list it passes and have them stay there.
94
+ */
95
+ export function rankCommands(items, query) {
96
+ return (items || [])
97
+ .map((item, i) => ({ item, i, score: scoreCommand(item, query) }))
98
+ .filter((r) => r.score > 0)
99
+ .sort((a, b) => b.score - a.score || a.i - b.i)
100
+ .map((r) => r.item);
101
+ }
102
+
103
+ /**
104
+ * The groups that answer `query`, best group first, each holding its own ranked
105
+ * rows and nothing that scored zero.
106
+ *
107
+ * A group is carried by its best row, and the caller's order breaks the tie at
108
+ * both levels — so a palette nobody has typed into is exactly the list that was
109
+ * passed in. cmdk ranks groups the same way; the tie-break is the kit's, and it
110
+ * is what keeps a heading from moving under a reader who has typed nothing.
111
+ */
112
+ export function rankGroups(groups, query) {
113
+ return (groups || [])
114
+ .map((g, i) => {
115
+ const items = rankCommands(g.items || [], query);
116
+ const best = items.length ? scoreCommand(items[0], query) : 0;
117
+ return { group: { ...g, items }, i, best };
118
+ })
119
+ .filter((g) => g.group.items.length > 0)
120
+ .sort((a, b) => b.best - a.best || a.i - b.i)
121
+ .map((g) => g.group);
122
+ }
123
+
124
+ /** The label for the key that opens it, on the platform the caller is on. */
125
+ export function paletteHotkey(platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
126
+ return /mac|iphone|ipad|ipod/i.test(String(platform || '')) ? '⌘K' : 'Ctrl K';
127
+ }
@@ -0,0 +1,16 @@
1
+ const FOLD = { ł: 'l', ø: 'o', đ: 'd', ð: 'd', ß: 'ss', æ: 'ae', œ: 'oe', ı: 'i', þ: 'th' };
2
+ const fold = (s) => String(s == null ? '' : s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
3
+ .replace(/[łøđðßæœıþ]/g, (c) => FOLD[c]);
4
+
5
+ /**
6
+ * Is this query narrowing anything? A query of spaces, or of marks with no letter
7
+ * under them, is not — it leaves every row showing, and the separators with them.
8
+ *
9
+ * Public because a second implementation of this dropdown cannot write its own: the
10
+ * React <Dropdown> asks these two, the way <CommandPalette> asks rankGroups(), so a
11
+ * list a server rendered and the same list after a keystroke hide the same rows.
12
+ */
13
+ export const dropdownFiltering = (query) => fold(query).trim() !== '';
14
+
15
+ /** Does `label` match `query`? A substring, anywhere, ignoring case and accents. */
16
+ export const dropdownMatch = (label, query) => fold(label).includes(fold(query).trim());
@@ -0,0 +1,2 @@
1
+ export const PAGE_SIZES = [25, 50, 100];
2
+ export const DEFAULT_PAGE_SIZE = 100;
@@ -0,0 +1,8 @@
1
+ export function segmentedNextIndex(key, index, length) {
2
+ if (!length) return null;
3
+ if (key === 'ArrowRight' || key === 'ArrowDown') return (index + 1) % length;
4
+ if (key === 'ArrowLeft' || key === 'ArrowUp') return (index - 1 + length) % length;
5
+ if (key === 'Home') return 0;
6
+ if (key === 'End') return length - 1;
7
+ return null;
8
+ }
@@ -0,0 +1,10 @@
1
+ export function formatNumericValue({ value, unit = '', missing = 'Not available' } = {}) {
2
+ const present = value != null && value !== '';
3
+ return { text: present ? String(value) : '—', unit: present ? unit : '', missing: present ? undefined : missing };
4
+ }
5
+
6
+ export function formatDeltaValue({ value, tone = 'neutral', basisId, missing = 'No earlier figure' } = {}) {
7
+ const present = value != null && value !== '';
8
+ const judged = present && ['success', 'danger'].includes(tone) && !/^[+−-]?0+(?:[.,]0+)?(?:[^\d.,].*)?$/.test(String(value).replace(/\s/g, ''));
9
+ return { text: String(present ? value : missing), className: `ui-delta${judged ? ` ui-delta--${tone}` : ''}`, basisId };
10
+ }
@@ -0,0 +1,6 @@
1
+ export declare function prefersReducedMotion(): boolean;
2
+ export declare function staggerDelay(index: number, stepMs?: number): number;
3
+ export declare function initReveal(root?: Document | Element): IntersectionObserver | undefined;
4
+ export declare const ENTRANCE_FALLBACK_MS: number;
5
+ export declare function playEntrance(element: Element | null, className?: string): void;
6
+ export declare function replay(element: HTMLElement | null): void;