@ceebee/ui 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { Modal as Modal$1, Button, theme, ConfigProvider, Progress } from 'antd';
3
3
  export * from 'antd';
4
+ import * as React from 'react';
4
5
  import { createContext, useId, useRef, useCallback, useInsertionEffect, useLayoutEffect, useState, useEffect, useMemo, useContext, Children } from 'react';
5
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
7
  import { StyleProvider, createCache, extractStyle } from '@ant-design/cssinjs';
@@ -13,6 +14,9 @@ import { X, Minus, Plus, RotateCcw, Minimize2, Maximize2, Search, Check, Chevron
13
14
  import { Toast } from '@base-ui/react/toast';
14
15
  import { Popover } from '@base-ui/react/popover';
15
16
  import { AnimatePresence, motion } from 'motion/react';
17
+ import { useSensors, useSensor, PointerSensor, DndContext, closestCorners, DragOverlay, useDroppable } from '@dnd-kit/core';
18
+ import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
19
+ import { CSS } from '@dnd-kit/utilities';
16
20
  import { ReactFlow, ConnectionMode, Background, Controls, applyNodeChanges, MarkerType, Handle, Position } from '@xyflow/react';
17
21
 
18
22
  // src/lib/cn.ts
@@ -118,6 +122,66 @@ function ModalRoot({
118
122
  }
119
123
  var Modal = Object.assign(ModalRoot, Modal$1);
120
124
 
125
+ // src/lib/css-probe.ts
126
+ function createCssProbe(root) {
127
+ const probe = document.createElement("span");
128
+ probe.style.position = "fixed";
129
+ probe.style.pointerEvents = "none";
130
+ probe.style.visibility = "hidden";
131
+ root.append(probe);
132
+ return {
133
+ color: (name) => resolveCssColor(probe, name),
134
+ length: (name) => resolveCssLength(probe, name),
135
+ done: () => probe.remove()
136
+ };
137
+ }
138
+ function resolveCssLength(probe, name) {
139
+ probe.style.width = `var(${name})`;
140
+ const value = Number.parseFloat(getComputedStyle(probe).width);
141
+ probe.style.removeProperty("width");
142
+ return Number.isFinite(value) ? value : void 0;
143
+ }
144
+ function resolveCssColor(probe, name) {
145
+ probe.style.color = `var(${name})`;
146
+ const value = getComputedStyle(probe).color;
147
+ probe.style.removeProperty("color");
148
+ if (!value) return void 0;
149
+ if (value.startsWith("var(")) return void 0;
150
+ if (/^(?:#|rgb|hsl|hsv)/i.test(value)) return value;
151
+ const canvas = document.createElement("canvas");
152
+ canvas.width = 1;
153
+ canvas.height = 1;
154
+ const context = canvas.getContext("2d", { willReadFrequently: true });
155
+ if (!context) return void 0;
156
+ context.clearRect(0, 0, 1, 1);
157
+ context.fillStyle = value;
158
+ context.fillRect(0, 0, 1, 1);
159
+ const [red = 0, green = 0, blue = 0, alpha = 255] = context.getImageData(0, 0, 1, 1).data;
160
+ return `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`;
161
+ }
162
+ var SKIN_LINK_ID = "cb-skin";
163
+ function watchTokens(onChange) {
164
+ const rootObserver = new MutationObserver(onChange);
165
+ rootObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
166
+ const headObserver = new MutationObserver((records) => {
167
+ const skinChanged = records.some((record) => [...record.addedNodes, ...record.removedNodes].some((node) => node instanceof HTMLElement && node.id === SKIN_LINK_ID));
168
+ if (skinChanged) onChange();
169
+ });
170
+ headObserver.observe(document.head, { childList: true });
171
+ const onSkinLoad = (event) => {
172
+ if (event.target instanceof HTMLElement && event.target.id === SKIN_LINK_ID) onChange();
173
+ };
174
+ document.addEventListener("load", onSkinLoad, true);
175
+ const dark = window.matchMedia("(prefers-color-scheme: dark)");
176
+ dark.addEventListener("change", onChange);
177
+ return () => {
178
+ rootObserver.disconnect();
179
+ headObserver.disconnect();
180
+ document.removeEventListener("load", onSkinLoad, true);
181
+ dark.removeEventListener("change", onChange);
182
+ };
183
+ }
184
+
121
185
  // src/theme/ant-theme-seeds.generated.ts
122
186
  var generatedCeebeeAntSeeds = {
123
187
  "ceebee": {
@@ -788,7 +852,6 @@ var THEME_MODE_COOKIE = "cb-theme-mode";
788
852
  function serializeThemeModeCookie(mode) {
789
853
  return `${THEME_MODE_COOKIE}=${mode}; Path=/; Max-Age=31536000; SameSite=Lax`;
790
854
  }
791
- var SKIN_LINK_ID = "cb-skin";
792
855
  function ThemeBridge({
793
856
  children,
794
857
  mode,
@@ -802,27 +865,12 @@ function ThemeBridge({
802
865
  }, []);
803
866
  useEffect(() => {
804
867
  refresh();
805
- const rootObserver = new MutationObserver(refresh);
806
- rootObserver.observe(document.documentElement, {
807
- attributes: true,
808
- attributeFilter: ["data-theme"]
809
- });
810
- const headObserver = new MutationObserver((records) => {
811
- const skinChanged = records.some((record) => [...record.addedNodes, ...record.removedNodes].some((node) => node instanceof HTMLElement && node.id === SKIN_LINK_ID));
812
- if (skinChanged) refresh();
813
- });
814
- headObserver.observe(document.head, { childList: true });
815
- const onSkinLoad = (event) => {
816
- if (event.target instanceof HTMLElement && event.target.id === SKIN_LINK_ID) refresh();
817
- };
818
- document.addEventListener("load", onSkinLoad, true);
868
+ const stopWatchingTokens = watchTokens(refresh);
819
869
  const coarsePointer = window.matchMedia("(pointer: coarse)");
820
870
  const onPointerChange = () => refresh();
821
871
  coarsePointer.addEventListener("change", onPointerChange);
822
872
  return () => {
823
- rootObserver.disconnect();
824
- headObserver.disconnect();
825
- document.removeEventListener("load", onSkinLoad, true);
873
+ stopWatchingTokens();
826
874
  coarsePointer.removeEventListener("change", onPointerChange);
827
875
  };
828
876
  }, [refresh]);
@@ -849,13 +897,9 @@ function mergeComponents(base, override) {
849
897
  return merged;
850
898
  }
851
899
  function readCeebeeThemeToken(root) {
852
- const probe = document.createElement("span");
853
- probe.style.position = "fixed";
854
- probe.style.pointerEvents = "none";
855
- probe.style.visibility = "hidden";
856
- root.append(probe);
857
- const color = (name) => resolveCssColor(probe, name);
858
- const length = (name) => resolveCssLength(probe, name);
900
+ const probe = createCssProbe(root);
901
+ const color = (name) => probe.color(name);
902
+ const length = (name) => probe.length(name);
859
903
  const tokens = {
860
904
  colorPrimary: color("--cb-tone-brand"),
861
905
  colorInfo: color("--cb-tone-info"),
@@ -915,7 +959,7 @@ function readCeebeeThemeToken(root) {
915
959
  dotActiveBorderColor: trackBg
916
960
  };
917
961
  }
918
- probe.remove();
962
+ probe.done();
919
963
  return {
920
964
  token: Object.fromEntries(
921
965
  Object.entries(tokens).filter(([, value]) => value !== void 0)
@@ -923,30 +967,6 @@ function readCeebeeThemeToken(root) {
923
967
  components
924
968
  };
925
969
  }
926
- function resolveCssLength(probe, name) {
927
- probe.style.width = `var(${name})`;
928
- const value = Number.parseFloat(getComputedStyle(probe).width);
929
- probe.style.removeProperty("width");
930
- return Number.isFinite(value) ? value : void 0;
931
- }
932
- function resolveCssColor(probe, name) {
933
- probe.style.color = `var(${name})`;
934
- const value = getComputedStyle(probe).color;
935
- probe.style.removeProperty("color");
936
- if (!value) return void 0;
937
- if (value.startsWith("var(")) return void 0;
938
- if (/^(?:#|rgb|hsl|hsv)/i.test(value)) return value;
939
- const canvas = document.createElement("canvas");
940
- canvas.width = 1;
941
- canvas.height = 1;
942
- const context = canvas.getContext("2d", { willReadFrequently: true });
943
- if (!context) return void 0;
944
- context.clearRect(0, 0, 1, 1);
945
- context.fillStyle = value;
946
- context.fillRect(0, 0, 1, 1);
947
- const [red = 0, green = 0, blue = 0, alpha = 255] = context.getImageData(0, 0, 1, 1).data;
948
- return `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`;
949
- }
950
970
  function CeebeeAntStyleProvider({ cache, children }) {
951
971
  return /* @__PURE__ */ jsx(StyleProvider, { cache, children });
952
972
  }
@@ -1673,6 +1693,813 @@ function PanZoomCanvasRoot({
1673
1693
  ] });
1674
1694
  }
1675
1695
  var PanZoomCanvas = Object.assign(PanZoomCanvasRoot, { Skeleton: PanZoomCanvasSkeleton });
1696
+ function BoardSkeleton({ columns = 3, cards = 3, label = "Loading board" }) {
1697
+ return /* @__PURE__ */ jsx("div", { className: "cb-board", "aria-busy": "true", "aria-label": label, children: /* @__PURE__ */ jsx("div", { className: "cb-board__surface", children: Array.from({ length: columns }, (_, column) => /* @__PURE__ */ jsxs("section", { className: "cb-board__column", children: [
1698
+ /* @__PURE__ */ jsxs("header", { className: "cb-board__head", children: [
1699
+ /* @__PURE__ */ jsx("span", { className: "cb-board__ghost cb-board__ghost cb-board__ghost--name" }),
1700
+ /* @__PURE__ */ jsx("span", { className: "cb-board__ghost cb-board__ghost cb-board__ghost--count" })
1701
+ ] }),
1702
+ /* @__PURE__ */ jsx("ol", { className: "cb-board__list", children: Array.from({ length: cards }, (_2, card) => /* @__PURE__ */ jsx("li", { className: "cb-board__card cb-board__ghost-card" }, card)) })
1703
+ ] }, column)) }) });
1704
+ }
1705
+
1706
+ // src/data/board/board.math.ts
1707
+ function locate(columns, cardId) {
1708
+ for (const column of columns) {
1709
+ const index = column.cards.findIndex((card) => card.id === cardId);
1710
+ if (index !== -1) return { columnId: column.id, index };
1711
+ }
1712
+ return null;
1713
+ }
1714
+ var columnOf = (columns, columnId) => columns.find((c) => c.id === columnId);
1715
+ function canPickUp(columns, cardId) {
1716
+ const at = locate(columns, cardId);
1717
+ if (!at) return false;
1718
+ return !columnOf(columns, at.columnId)?.cards.find((c) => c.id === cardId)?.disabled;
1719
+ }
1720
+ function refusalFor(columns, move) {
1721
+ if (!canPickUp(columns, move.cardId)) return "this card cannot be moved";
1722
+ const target = columnOf(columns, move.to.columnId);
1723
+ if (!target) return "that column is not on the board";
1724
+ if (target.accepts === false) return "that column does not take cards";
1725
+ return null;
1726
+ }
1727
+ function isNoop(move) {
1728
+ return move.from.columnId === move.to.columnId && move.from.index === move.to.index;
1729
+ }
1730
+ function applyMove(columns, move) {
1731
+ const card = columnOf(columns, move.from.columnId)?.cards[move.from.index];
1732
+ if (!card || card.id !== move.cardId) return [...columns];
1733
+ return columns.map((column) => {
1734
+ if (column.id === move.from.columnId && column.id === move.to.columnId) {
1735
+ const rest = column.cards.filter((_, i) => i !== move.from.index);
1736
+ rest.splice(clamp(move.to.index, rest.length), 0, card);
1737
+ return { ...column, cards: rest };
1738
+ }
1739
+ if (column.id === move.from.columnId) {
1740
+ return { ...column, cards: column.cards.filter((_, i) => i !== move.from.index) };
1741
+ }
1742
+ if (column.id === move.to.columnId) {
1743
+ const next = [...column.cards];
1744
+ next.splice(clamp(move.to.index, next.length), 0, card);
1745
+ return { ...column, cards: next };
1746
+ }
1747
+ return column;
1748
+ });
1749
+ }
1750
+ var clamp = (value, max) => Math.max(0, Math.min(value, max));
1751
+ function nextTarget(columns, held, direction) {
1752
+ const at = columns.findIndex((c) => c.id === held.columnId);
1753
+ const current = columns[at];
1754
+ if (!current) return null;
1755
+ if (direction === "up" || direction === "down") {
1756
+ const size = current.cards.length;
1757
+ const index = held.index + (direction === "down" ? 1 : -1);
1758
+ return index < 0 || index > size - 1 ? null : { columnId: held.columnId, index };
1759
+ }
1760
+ const step = direction === "right" ? 1 : -1;
1761
+ for (let i = at + step; i >= 0 && i < columns.length; i += step) {
1762
+ const column = columns[i];
1763
+ if (!column || column.accepts === false) continue;
1764
+ return { columnId: column.id, index: clamp(held.index, column.cards.length) };
1765
+ }
1766
+ return null;
1767
+ }
1768
+ function columnLoad(column) {
1769
+ const count = column.cards.length;
1770
+ return { count, over: typeof column.limit === "number" && count > column.limit };
1771
+ }
1772
+ var DEFAULTS = {
1773
+ pickUp: "Picked up. Use the arrow keys to move it, Space to drop, Escape to cancel.",
1774
+ dropped: (card, column) => `${card} moved to ${column}.`,
1775
+ cancelled: "Move cancelled.",
1776
+ refused: (reason) => `Move refused: ${reason}`,
1777
+ undo: "Undo",
1778
+ move: (card) => `Move ${card}`,
1779
+ undone: "Move undone.",
1780
+ lanes: "Column",
1781
+ over: (count, limit) => `${count} of ${limit}, over the limit`
1782
+ };
1783
+ var textOf = (node, fallback) => typeof node === "string" ? node : fallback;
1784
+ function BoardRoot({
1785
+ columns,
1786
+ onMove,
1787
+ layout = "auto",
1788
+ phoneQuery = "(max-width: 640px)",
1789
+ labels,
1790
+ motion: motion3 = true,
1791
+ handle = false,
1792
+ "aria-label": ariaLabel = "Board"
1793
+ }) {
1794
+ const text = { ...DEFAULTS, ...labels };
1795
+ const [optimistic, setOptimistic] = React.useState(null);
1796
+ const [held, setHeld] = React.useState(null);
1797
+ const [dragging, setDragging] = React.useState(null);
1798
+ const [announcement, setAnnouncement] = React.useState("");
1799
+ const [undoable, setUndoable] = React.useState(null);
1800
+ const [lane, setLane] = React.useState(0);
1801
+ const view = optimistic ?? columns;
1802
+ React.useEffect(() => setOptimistic(null), [columns]);
1803
+ const narrow = useNarrow(layout === "auto" ? phoneQuery : null);
1804
+ const lanes = layout === "lanes" || layout === "auto" && narrow;
1805
+ const nameOf = (columnId) => {
1806
+ const column = view.find((c) => c.id === columnId);
1807
+ return column?.label ?? textOf(column?.name, columnId);
1808
+ };
1809
+ const commit = React.useCallback(
1810
+ async (move, announceAs) => {
1811
+ if (isNoop(move)) return;
1812
+ const refusal = refusalFor(view, move);
1813
+ if (refusal) {
1814
+ setAnnouncement(text.refused(refusal));
1815
+ return;
1816
+ }
1817
+ const before = view;
1818
+ setOptimistic(applyMove(view, move));
1819
+ setAnnouncement(announceAs ?? text.dropped(titleOf(view, move.cardId), nameOf(move.to.columnId)));
1820
+ try {
1821
+ const result = await onMove(move);
1822
+ if (result && "refused" in result) {
1823
+ setOptimistic(before);
1824
+ setAnnouncement(text.refused(result.refused));
1825
+ return;
1826
+ }
1827
+ setUndoable({ cardId: move.cardId, from: move.to, to: move.from });
1828
+ } catch (e) {
1829
+ setOptimistic(before);
1830
+ setAnnouncement(text.refused(e instanceof Error ? e.message : String(e)));
1831
+ }
1832
+ },
1833
+ [view, onMove, text]
1834
+ );
1835
+ const onCardKeyDown = (event, cardId) => {
1836
+ const DIRECTIONS = {
1837
+ ArrowLeft: "left",
1838
+ ArrowRight: "right",
1839
+ ArrowUp: "up",
1840
+ ArrowDown: "down"
1841
+ };
1842
+ if (event.key === "Escape" && held) {
1843
+ event.preventDefault();
1844
+ setHeld(null);
1845
+ setAnnouncement(text.cancelled);
1846
+ return;
1847
+ }
1848
+ if (event.key === " " || event.key === "Enter") {
1849
+ event.preventDefault();
1850
+ if (!held) {
1851
+ if (!canPickUp(view, cardId)) {
1852
+ const card = view.flatMap((c) => c.cards).find((c) => c.id === cardId);
1853
+ setAnnouncement(text.refused(card?.disabledReason ?? "this card cannot be moved"));
1854
+ return;
1855
+ }
1856
+ const at = locate(view, cardId);
1857
+ if (at) {
1858
+ setHeld({ cardId, at });
1859
+ setAnnouncement(text.pickUp);
1860
+ }
1861
+ return;
1862
+ }
1863
+ const from = locate(view, held.cardId);
1864
+ if (from) void commit({ cardId: held.cardId, from, to: held.at });
1865
+ setHeld(null);
1866
+ return;
1867
+ }
1868
+ const direction = DIRECTIONS[event.key];
1869
+ if (!direction || !held) return;
1870
+ event.preventDefault();
1871
+ const target = nextTarget(view, held.at, direction);
1872
+ if (!target) return;
1873
+ setHeld({ ...held, at: target });
1874
+ setAnnouncement(`${nameOf(target.columnId)}, ${target.index + 1}`);
1875
+ };
1876
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
1877
+ const onDragEnd = (event) => {
1878
+ setDragging(null);
1879
+ const cardId = String(event.active.id);
1880
+ const over = event.over ? String(event.over.id) : null;
1881
+ if (!over) return;
1882
+ const from = locate(view, cardId);
1883
+ if (!from) return;
1884
+ const onColumn = view.find((c) => c.id === over);
1885
+ const to = onColumn ? { columnId: onColumn.id, index: onColumn.cards.length } : locate(view, over);
1886
+ if (to) void commit({ cardId, from, to });
1887
+ };
1888
+ const laneColumn = view[Math.min(lane, Math.max(view.length - 1, 0))];
1889
+ const shown = lanes ? laneColumn ? [laneColumn] : [] : view;
1890
+ return /* @__PURE__ */ jsxs("div", { className: "cb-board", "data-motion": motion3 ? void 0 : "off", children: [
1891
+ lanes ? /* @__PURE__ */ jsx("div", { className: "cb-board__lanes", role: "tablist", "aria-label": text.lanes, children: view.map((column, i) => /* @__PURE__ */ jsxs(
1892
+ "button",
1893
+ {
1894
+ type: "button",
1895
+ role: "tab",
1896
+ "aria-selected": i === lane,
1897
+ className: "cb-board__lane",
1898
+ onClick: () => setLane(i),
1899
+ children: [
1900
+ column.name,
1901
+ " ",
1902
+ /* @__PURE__ */ jsx("span", { className: "cb-board__count", children: column.cards.length })
1903
+ ]
1904
+ },
1905
+ column.id
1906
+ )) }) : null,
1907
+ /* @__PURE__ */ jsxs(
1908
+ DndContext,
1909
+ {
1910
+ sensors,
1911
+ collisionDetection: closestCorners,
1912
+ onDragStart: (e) => setDragging(String(e.active.id)),
1913
+ onDragCancel: () => setDragging(null),
1914
+ onDragEnd,
1915
+ children: [
1916
+ /* @__PURE__ */ jsx("div", { className: "cb-board__surface", role: "group", "aria-label": ariaLabel, "data-lanes": lanes ? "" : void 0, children: shown.map((column) => /* @__PURE__ */ jsx(Column, { column, held, onCardKeyDown, labels: text, handle }, column.id)) }),
1917
+ /* @__PURE__ */ jsx(DragOverlay, { children: dragging ? /* @__PURE__ */ jsx("div", { className: "cb-board__card cb-board__card--lift", children: cardTitle(view, dragging) }) : null })
1918
+ ]
1919
+ }
1920
+ ),
1921
+ undoable ? /* @__PURE__ */ jsx("div", { className: "cb-board__undo", children: /* @__PURE__ */ jsx(
1922
+ "button",
1923
+ {
1924
+ type: "button",
1925
+ onClick: () => {
1926
+ const move = undoable;
1927
+ setUndoable(null);
1928
+ void commit(move, text.undone);
1929
+ },
1930
+ children: text.undo
1931
+ }
1932
+ ) }) : null,
1933
+ /* @__PURE__ */ jsx("div", { className: "cb-board__live", role: "status", "aria-live": "polite", children: announcement })
1934
+ ] });
1935
+ }
1936
+ var cardTitle = (columns, cardId) => columns.flatMap((c) => c.cards).find((c) => c.id === cardId)?.title ?? null;
1937
+ var titleOf = (columns, cardId) => {
1938
+ const card = columns.flatMap((c) => c.cards).find((c) => c.id === cardId);
1939
+ return card?.label ?? textOf(card?.title, cardId);
1940
+ };
1941
+ function Column({
1942
+ column,
1943
+ held,
1944
+ onCardKeyDown,
1945
+ labels,
1946
+ handle
1947
+ }) {
1948
+ const { setNodeRef, isOver } = useDroppable({ id: column.id });
1949
+ const load = columnLoad(column);
1950
+ const holding = held?.at.columnId === column.id;
1951
+ return /* @__PURE__ */ jsxs(
1952
+ "section",
1953
+ {
1954
+ ref: setNodeRef,
1955
+ className: "cb-board__column",
1956
+ "data-over": isOver ? "" : void 0,
1957
+ "data-refuses": column.accepts === false ? "" : void 0,
1958
+ "aria-label": column.label ?? (typeof column.name === "string" ? column.name : void 0),
1959
+ children: [
1960
+ /* @__PURE__ */ jsxs("header", { className: "cb-board__head", children: [
1961
+ /* @__PURE__ */ jsx("span", { className: "cb-board__name", children: column.name }),
1962
+ /* @__PURE__ */ jsx("span", { className: "cb-board__count", "data-over-limit": load.over ? "" : void 0, children: typeof column.limit === "number" ? `${load.count}/${column.limit}` : load.count })
1963
+ ] }),
1964
+ /* @__PURE__ */ jsx(SortableContext, { items: column.cards.map((c) => c.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsxs("ol", { className: "cb-board__list", children: [
1965
+ column.cards.map((card, index) => /* @__PURE__ */ jsx(
1966
+ Card,
1967
+ {
1968
+ card,
1969
+ held: held?.cardId === card.id,
1970
+ marker: holding && held?.at.index === index,
1971
+ onKeyDown: onCardKeyDown,
1972
+ handle,
1973
+ labels
1974
+ },
1975
+ card.id
1976
+ )),
1977
+ holding && held.at.index >= column.cards.length ? /* @__PURE__ */ jsx("li", { className: "cb-board__marker", "aria-hidden": true }) : null
1978
+ ] }) }),
1979
+ column.cards.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cb-board__empty", children: column.empty ?? null }) : null,
1980
+ column.accepts === false && column.refusal ? /* @__PURE__ */ jsx("p", { className: "cb-board__refusal", children: column.refusal }) : null,
1981
+ load.over && typeof column.limit === "number" ? /* @__PURE__ */ jsx("p", { className: "cb-board__over", children: labels.over(load.count, column.limit) }) : null
1982
+ ]
1983
+ }
1984
+ );
1985
+ }
1986
+ function Card({
1987
+ card,
1988
+ held,
1989
+ marker,
1990
+ onKeyDown,
1991
+ handle,
1992
+ labels
1993
+ }) {
1994
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
1995
+ id: card.id,
1996
+ disabled: card.disabled
1997
+ });
1998
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1999
+ marker ? /* @__PURE__ */ jsx("li", { className: "cb-board__marker", "aria-hidden": true }) : null,
2000
+ /* @__PURE__ */ jsxs(
2001
+ "li",
2002
+ {
2003
+ ref: setNodeRef,
2004
+ style: { transform: CSS.Transform.toString(transform), transition },
2005
+ className: "cb-board__card",
2006
+ "data-dragging": isDragging ? "" : void 0,
2007
+ "data-held": held ? "" : void 0,
2008
+ "data-disabled": card.disabled ? "" : void 0,
2009
+ "data-handle": handle ? "" : void 0,
2010
+ ...handle ? {} : {
2011
+ ...attributes,
2012
+ ...card.disabled ? {} : listeners,
2013
+ tabIndex: 0,
2014
+ "aria-roledescription": "draggable card",
2015
+ "aria-disabled": card.disabled || void 0,
2016
+ onKeyDown: (event) => onKeyDown(event, card.id)
2017
+ },
2018
+ children: [
2019
+ handle ? /* @__PURE__ */ jsx(
2020
+ "button",
2021
+ {
2022
+ type: "button",
2023
+ className: "cb-board__handle",
2024
+ ...attributes,
2025
+ ...card.disabled ? {} : listeners,
2026
+ "aria-label": labels.move(card.label ?? (typeof card.title === "string" ? card.title : card.id)),
2027
+ "aria-roledescription": "drag handle",
2028
+ "aria-disabled": card.disabled || void 0,
2029
+ disabled: card.disabled,
2030
+ onKeyDown: (event) => onKeyDown(event, card.id),
2031
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u283F" })
2032
+ }
2033
+ ) : null,
2034
+ /* @__PURE__ */ jsx("div", { className: "cb-board__title", children: card.title }),
2035
+ card.meta ? /* @__PURE__ */ jsx("div", { className: "cb-board__meta", children: card.meta }) : null
2036
+ ]
2037
+ }
2038
+ )
2039
+ ] });
2040
+ }
2041
+ function useNarrow(query) {
2042
+ const [narrow, setNarrow] = React.useState(false);
2043
+ React.useEffect(() => {
2044
+ if (!query || typeof window === "undefined" || !window.matchMedia) return;
2045
+ const mql = window.matchMedia(query);
2046
+ const read = () => setNarrow(mql.matches);
2047
+ read();
2048
+ mql.addEventListener("change", read);
2049
+ return () => mql.removeEventListener("change", read);
2050
+ }, [query]);
2051
+ return narrow;
2052
+ }
2053
+ var Board = Object.assign(BoardRoot, { Skeleton: BoardSkeleton });
2054
+
2055
+ // src/data/time-series/time-series.chart.ts
2056
+ async function mountTimeSeries(host, shape, palette) {
2057
+ const { BaselineSeries, ColorType, LineSeries, LineStyle, createChart, createSeriesMarkers } = await import('lightweight-charts');
2058
+ const chart = createChart(host, {
2059
+ autoSize: true,
2060
+ /* A dashboard chart is read, not traded. Panning and zooming would let somebody scroll the data
2061
+ off the screen and leave them looking at a fragment with no way back, so the whole span is
2062
+ fitted and the viewport is fixed. */
2063
+ handleScroll: false,
2064
+ handleScale: false
2065
+ });
2066
+ const scale = shape.range ? () => ({ priceRange: { minValue: shape.range?.min ?? 0, maxValue: shape.range?.max ?? 0 } }) : void 0;
2067
+ const common = { priceLineVisible: false, lastValueVisible: false, autoscaleInfoProvider: scale };
2068
+ const drawn = shape.baseline ? [
2069
+ chart.addSeries(BaselineSeries, {
2070
+ ...common,
2071
+ baseValue: { type: "price", price: shape.baseline.value },
2072
+ lineWidth: 2
2073
+ })
2074
+ ] : shape.series.map(
2075
+ (series) => chart.addSeries(LineSeries, {
2076
+ ...common,
2077
+ lineWidth: series.emphasis === "reference" ? 2 : 3,
2078
+ lineStyle: series.emphasis === "reference" ? LineStyle.Dashed : LineStyle.Solid
2079
+ })
2080
+ );
2081
+ const markedSeries = drawn.at(-1);
2082
+ const markers = markedSeries ? createSeriesMarkers(markedSeries, []) : null;
2083
+ let current = palette;
2084
+ const apply = (next) => {
2085
+ current = next;
2086
+ chart.applyOptions({
2087
+ layout: {
2088
+ background: { type: ColorType.Solid, color: next.background },
2089
+ textColor: next.text,
2090
+ fontFamily: next.font,
2091
+ /* Apache-2.0 asks that the NOTICE travel with the distribution, not that a logo be painted
2092
+ into a consumer's dashboard. The attribution lives in THIRD_PARTY_NOTICES.md. */
2093
+ attributionLogo: false
2094
+ },
2095
+ grid: { vertLines: { color: next.grid }, horzLines: { color: next.grid } },
2096
+ rightPriceScale: { borderColor: next.grid, scaleMargins: { top: 0.1, bottom: 0.08 } },
2097
+ timeScale: { borderColor: next.grid, fixLeftEdge: true, fixRightEdge: true },
2098
+ crosshair: { vertLine: { color: next.muted }, horzLine: { color: next.muted } },
2099
+ localization: { priceFormatter: shape.format }
2100
+ });
2101
+ drawn.forEach((series, index) => {
2102
+ if (shape.baseline) {
2103
+ series.applyOptions({
2104
+ topLineColor: next.above,
2105
+ topFillColor1: next.above,
2106
+ topFillColor2: next.background,
2107
+ bottomLineColor: next.below,
2108
+ bottomFillColor1: next.background,
2109
+ bottomFillColor2: next.below,
2110
+ crosshairMarkerBorderColor: next.background
2111
+ });
2112
+ return;
2113
+ }
2114
+ series.applyOptions({
2115
+ color: next.series[index] ?? next.text,
2116
+ crosshairMarkerBorderColor: next.background
2117
+ });
2118
+ });
2119
+ };
2120
+ apply(palette);
2121
+ return {
2122
+ setData(series) {
2123
+ drawn.forEach((drawnSeries, index) => {
2124
+ const points = series[index]?.points ?? [];
2125
+ drawnSeries.setData(points.map((point) => ({ time: point.day, value: point.value })));
2126
+ });
2127
+ chart.timeScale().fitContent();
2128
+ },
2129
+ mark(day, text) {
2130
+ markers?.setMarkers(day === null ? [] : [{
2131
+ time: day,
2132
+ position: "belowBar",
2133
+ shape: "arrowUp",
2134
+ color: current.series[current.series.length - 1] ?? current.text,
2135
+ text
2136
+ }]);
2137
+ },
2138
+ applyPalette: apply,
2139
+ destroy() {
2140
+ chart.remove();
2141
+ }
2142
+ };
2143
+ }
2144
+
2145
+ // src/data/time-series/time-series.math.ts
2146
+ function asDay(value) {
2147
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
2148
+ const at = (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).getTime();
2149
+ if (Number.isNaN(at)) return null;
2150
+ return new Date(at).toISOString().startsWith(value) ? value : null;
2151
+ }
2152
+ function seriesPoints(points) {
2153
+ const byDay = /* @__PURE__ */ new Map();
2154
+ for (const point of points) {
2155
+ if (asDay(point.day) === null) continue;
2156
+ byDay.set(point.day, point.value);
2157
+ }
2158
+ return [...byDay].map(([day, value]) => ({ day, value })).sort((a, b) => a.day.localeCompare(b.day));
2159
+ }
2160
+ function valueOn(points, day) {
2161
+ const reached = seriesPoints(points).filter((point) => point.day <= day);
2162
+ const last = reached[reached.length - 1];
2163
+ return last ? last.value : null;
2164
+ }
2165
+ function alignRows(series) {
2166
+ const days = [...new Set(series.flatMap((one) => seriesPoints(one.points).map((point) => point.day)))].sort();
2167
+ return days.map((day) => {
2168
+ const values = {};
2169
+ for (const one of series) values[one.key] = valueOn(one.points, day);
2170
+ return { day, values };
2171
+ });
2172
+ }
2173
+ function seriesSpan(series) {
2174
+ const days = series.flatMap((one) => seriesPoints(one.points).map((point) => point.day)).sort();
2175
+ const from = days[0];
2176
+ const to = days[days.length - 1];
2177
+ return from && to ? { from, to } : null;
2178
+ }
2179
+ function nearestDay(days, day) {
2180
+ const known = days.filter((candidate) => asDay(candidate) !== null);
2181
+ if (known.length === 0 || asDay(day) === null) return null;
2182
+ const distance2 = (candidate) => Math.abs((/* @__PURE__ */ new Date(`${candidate}T00:00:00Z`)).getTime() - (/* @__PURE__ */ new Date(`${day}T00:00:00Z`)).getTime());
2183
+ return known.reduce((closest, candidate) => distance2(candidate) < distance2(closest) ? candidate : closest);
2184
+ }
2185
+ function round1(value) {
2186
+ return Math.round(value * 10) / 10;
2187
+ }
2188
+ function TimeSeriesChartSkeleton({
2189
+ height = 260,
2190
+ label = "Loading chart",
2191
+ className
2192
+ }) {
2193
+ return /* @__PURE__ */ jsxs("div", { className: cn("cb-chart", "cb-chart--skeleton", className), role: "status", "aria-label": label, children: [
2194
+ /* @__PURE__ */ jsx("span", { className: "cb-chart__skeleton-head", "aria-hidden": "true" }),
2195
+ /* @__PURE__ */ jsx("span", { className: "cb-chart__skeleton-plot", style: { height }, "aria-hidden": "true" })
2196
+ ] });
2197
+ }
2198
+ function TimeSeriesChart({
2199
+ label,
2200
+ series,
2201
+ format,
2202
+ range,
2203
+ baseline,
2204
+ mark,
2205
+ height = 260,
2206
+ emptyLabel = "Nothing has been reported yet.",
2207
+ tableLabel = "Readings by day",
2208
+ dayLabel = "Day",
2209
+ loading = false,
2210
+ className
2211
+ }) {
2212
+ const host = useRef(null);
2213
+ const [forcedColors, setForcedColors] = useState(false);
2214
+ const cleaned = useMemo(
2215
+ () => series.map((one) => ({ ...one, points: seriesPoints(one.points) })),
2216
+ [series]
2217
+ );
2218
+ const rows = useMemo(() => alignRows(cleaned), [cleaned]);
2219
+ const empty = rows.length === 0;
2220
+ const markDay = useMemo(
2221
+ () => mark ? nearestDay(rows.map((row) => row.day), mark.day) : null,
2222
+ [rows, mark]
2223
+ );
2224
+ useEffect(() => {
2225
+ const query = window.matchMedia("(forced-colors: active)");
2226
+ const read = () => setForcedColors(query.matches);
2227
+ read();
2228
+ query.addEventListener("change", read);
2229
+ return () => query.removeEventListener("change", read);
2230
+ }, []);
2231
+ const tokens = useMemo(() => cleaned.map((one) => one.colorToken), [cleaned]);
2232
+ useEffect(() => {
2233
+ const element = host.current;
2234
+ if (!element || empty || forcedColors || loading) return;
2235
+ let chart = null;
2236
+ let cancelled = false;
2237
+ let palette = readPalette(element, tokens);
2238
+ if (!palette) return;
2239
+ void mountTimeSeries(element, { series: cleaned, format, range, baseline }, palette).then((mounted) => {
2240
+ if (cancelled) {
2241
+ mounted.destroy();
2242
+ return;
2243
+ }
2244
+ chart = mounted;
2245
+ const current = readPalette(element, tokens) ?? palette;
2246
+ if (current) mounted.applyPalette(current);
2247
+ mounted.setData(cleaned);
2248
+ mounted.mark(markDay, mark?.label ?? "");
2249
+ });
2250
+ const stopWatchingTokens = watchTokens(() => {
2251
+ const next = readPalette(element, tokens);
2252
+ if (!next) return;
2253
+ palette = next;
2254
+ chart?.applyPalette(next);
2255
+ });
2256
+ return () => {
2257
+ cancelled = true;
2258
+ stopWatchingTokens();
2259
+ chart?.destroy();
2260
+ chart = null;
2261
+ };
2262
+ }, [cleaned, tokens, format, range, baseline, markDay, mark, empty, forcedColors, loading]);
2263
+ if (loading) return /* @__PURE__ */ jsx(TimeSeriesChartSkeleton, { height, className });
2264
+ return /* @__PURE__ */ jsxs("div", { className: cn("cb-chart", className), "data-forced-colors": forcedColors || void 0, children: [
2265
+ empty ? /* @__PURE__ */ jsx("p", { className: "cb-chart__empty", style: { minHeight: height }, children: emptyLabel }) : null,
2266
+ empty || forcedColors ? null : /* @__PURE__ */ jsx("div", { ref: host, className: "cb-chart__canvas", style: { height }, role: "img", "aria-label": label }),
2267
+ empty ? null : /* @__PURE__ */ jsx("div", { className: "cb-chart__rows", children: /* @__PURE__ */ jsxs("table", { className: "cb-chart__table", children: [
2268
+ /* @__PURE__ */ jsx("caption", { children: tableLabel }),
2269
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2270
+ /* @__PURE__ */ jsx("th", { scope: "col", children: dayLabel }),
2271
+ cleaned.map((one) => /* @__PURE__ */ jsx("th", { scope: "col", children: one.label }, one.key))
2272
+ ] }) }),
2273
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs("tr", { "data-marked": row.day === markDay || void 0, children: [
2274
+ /* @__PURE__ */ jsx("th", { scope: "row", children: row.day }),
2275
+ cleaned.map((one) => {
2276
+ const value = row.values[one.key];
2277
+ return /* @__PURE__ */ jsx("td", { children: value === null || value === void 0 ? "\u2014" : format(value) }, one.key);
2278
+ })
2279
+ ] }, row.day)) })
2280
+ ] }) })
2281
+ ] });
2282
+ }
2283
+ function readPalette(host, seriesTokens) {
2284
+ const probe = createCssProbe(host);
2285
+ const text = probe.color("--cb-fg-muted");
2286
+ const muted = probe.color("--cb-fg-subtle");
2287
+ const grid = probe.color("--cb-border");
2288
+ const background = probe.color("--cb-surface");
2289
+ const above = probe.color("--cb-tone-success");
2290
+ const below = probe.color("--cb-tone-danger");
2291
+ const seriesColors = seriesTokens.map((token) => probe.color(token));
2292
+ probe.done();
2293
+ const font = getComputedStyle(host).fontFamily;
2294
+ if (!text || !muted || !grid || !background || !above || !below || !font) return null;
2295
+ if (seriesColors.some((colour) => colour === void 0)) return null;
2296
+ return {
2297
+ text,
2298
+ muted,
2299
+ grid,
2300
+ background,
2301
+ font,
2302
+ above,
2303
+ below,
2304
+ series: seriesColors.filter((colour) => colour !== void 0)
2305
+ };
2306
+ }
2307
+
2308
+ // src/data/balance-curve/balance-curve.math.ts
2309
+ function balanceReading(points, threshold) {
2310
+ const ordered = seriesPoints(points);
2311
+ if (ordered.length === 0) {
2312
+ return { lowest: null, lowestDay: null, firstBelowDay: null, daysBelow: 0, closing: null };
2313
+ }
2314
+ let lowestPoint = ordered[0];
2315
+ for (const point of ordered) {
2316
+ if (lowestPoint === void 0 || point.value < lowestPoint.value) lowestPoint = point;
2317
+ }
2318
+ const below = ordered.filter((point) => point.value < threshold);
2319
+ return {
2320
+ lowest: lowestPoint?.value ?? null,
2321
+ lowestDay: lowestPoint?.day ?? null,
2322
+ firstBelowDay: below[0]?.day ?? null,
2323
+ daysBelow: below.length,
2324
+ closing: ordered.at(-1)?.value ?? null
2325
+ };
2326
+ }
2327
+ function BalanceCurveSkeleton({
2328
+ height = 260,
2329
+ label = "Loading balance",
2330
+ className
2331
+ }) {
2332
+ return /* @__PURE__ */ jsx(TimeSeriesChartSkeleton, { height, label, className });
2333
+ }
2334
+ function BalanceCurveRoot({
2335
+ label,
2336
+ balances,
2337
+ format,
2338
+ threshold = { value: 0 },
2339
+ height = 260,
2340
+ seriesLabel = "Balance",
2341
+ emptyLabel = "Nothing to project yet.",
2342
+ tableLabel = "Balance by day",
2343
+ belowLabel = (from, lowest, days) => `Below the line from ${from} \u2014 lowest ${lowest}, ${days} day(s) under.`,
2344
+ clearLabel = (lowest) => `Stays above the line. Lowest point ${lowest}.`,
2345
+ loading = false,
2346
+ className
2347
+ }) {
2348
+ const points = useMemo(() => seriesPoints(balances), [balances]);
2349
+ const reading = useMemo(() => balanceReading(points, threshold.value), [points, threshold.value]);
2350
+ const series = useMemo(
2351
+ () => [{ key: "balance", label: seriesLabel, points, emphasis: "primary", colorToken: "--cb-tone-brand" }],
2352
+ [points, seriesLabel]
2353
+ );
2354
+ if (loading) return /* @__PURE__ */ jsx(BalanceCurveSkeleton, { height, className });
2355
+ const breached = reading.firstBelowDay !== null;
2356
+ return /* @__PURE__ */ jsxs("figure", { className: cn("cb-balance-curve", className), children: [
2357
+ /* @__PURE__ */ jsxs("figcaption", { className: "cb-balance-curve__head", children: [
2358
+ /* @__PURE__ */ jsx("span", { className: "cb-balance-curve__label", children: label }),
2359
+ threshold.label ? /* @__PURE__ */ jsx("span", { className: "cb-balance-curve__threshold", children: threshold.label }) : null
2360
+ ] }),
2361
+ reading.lowest === null ? null : /* @__PURE__ */ jsx("p", { className: "cb-balance-curve__reading", "data-state": breached ? "below" : "clear", children: breached && reading.firstBelowDay ? belowLabel(reading.firstBelowDay, format(reading.lowest), reading.daysBelow) : clearLabel(format(reading.lowest)) }),
2362
+ /* @__PURE__ */ jsx(
2363
+ TimeSeriesChart,
2364
+ {
2365
+ label,
2366
+ series,
2367
+ format,
2368
+ baseline: threshold,
2369
+ mark: reading.lowestDay ? { day: reading.lowestDay, label: format(reading.lowest ?? 0) } : void 0,
2370
+ height,
2371
+ emptyLabel,
2372
+ tableLabel
2373
+ }
2374
+ )
2375
+ ] });
2376
+ }
2377
+ var BalanceCurve = Object.assign(BalanceCurveRoot, { Skeleton: BalanceCurveSkeleton });
2378
+
2379
+ // src/data/progress-curve/progress-curve.math.ts
2380
+ function toPoints(points) {
2381
+ return seriesPoints(points.map((point) => ({ day: point.day, value: point.percent })));
2382
+ }
2383
+ function readingOn(planned, actual, day) {
2384
+ const plannedPercent = valueOn(toPoints(planned), day);
2385
+ const actualPercent = valueOn(toPoints(actual), day);
2386
+ return {
2387
+ plannedPercent,
2388
+ actualPercent,
2389
+ gap: plannedPercent === null || actualPercent === null ? null : round1(actualPercent - plannedPercent)
2390
+ };
2391
+ }
2392
+ function curveRows(planned, actual) {
2393
+ return alignRows([
2394
+ { key: "planned", points: toPoints(planned) },
2395
+ { key: "actual", points: toPoints(actual) }
2396
+ ]).map((row) => {
2397
+ const plannedPercent = row.values.planned ?? null;
2398
+ const actualPercent = row.values.actual ?? null;
2399
+ return {
2400
+ day: row.day,
2401
+ plannedPercent,
2402
+ actualPercent,
2403
+ gap: plannedPercent === null || actualPercent === null ? null : round1(actualPercent - plannedPercent)
2404
+ };
2405
+ });
2406
+ }
2407
+ function ProgressCurveSkeleton({
2408
+ height = 260,
2409
+ label = "Loading progress",
2410
+ className
2411
+ }) {
2412
+ return /* @__PURE__ */ jsx(TimeSeriesChartSkeleton, { height, label, className });
2413
+ }
2414
+ function ProgressCurveRoot({
2415
+ planned,
2416
+ actual,
2417
+ label,
2418
+ today,
2419
+ height = 260,
2420
+ plannedLabel = "Planned",
2421
+ actualLabel = "Actual",
2422
+ lastReportLabel = "Last report",
2423
+ emptyLabel = "Nothing has been reported yet.",
2424
+ tableLabel = "Progress by day",
2425
+ aheadLabel = "ahead of plan",
2426
+ behindLabel = "behind plan",
2427
+ onTrackLabel = "on plan",
2428
+ loading = false,
2429
+ className
2430
+ }) {
2431
+ const rows = useMemo(() => curveRows(planned, actual), [planned, actual]);
2432
+ const actualPoints = useMemo(() => toPoints(actual), [actual]);
2433
+ const lastReport = actualPoints.at(-1)?.day ?? null;
2434
+ const latest = useMemo(() => {
2435
+ if (!today) return rows[rows.length - 1];
2436
+ const day = asDay(today);
2437
+ return day === null ? rows[rows.length - 1] : { day, ...readingOn(planned, actual, day) };
2438
+ }, [today, rows, planned, actual]);
2439
+ const series = useMemo(
2440
+ () => [
2441
+ { key: "planned", label: plannedLabel, points: toPoints(planned), emphasis: "reference", colorToken: "--cb-fg-subtle" },
2442
+ { key: "actual", label: actualLabel, points: toPoints(actual), emphasis: "primary", colorToken: "--cb-tone-brand" }
2443
+ ],
2444
+ [planned, actual, plannedLabel, actualLabel]
2445
+ );
2446
+ if (loading) return /* @__PURE__ */ jsx(ProgressCurveSkeleton, { height, className });
2447
+ return /* @__PURE__ */ jsxs("figure", { className: cn("cb-progress-curve", className), children: [
2448
+ /* @__PURE__ */ jsxs("figcaption", { className: "cb-progress-curve__head", children: [
2449
+ /* @__PURE__ */ jsx("span", { className: "cb-progress-curve__label", children: label }),
2450
+ /* @__PURE__ */ jsx("span", { className: "cb-chart-legend", children: series.map((one) => /* @__PURE__ */ jsx("span", { className: "cb-chart-legend__key", "data-emphasis": one.emphasis, "data-series": one.key, children: one.label }, one.key)) })
2451
+ ] }),
2452
+ latest ? /* @__PURE__ */ jsxs("p", { className: "cb-progress-curve__reading", "data-state": stateOf(latest.gap), children: [
2453
+ /* @__PURE__ */ jsx("strong", { children: percent(latest.actualPercent) }),
2454
+ " ",
2455
+ actualLabel.toLowerCase(),
2456
+ " \xB7 ",
2457
+ percent(latest.plannedPercent),
2458
+ " ",
2459
+ plannedLabel.toLowerCase(),
2460
+ latest.gap === null ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
2461
+ " \u2014 ",
2462
+ Math.abs(latest.gap),
2463
+ " ",
2464
+ gapWord(latest.gap, { aheadLabel, behindLabel, onTrackLabel })
2465
+ ] }),
2466
+ /* @__PURE__ */ jsxs("span", { className: "cb-progress-curve__on-day", children: [
2467
+ " (",
2468
+ latest.day,
2469
+ ")"
2470
+ ] })
2471
+ ] }) : null,
2472
+ /* @__PURE__ */ jsx(
2473
+ TimeSeriesChart,
2474
+ {
2475
+ label,
2476
+ series,
2477
+ format: percentOf,
2478
+ range: { min: 0, max: 100 },
2479
+ mark: lastReport ? { day: lastReport, label: lastReportLabel } : void 0,
2480
+ height,
2481
+ emptyLabel,
2482
+ tableLabel
2483
+ }
2484
+ )
2485
+ ] });
2486
+ }
2487
+ var ProgressCurve = Object.assign(ProgressCurveRoot, { Skeleton: ProgressCurveSkeleton });
2488
+ var percentOf = (value) => `${Math.round(value)}%`;
2489
+ function percent(value) {
2490
+ return value === null ? "\u2014" : `${round1(value)}%`;
2491
+ }
2492
+ function stateOf(gap) {
2493
+ if (gap === null) return "unknown";
2494
+ if (gap < 0) return "behind";
2495
+ if (gap > 0) return "ahead";
2496
+ return "on-plan";
2497
+ }
2498
+ function gapWord(gap, words) {
2499
+ if (gap < 0) return words.behindLabel;
2500
+ if (gap > 0) return words.aheadLabel;
2501
+ return words.onTrackLabel;
2502
+ }
1676
2503
  function DiagramNodeView({ data, selected }) {
1677
2504
  return /* @__PURE__ */ jsxs(
1678
2505
  "div",
@@ -1980,4 +2807,4 @@ function DiagramEditorRoot(props) {
1980
2807
  }
1981
2808
  var DiagramEditor = Object.assign(DiagramEditorRoot, { Skeleton: DiagramSkeleton });
1982
2809
 
1983
- export { CeebeeAntStyleProvider, Checklist, CommandPalette, DEFAULT_LABELS, Diagram, DiagramEditor, LabelsProvider, Modal, MotionProvider, PanZoomCanvas, Reveal, Sidebar, Stagger, StickerGroup, ThemeBridge, ThemeProvider, ToastProvider, TopBar, createCeebeeAntStyleCache, extractCeebeeAntStyles, filterCommands, groupCommands, rankCommand, useLabels, useMotionSettings, useTheme, useToast };
2810
+ export { BalanceCurve, BalanceCurveSkeleton, Board, CeebeeAntStyleProvider, Checklist, CommandPalette, DEFAULT_LABELS, Diagram, DiagramEditor, LabelsProvider, Modal, MotionProvider, PanZoomCanvas, ProgressCurve, ProgressCurveSkeleton, Reveal, Sidebar, Stagger, StickerGroup, ThemeBridge, ThemeProvider, TimeSeriesChart, TimeSeriesChartSkeleton, ToastProvider, TopBar, alignRows, asDay, balanceReading, createCeebeeAntStyleCache, curveRows, extractCeebeeAntStyles, filterCommands, groupCommands, nearestDay, rankCommand, readingOn, round1, seriesPoints, seriesSpan, toPoints, useLabels, useMotionSettings, useTheme, useToast, valueOn };