@apliteni/apliteni-ui 0.113.0 → 0.115.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.
@@ -273,6 +273,43 @@ import { useId as useId2 } from "react";
273
273
 
274
274
  // src/Tooltip.tsx
275
275
  import { useEffect, useId, useLayoutEffect as useLayoutEffect2, useRef as useRef2, useState } from "react";
276
+
277
+ // src/tip.ts
278
+ function placeTip(host, mark, tip, bound) {
279
+ const mrect = mark.getBoundingClientRect();
280
+ const rect = host.getBoundingClientRect();
281
+ const view = host.ownerDocument.documentElement;
282
+ const clip = { top: 0, left: 0, right: view.clientWidth, bottom: view.clientHeight };
283
+ for (let el = host; el && el !== host.ownerDocument.body; el = el.parentElement) {
284
+ const style = getComputedStyle(el);
285
+ if (![style.overflow, style.overflowX, style.overflowY].some((v) => v && v !== "visible")) continue;
286
+ const bounds = el.getBoundingClientRect();
287
+ clip.top = Math.max(clip.top, bounds.top);
288
+ clip.left = Math.max(clip.left, bounds.left);
289
+ clip.right = Math.min(clip.right, bounds.right);
290
+ clip.bottom = Math.min(clip.bottom, bounds.bottom);
291
+ }
292
+ if (bound) {
293
+ const edge = bound.getBoundingClientRect();
294
+ clip.top = Math.max(clip.top, edge.top);
295
+ clip.left = Math.max(clip.left, edge.left);
296
+ clip.right = Math.min(clip.right, edge.right);
297
+ clip.bottom = Math.min(clip.bottom, edge.bottom);
298
+ }
299
+ const gap = parseFloat(getComputedStyle(tip).getPropertyValue("--ui-tip-gap")) || 8;
300
+ const above = mrect.top - clip.top;
301
+ const below = clip.bottom - mrect.bottom;
302
+ const isBelow = above < tip.offsetHeight + gap && below > above;
303
+ tip.classList.toggle("is-below", isBelow);
304
+ const centre = mrect.left + mrect.width / 2;
305
+ const ideal = centre - tip.offsetWidth / 2;
306
+ const left = Math.max(clip.left, Math.min(ideal, clip.right - tip.offsetWidth));
307
+ tip.style.setProperty("--ui-tip-x", `${centre - rect.left - host.clientLeft + host.scrollLeft}px`);
308
+ tip.style.setProperty("--ui-tip-y", `${(isBelow ? mrect.bottom : mrect.top) - rect.top - host.clientTop + host.scrollTop}px`);
309
+ tip.style.setProperty("--ui-tip-shift", `${left - ideal}px`);
310
+ }
311
+
312
+ // src/Tooltip.tsx
276
313
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
277
314
  function Tooltip({ text: text2, children }) {
278
315
  const id = useId();
@@ -282,34 +319,7 @@ function Tooltip({ text: text2, children }) {
282
319
  const ignoreMouseUntil = useRef2(0);
283
320
  const touchMoved = useRef2(false);
284
321
  const [open, setOpen] = useState(false);
285
- function place() {
286
- const h = host.current;
287
- const t = tip.current;
288
- const mark = trigger.current.getBoundingClientRect();
289
- const rect = h.getBoundingClientRect();
290
- const view = h.ownerDocument.documentElement;
291
- const clip = { top: 0, left: 0, right: view.clientWidth, bottom: view.clientHeight };
292
- for (let el = h; el && el !== h.ownerDocument.body; el = el.parentElement) {
293
- const style = getComputedStyle(el);
294
- if (![style.overflow, style.overflowX, style.overflowY].some((v) => v && v !== "visible")) continue;
295
- const bounds = el.getBoundingClientRect();
296
- clip.top = Math.max(clip.top, bounds.top);
297
- clip.left = Math.max(clip.left, bounds.left);
298
- clip.right = Math.min(clip.right, bounds.right);
299
- clip.bottom = Math.min(clip.bottom, bounds.bottom);
300
- }
301
- const gap = parseFloat(getComputedStyle(t).getPropertyValue("--ui-tip-gap")) || 8;
302
- const above = mark.top - clip.top;
303
- const below = clip.bottom - mark.bottom;
304
- const isBelow = above < t.offsetHeight + gap && below > above;
305
- t.classList.toggle("is-below", isBelow);
306
- const centre = mark.left + mark.width / 2;
307
- const ideal = centre - t.offsetWidth / 2;
308
- const left = Math.max(clip.left, Math.min(ideal, clip.right - t.offsetWidth));
309
- t.style.setProperty("--ui-tip-x", `${centre - rect.left - h.clientLeft + h.scrollLeft}px`);
310
- t.style.setProperty("--ui-tip-y", `${(isBelow ? mark.bottom : mark.top) - rect.top - h.clientTop + h.scrollTop}px`);
311
- t.style.setProperty("--ui-tip-shift", `${left - ideal}px`);
312
- }
322
+ const place = () => placeTip(host.current, trigger.current, tip.current);
313
323
  useLayoutEffect2(() => {
314
324
  if (open) place();
315
325
  }, [open, text2]);
@@ -1898,7 +1908,8 @@ import {
1898
1908
  filterPanelFit,
1899
1909
  transitionMs as transitionMs2,
1900
1910
  dropdownViewportFit,
1901
- dropdownHeightFit
1911
+ dropdownHeightFit,
1912
+ filterPanelRow
1902
1913
  } from "@apliteni/apliteni-ui";
1903
1914
  import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
1904
1915
  var EXIT_SLACK_MS2 = 50;
@@ -2001,7 +2012,7 @@ function Dropdown({
2001
2012
  };
2002
2013
  fit();
2003
2014
  const view = root.current?.ownerDocument?.defaultView;
2004
- const row2 = root.current?.closest(".ui-filter-bar__chip")?.closest(".ui-filter-bar");
2015
+ const row2 = filterPanelRow(root.current);
2005
2016
  if (!row2 || !view) return;
2006
2017
  const stop = [];
2007
2018
  let ro = null;
@@ -2036,7 +2047,7 @@ function Dropdown({
2036
2047
  opened.current = true;
2037
2048
  return;
2038
2049
  }
2039
- if (!opened.current || !el || !root.current?.closest(".ui-filter-bar__chip")) return;
2050
+ if (!opened.current || !el || !filterPanelRow(root.current)) return;
2040
2051
  el.classList.add("is-closing");
2041
2052
  let done = false;
2042
2053
  const finish = () => {
@@ -2134,7 +2145,7 @@ function Dropdown({
2134
2145
  const want = landOn.current;
2135
2146
  landOn.current = null;
2136
2147
  if (!open) {
2137
- if (panel.current?.style && root.current?.closest(".ui-filter-bar") && !root.current.closest(".ui-filter-bar__chip")) {
2148
+ if (panel.current?.style && root.current?.closest(".ui-filter-bar") && !filterPanelRow(root.current)) {
2138
2149
  panel.current.style.minWidth = "";
2139
2150
  }
2140
2151
  return;
@@ -2558,18 +2569,43 @@ function Segmented({ options, value, onChange, label, appearance = "pill", size,
2558
2569
  // src/FilterBar.tsx
2559
2570
  import { useRef as useRef11, useState as useState8 } from "react";
2560
2571
  import { filterChipText, filterChipName, filterChipUnset, filterChipItems, focusNextStop } from "@apliteni/apliteni-ui";
2561
- import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
2562
- function FilterBar({ filters, label = "Filters", clearLabel = "Clear all filters", disabled = false, busy = false, onRemove, onClear, onChange }) {
2572
+ import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
2573
+ var SEARCH_AT = 10;
2574
+ var isRow2 = (entry) => entry !== "---" && !entry.separator;
2575
+ function FilterBar({ filters, label = "Filters", clearLabel = "Clear all", add, addLabel = "Filter", disabled = false, busy = false, onRemove, onClear, onChange, onAdd }) {
2563
2576
  const bar = useRef11(null);
2564
2577
  const focused = useRef11(null);
2578
+ const landing = useRef11(null);
2565
2579
  const [opened, setOpened] = useState8({});
2580
+ const [adding, setAdding] = useState8(false);
2566
2581
  const blocked = disabled || busy;
2582
+ const offer = (add || []).filter((entry) => !filters.some((filter) => filter.id === entry.id));
2583
+ const source = /* @__PURE__ */ new Map();
2584
+ const sections = offer.map((entry) => ({ label: entry.label, items: entry.items.map((item) => {
2585
+ if (!isRow2(item)) return item;
2586
+ const copy = { ...item };
2587
+ source.set(copy, entry.id);
2588
+ return copy;
2589
+ }) }));
2590
+ const options = sections.reduce((count, section) => count + section.items.filter(isRow2).length, 0);
2591
+ const offering = Boolean(onAdd && offer.length);
2567
2592
  useIsoLayoutEffect(() => {
2568
2593
  const stale = Object.keys(opened).filter((id) => !filters.some((filter) => filter.id === id && filter.open === opened[id].against));
2569
2594
  if (stale.length) setOpened((previous) => Object.fromEntries(Object.entries(previous).filter(([id]) => !stale.includes(id))));
2595
+ const chips = Array.from(bar.current?.querySelectorAll("[data-filter-id]") ?? []);
2596
+ const asked = landing.current;
2597
+ landing.current = null;
2598
+ if (asked !== null) {
2599
+ const chip = chips.find((c) => c.dataset.filterId === asked && !c.disabled);
2600
+ const trigger = !blocked && chip?.querySelector("[data-dropdown-trigger]");
2601
+ if (trigger) {
2602
+ focused.current = null;
2603
+ trigger.focus();
2604
+ return;
2605
+ }
2606
+ }
2570
2607
  const prior = focused.current;
2571
2608
  if (!prior || prior.id === null && filters.length > 0 || filters.some((f) => f.id === prior.id)) return;
2572
- const chips = Array.from(bar.current?.querySelectorAll("[data-filter-id]") ?? []);
2573
2609
  const next = chips.slice(prior.index).find((chip) => !chip.disabled) || chips.slice(0, prior.index).reverse().find((chip) => !chip.disabled);
2574
2610
  const target = !blocked && next?.querySelector("[data-dropdown-trigger]");
2575
2611
  focused.current = null;
@@ -2632,9 +2668,32 @@ function FilterBar({ filters, label = "Filters", clearLabel = "Clear all filters
2632
2668
  },
2633
2669
  filter.id
2634
2670
  )),
2671
+ offering && /* @__PURE__ */ jsx24("div", { "data-filter-add": "", onFocus: () => {
2672
+ focused.current = null;
2673
+ }, children: /* @__PURE__ */ jsx24(
2674
+ Dropdown,
2675
+ {
2676
+ variant: "menu",
2677
+ sections,
2678
+ ariaLabel: addLabel,
2679
+ triggerContent: /* @__PURE__ */ jsxs19(Fragment6, { children: [
2680
+ /* @__PURE__ */ jsx24(Icon, { name: "plus" }),
2681
+ addLabel
2682
+ ] }),
2683
+ search: options >= SEARCH_AT && { label: `Search ${label.toLowerCase()}` },
2684
+ open: adding && !blocked,
2685
+ onOpenChange: setAdding,
2686
+ onSelect: (value, item) => {
2687
+ const id = source.get(item);
2688
+ if (blocked || !id) return;
2689
+ landing.current = id;
2690
+ onAdd?.(id, value == null ? void 0 : String(value));
2691
+ }
2692
+ }
2693
+ ) }),
2635
2694
  filters.length > 0 && /* @__PURE__ */ jsx24("span", { "data-filter-clear": "", onFocus: () => {
2636
2695
  focused.current = { id: null, index: 0 };
2637
- }, children: /* @__PURE__ */ jsx24(Button, { size: "sm", onClick: () => {
2696
+ }, children: /* @__PURE__ */ jsx24(Button, { size: "sm", variant: "ghost", onClick: () => {
2638
2697
  if (!blocked) onClear();
2639
2698
  }, children: clearLabel }) })
2640
2699
  ]
@@ -2668,7 +2727,7 @@ function DrawerSection({ title, headingLevel = 3, className, children, ...rest }
2668
2727
 
2669
2728
  // src/Snippet.tsx
2670
2729
  import { useEffect as useEffect7, useRef as useRef12, useState as useState9 } from "react";
2671
- import { Fragment as Fragment6, jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
2730
+ import { Fragment as Fragment7, jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
2672
2731
  function Snippet({ label = "shell", code = "", children, copy = true, reveal = false, copyLabel = "Copy code" }) {
2673
2732
  const [status, setStatus] = useState9("idle");
2674
2733
  const request = useRef12(0);
@@ -2696,7 +2755,7 @@ function Snippet({ label = "shell", code = "", children, copy = true, reveal = f
2696
2755
  return /* @__PURE__ */ jsxs21("div", { className: reveal ? "ui-snippet ui-snippet--reveal" : "ui-snippet", children: [
2697
2756
  /* @__PURE__ */ jsxs21("div", { className: "ui-snippet__bar", children: [
2698
2757
  /* @__PURE__ */ jsx26("span", { children: label }),
2699
- copy && /* @__PURE__ */ jsxs21(Fragment6, { children: [
2758
+ copy && /* @__PURE__ */ jsxs21(Fragment7, { children: [
2700
2759
  /* @__PURE__ */ jsx26(
2701
2760
  "button",
2702
2761
  {
@@ -2718,7 +2777,7 @@ function Snippet({ label = "shell", code = "", children, copy = true, reveal = f
2718
2777
  // src/Tabs.tsx
2719
2778
  import { useEffect as useEffect8, useId as useId8, useRef as useRef13 } from "react";
2720
2779
  import { playEntrance } from "@apliteni/apliteni-ui/motion";
2721
- import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
2780
+ import { Fragment as Fragment8, jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
2722
2781
  function Tabs({ items, value, onChange, label }) {
2723
2782
  const id = useId8();
2724
2783
  const previous = useRef13(value);
@@ -2767,7 +2826,7 @@ function Tabs({ items, value, onChange, label }) {
2767
2826
  },
2768
2827
  children: [
2769
2828
  item.label,
2770
- item.count != null && /* @__PURE__ */ jsxs22(Fragment7, { children: [
2829
+ item.count != null && /* @__PURE__ */ jsxs22(Fragment8, { children: [
2771
2830
  " ",
2772
2831
  item.count
2773
2832
  ] })
@@ -2994,7 +3053,7 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
2994
3053
 
2995
3054
  // src/FileDrop.tsx
2996
3055
  import { useRef as useRef16, useState as useState11 } from "react";
2997
- import { Fragment as Fragment8, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
3056
+ import { Fragment as Fragment9, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
2998
3057
  var MARKS = {
2999
3058
  uploading: "clock",
3000
3059
  done: "circleCheck",
@@ -3106,7 +3165,7 @@ function FileDrop({
3106
3165
  children: /* @__PURE__ */ jsx30("span", { style: { width: `${file.progress}%` } })
3107
3166
  }
3108
3167
  )
3109
- ] }) : /* @__PURE__ */ jsxs25(Fragment8, { children: [
3168
+ ] }) : /* @__PURE__ */ jsxs25(Fragment9, { children: [
3110
3169
  /* @__PURE__ */ jsx30(
3111
3170
  Button,
3112
3171
  {
@@ -3303,7 +3362,7 @@ import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName }
3303
3362
  // src/SidebarNav.tsx
3304
3363
  import { forwardRef as forwardRef8, useEffect as useEffect12, useId as useId10, useState as useState13 } from "react";
3305
3364
  import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
3306
- import { Fragment as Fragment9, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
3365
+ import { Fragment as Fragment10, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
3307
3366
  var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
3308
3367
  function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
3309
3368
  const uid = useId10();
@@ -3329,7 +3388,7 @@ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
3329
3388
  item.disabled && "is-disabled",
3330
3389
  item.danger && "is-danger"
3331
3390
  ].filter(Boolean).join(" ");
3332
- const children = /* @__PURE__ */ jsxs28(Fragment9, { children: [
3391
+ const children = /* @__PURE__ */ jsxs28(Fragment10, { children: [
3333
3392
  (item.leading ?? item.icon) && /* @__PURE__ */ jsx33("span", { className: "ui-nav__ic", children: item.leading ?? /* @__PURE__ */ jsx33(Icon, { name: item.icon }) }),
3334
3393
  /* @__PURE__ */ jsx33("span", { className: "ui-nav__label", children: item.label }),
3335
3394
  count != null && count !== "" && /* @__PURE__ */ jsx33("span", { className: `ui-nav__badge is-${typeof item.badge === "object" ? item.badge.tone ?? "neutral" : "neutral"}`, children: count }),
@@ -3398,7 +3457,7 @@ var SidebarNav = forwardRef8(function SidebarNav2({
3398
3457
  });
3399
3458
 
3400
3459
  // src/AppShell.tsx
3401
- import { Fragment as Fragment10, jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
3460
+ import { Fragment as Fragment11, jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
3402
3461
  var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
3403
3462
  function AppShell({
3404
3463
  sections,
@@ -3473,7 +3532,7 @@ function AppShell({
3473
3532
  onClick: (event) => {
3474
3533
  if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
3475
3534
  },
3476
- children: /* @__PURE__ */ jsxs29(Fragment10, { children: [
3535
+ children: /* @__PURE__ */ jsxs29(Fragment11, { children: [
3477
3536
  /* @__PURE__ */ jsx34("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx34(Icon, { name: section.icon }) }),
3478
3537
  /* @__PURE__ */ jsx34("span", { className: "ui-nav__label", children: section.label }),
3479
3538
  section.count != null && /* @__PURE__ */ jsx34("span", { className: "ui-nav__badge is-neutral", children: section.count })
@@ -3796,10 +3855,690 @@ var Success = forwardRef9(function Success2({
3796
3855
  );
3797
3856
  });
3798
3857
 
3858
+ // src/Chart.tsx
3859
+ import {
3860
+ useCallback as useCallback5,
3861
+ useEffect as useEffect15,
3862
+ useId as useId12,
3863
+ useLayoutEffect as useLayoutEffect5,
3864
+ useMemo as useMemo4,
3865
+ useRef as useRef19,
3866
+ useState as useState16
3867
+ } from "react";
3868
+ import { CHART_FLOOR, bridgeWalk, chartScale, pointTarget } from "@apliteni/apliteni-ui";
3869
+ import { Fragment as Fragment12, jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
3870
+ var HEIGHT = { months: 216, bridge: 216, spark: 32 };
3871
+ var PAD = 2;
3872
+ var ZERO_INSET = 1.5;
3873
+ var TARGET = 24;
3874
+ var TAP_FLOOR = 44;
3875
+ var PHONE_MAX2 = 560;
3876
+ var FALLBACK_COL = 48;
3877
+ var BAR_SHARE = 0.5;
3878
+ var BAR_MAX = 24;
3879
+ var BAR_MAX_BRIDGE = 40;
3880
+ var LANE_GAP = 2;
3881
+ var BAR_RADIUS = 4;
3882
+ var MISSING = "\u2014";
3883
+ var cx8 = (...parts) => parts.filter(Boolean).join(" ");
3884
+ var px = (n) => Math.round(n * 10) / 10;
3885
+ var columnOf = (p, fallback) => ({
3886
+ label: p.label,
3887
+ short: p.short || fallback,
3888
+ estimated: Boolean(p.estimated),
3889
+ note: p.note
3890
+ });
3891
+ function onPrevious(value, previous, when) {
3892
+ if (previous === void 0 || previous === 0 || !Number.isFinite(previous) || !when) return "";
3893
+ const pct = (value - previous) / Math.abs(previous) * 100;
3894
+ return `${pct < 0 ? "\u2212" : "+"}${Math.abs(pct).toFixed(1)}% on ${when}`;
3895
+ }
3896
+ function barPath(m) {
3897
+ const r = Math.min(BAR_RADIUS, m.w / 2, m.h / (m.foot ? 1 : 4));
3898
+ const { x, y: top, w, h } = m;
3899
+ const bottom = top + h;
3900
+ const arc = (dx, dy, sweep) => `a${px(r)} ${px(r)} 0 0 ${sweep} ${px(dx)} ${px(dy)}`;
3901
+ if (r <= 0) return `M${px(x)} ${px(top)}h${px(w)}v${px(h)}h${px(-w)}z`;
3902
+ const roundTop = m.foot !== "top";
3903
+ const roundBottom = m.foot !== "bottom";
3904
+ return `M${px(x)} ${px(roundBottom ? bottom - r : bottom)}V${px(roundTop ? top + r : top)}` + (roundTop ? `${arc(r, -r, 1)}h${px(w - r * 2)}${arc(r, r, 1)}` : `h${px(w)}`) + `V${px(roundBottom ? bottom - r : bottom)}` + (roundBottom ? `${arc(-r, r, 1)}h${px(-(w - r * 2))}${arc(-r, -r, 1)}` : `h${px(-w)}`) + "z";
3905
+ }
3906
+ function useTapFloor() {
3907
+ const [floor, setFloor] = useState16(TARGET);
3908
+ useEffect15(() => {
3909
+ const query = typeof window === "undefined" ? null : window.matchMedia?.(`(max-width: ${PHONE_MAX2}px) and (pointer: coarse)`);
3910
+ if (!query) return;
3911
+ const read = () => setFloor(query.matches ? TAP_FLOOR : TARGET);
3912
+ read();
3913
+ query.addEventListener?.("change", read);
3914
+ return () => query.removeEventListener?.("change", read);
3915
+ }, []);
3916
+ return floor;
3917
+ }
3918
+ function stateOf(column) {
3919
+ if (!column?.estimated) return "";
3920
+ return column.note ? `Estimated \u2014 ${column.note}` : "Estimated";
3921
+ }
3922
+ function seriesFrame(periods, series, spark, format) {
3923
+ const columns = periods.map((p) => columnOf(p, p.label.split(" ")[0]));
3924
+ const shapeOf = (s) => s.shape ?? "bars";
3925
+ const split = series.some((s) => shapeOf(s) === "bars-below");
3926
+ const reach = series.flatMap((s) => s.values.map((v) => shapeOf(s) === "bars-below" ? -v : v));
3927
+ const scale = chartScale(spark ? reach : [...reach, 0], spark ? { zero: false, nice: false, floor: CHART_FLOOR } : { zero: true, ticks: 4 });
3928
+ const bars = [];
3929
+ const lines = [];
3930
+ const bandOf = (s) => split ? shapeOf(s) === "bars-below" ? "down" : "up" : "full";
3931
+ const inBand = (band) => series.filter((s) => shapeOf(s) !== "line" && bandOf(s) === band);
3932
+ for (const s of series) {
3933
+ const shape = shapeOf(s);
3934
+ const tone = s.tone ?? (shape === "line" ? "neutral" : "accent");
3935
+ if (shape === "line") {
3936
+ lines.push({ id: s.id, tone, values: s.values.map((v) => Number.isFinite(v) ? v : Number.NaN) });
3937
+ continue;
3938
+ }
3939
+ const below = shape === "bars-below";
3940
+ const band = bandOf(s);
3941
+ const lanes = inBand(band).length;
3942
+ const lane = inBand(band).indexOf(s);
3943
+ s.values.forEach((raw, i) => {
3944
+ if (!Number.isFinite(raw) || i >= columns.length) return;
3945
+ const value = raw;
3946
+ bars.push({
3947
+ id: `${s.id}-${i}`,
3948
+ name: s.name,
3949
+ tone,
3950
+ column: i,
3951
+ from: 0,
3952
+ to: below ? -value : value,
3953
+ detail: onPrevious(value, s.values[i - 1], columns[i - 1]?.short ?? ""),
3954
+ band,
3955
+ lane,
3956
+ lanes
3957
+ });
3958
+ });
3959
+ }
3960
+ return {
3961
+ columns,
3962
+ scale,
3963
+ bars,
3964
+ lines,
3965
+ links: false,
3966
+ keys: series.map((s) => ({
3967
+ id: s.id,
3968
+ name: s.name,
3969
+ shape: shapeOf(s),
3970
+ tone: s.tone ?? (shapeOf(s) === "line" ? "neutral" : "accent")
3971
+ })),
3972
+ tableHead: ["Period", ...series.map((s) => s.name)],
3973
+ tableRows: columns.map((column, i) => ({
3974
+ column,
3975
+ cells: series.map((s) => Number.isFinite(s.values[i]) ? format(s.values[i]) : MISSING)
3976
+ }))
3977
+ };
3978
+ }
3979
+ function bridgeFrame(steps, tones, format) {
3980
+ const rise = tones?.rise ?? "good";
3981
+ const fall = tones?.fall ?? "bad";
3982
+ const total = tones?.total ?? "neutral";
3983
+ const walk = bridgeWalk(steps);
3984
+ const columns = walk.map((step) => columnOf(step, step.label));
3985
+ const toneOf = (step) => step.kind === "total" ? total : step.value < 0 ? fall : rise;
3986
+ const named = [];
3987
+ for (const step of walk) {
3988
+ const tone = toneOf(step);
3989
+ const name = step.kind === "total" ? "Totals" : step.value < 0 ? "Decreases" : "Increases";
3990
+ if (!named.some((k) => k.id === tone)) named.push({ id: tone, name, tone, shape: "bars" });
3991
+ }
3992
+ return {
3993
+ columns,
3994
+ scale: chartScale([...walk.flatMap((s) => [s.from, s.to]), 0], { zero: true, ticks: 4 }),
3995
+ bars: walk.map((step, i) => ({
3996
+ id: `step-${i}`,
3997
+ name: step.label,
3998
+ tone: toneOf(step),
3999
+ column: i,
4000
+ from: step.from,
4001
+ to: step.to,
4002
+ detail: step.kind === "total" ? "" : `Running total ${format(step.to)}`,
4003
+ band: "full",
4004
+ lane: 0,
4005
+ lanes: 1
4006
+ })),
4007
+ lines: [],
4008
+ links: true,
4009
+ keys: named,
4010
+ tableHead: ["Step", "Amount", "Running total"],
4011
+ tableRows: walk.map((step, i) => ({ column: columns[i], cells: [format(step.value), format(step.to)] }))
4012
+ };
4013
+ }
4014
+ function Chart(props) {
4015
+ const {
4016
+ title,
4017
+ format,
4018
+ formatAxis = format,
4019
+ height,
4020
+ table,
4021
+ selectable,
4022
+ selected,
4023
+ onSelect,
4024
+ className
4025
+ } = props;
4026
+ const variant = props.variant ?? "months";
4027
+ const spark = variant === "spark";
4028
+ const plotHeight = height ?? HEIGHT[variant];
4029
+ const showTable = table ?? !spark;
4030
+ const uid = useId12().replace(/[^\w-]/g, "");
4031
+ const frameEl = useRef19(null);
4032
+ const scroller = useRef19(null);
4033
+ const plot = useRef19(null);
4034
+ const tipEl = useRef19(null);
4035
+ const svgEl = useRef19(null);
4036
+ const ignoreMouseUntil = useRef19(0);
4037
+ const touchMoved = useRef19(false);
4038
+ const dismissed = useRef19(null);
4039
+ const opening = useRef19(false);
4040
+ const pointerFocus = useRef19(false);
4041
+ const floor = useTapFloor();
4042
+ const [width, setWidth] = useState16(0);
4043
+ const [openId, setOpenId] = useState16(null);
4044
+ const [rawCursor, setCursor] = useState16(0);
4045
+ const [said, setSaid] = useState16("");
4046
+ const [ownPick, setOwnPick] = useState16(null);
4047
+ const pick = selected === void 0 ? ownPick : selected;
4048
+ const steps = props.variant === "bridge" ? props.steps : void 0;
4049
+ const bridgeTones = props.variant === "bridge" ? props.tones : void 0;
4050
+ const periods = props.variant === "bridge" ? void 0 : props.periods;
4051
+ const series = props.variant === "bridge" ? void 0 : props.series;
4052
+ const frame = useMemo4(() => steps ? bridgeFrame(steps, bridgeTones, format) : seriesFrame(periods ?? [], series ?? [], spark, format), [steps, bridgeTones, periods, series, spark, format]);
4053
+ const { columns, scale, lines, keys, tableHead, tableRows } = frame;
4054
+ const count = columns.length || 1;
4055
+ const cursor = Math.min(rawCursor, Math.max(columns.length - 1, 0));
4056
+ const plotWidth = width || count * FALLBACK_COL;
4057
+ const colWidth = plotWidth / count;
4058
+ const boxHeight = Math.max(plotHeight, floor);
4059
+ const boxTop = -(boxHeight - plotHeight) / 2;
4060
+ const span = scale.max - scale.min || 1;
4061
+ const y = useCallback5(
4062
+ (value) => PAD + (scale.max - value) / span * (plotHeight - PAD * 2),
4063
+ [scale.max, span, plotHeight]
4064
+ );
4065
+ const zero = y(Math.min(Math.max(0, scale.min), scale.max));
4066
+ const lanes = Math.max(1, ...frame.bars.map((b) => b.lanes));
4067
+ const cap = variant === "bridge" ? BAR_MAX_BRIDGE : BAR_MAX;
4068
+ const groupWidth = Math.max(4, Math.min(
4069
+ colWidth * BAR_SHARE,
4070
+ colWidth - 6,
4071
+ cap * lanes + LANE_GAP * (lanes - 1)
4072
+ ));
4073
+ const barWidth = (groupWidth - LANE_GAP * (lanes - 1)) / lanes;
4074
+ const centre = useCallback5((column) => column * colWidth + colWidth / 2, [colWidth]);
4075
+ const marks = useMemo4(() => {
4076
+ const out = frame.bars.map((bar) => {
4077
+ let top = Math.min(y(bar.from), y(bar.to));
4078
+ let bottom = Math.max(y(bar.from), y(bar.to));
4079
+ let foot = null;
4080
+ if (bottom - top > ZERO_INSET + 1) {
4081
+ if (Math.abs(bottom - zero) < 0.5) {
4082
+ bottom -= ZERO_INSET;
4083
+ foot = "bottom";
4084
+ }
4085
+ if (Math.abs(top - zero) < 0.5) {
4086
+ top += ZERO_INSET;
4087
+ foot = "top";
4088
+ }
4089
+ }
4090
+ return {
4091
+ id: bar.id,
4092
+ column: bar.column,
4093
+ name: bar.name,
4094
+ tone: bar.tone,
4095
+ value: bar.band === "down" ? bar.from - bar.to : bar.to - bar.from,
4096
+ detail: bar.detail,
4097
+ kind: "bar",
4098
+ band: bar.band,
4099
+ foot,
4100
+ lane: bar.lane,
4101
+ lanes: bar.lanes,
4102
+ // Every bar in the chart is drawn at one width, and each band centres
4103
+ // the lanes it actually has: a chart with two series above the line and
4104
+ // one below puts the single one in the middle of its column rather than
4105
+ // in the left half of a group it does not fill.
4106
+ x: centre(bar.column) - (bar.lanes * barWidth + LANE_GAP * (bar.lanes - 1)) / 2 + bar.lane * (barWidth + LANE_GAP),
4107
+ y: top,
4108
+ w: barWidth,
4109
+ h: Math.max(bottom - top, 1),
4110
+ estimated: Boolean(frame.columns[bar.column]?.estimated)
4111
+ };
4112
+ });
4113
+ const dots = [];
4114
+ for (const line of lines) {
4115
+ line.values.forEach((value, i) => {
4116
+ if (!Number.isFinite(value)) return;
4117
+ dots.push({
4118
+ id: `${line.id}-${i}`,
4119
+ column: i,
4120
+ name: keys.find((k) => k.id === line.id)?.name ?? line.id,
4121
+ tone: line.tone,
4122
+ value,
4123
+ detail: onPrevious(value, line.values[i - 1], columns[i - 1]?.short ?? ""),
4124
+ kind: "dot",
4125
+ band: "full",
4126
+ foot: null,
4127
+ lane: 0,
4128
+ lanes: 1,
4129
+ x: centre(i) - TARGET / 2,
4130
+ y: y(value) - TARGET / 2,
4131
+ w: TARGET,
4132
+ h: TARGET,
4133
+ estimated: Boolean(columns[i]?.estimated)
4134
+ });
4135
+ });
4136
+ }
4137
+ const box = { left: 0, right: plotWidth, top: boxTop, bottom: boxTop + boxHeight };
4138
+ for (const dot of dots) {
4139
+ const apart = dots.filter((d) => d !== dot && d.column === dot.column).map((d) => d.y + d.h / 2);
4140
+ dot.hit = pointTarget(
4141
+ { x: dot.x + dot.w / 2, y: dot.y + dot.h / 2 },
4142
+ { floor, min: Math.min(TARGET, colWidth), box, column: [dot.column * colWidth, (dot.column + 1) * colWidth], apart }
4143
+ );
4144
+ }
4145
+ return [...out, ...dots];
4146
+ }, [frame, lines, keys, columns, centre, barWidth, groupWidth, y, floor, colWidth, plotWidth, boxTop, boxHeight]);
4147
+ const byId = useMemo4(() => new Map(marks.map((m) => [m.id, m])), [marks]);
4148
+ const lead = useCallback5((column) => marks.find((m) => m.column === column), [marks]);
4149
+ const open = openId ? byId.get(openId) : void 0;
4150
+ useEffect15(() => {
4151
+ const el = plot.current;
4152
+ if (!el) return;
4153
+ const read = () => setWidth(el.clientWidth);
4154
+ read();
4155
+ if (typeof ResizeObserver === "undefined") return;
4156
+ const observer = new ResizeObserver(read);
4157
+ observer.observe(el);
4158
+ return () => observer.disconnect();
4159
+ }, []);
4160
+ const syncEdges = useCallback5(() => {
4161
+ const el = scroller.current;
4162
+ if (!el) return;
4163
+ const last = el.scrollWidth - el.clientWidth;
4164
+ el.classList.toggle("is-more-before", el.scrollLeft > 1);
4165
+ el.classList.toggle("is-more-after", el.scrollLeft < last - 1);
4166
+ }, []);
4167
+ useEffect15(syncEdges, [syncEdges, plotWidth, count]);
4168
+ useLayoutEffect5(() => {
4169
+ if (!openId || !frameEl.current || !tipEl.current || !svgEl.current) return;
4170
+ const anchor = [...svgEl.current.querySelectorAll("[data-anchor]")].find((el) => el.getAttribute("data-anchor") === openId);
4171
+ if (anchor) placeTip(frameEl.current, anchor, tipEl.current, frameEl.current);
4172
+ }, [openId, plotWidth, plotHeight]);
4173
+ useEffect15(() => {
4174
+ if (!openId) return;
4175
+ const doc = frameEl.current?.ownerDocument;
4176
+ const view = doc?.defaultView;
4177
+ if (!doc || !view) return;
4178
+ const hide = () => setOpenId(null);
4179
+ const escape = (event) => {
4180
+ if (event.key !== "Escape") return;
4181
+ dismissed.current = openId;
4182
+ hide();
4183
+ };
4184
+ doc.addEventListener("keydown", escape);
4185
+ view.addEventListener("resize", hide);
4186
+ return () => {
4187
+ doc.removeEventListener("keydown", escape);
4188
+ view.removeEventListener("resize", hide);
4189
+ };
4190
+ }, [openId]);
4191
+ const show = (id) => {
4192
+ if (!id) {
4193
+ setOpenId(null);
4194
+ return;
4195
+ }
4196
+ if (dismissed.current === id) return;
4197
+ dismissed.current = null;
4198
+ setOpenId(id);
4199
+ };
4200
+ const markAt = (target) => target.closest?.("[data-mark]")?.getAttribute("data-mark") ?? null;
4201
+ const announce = useCallback5((column, picked = pick) => {
4202
+ const where = columns[column];
4203
+ if (!where) return;
4204
+ const values = marks.filter((m) => m.column === column).map((m) => `${m.name} ${format(m.value)}`).join(". ") || "No value";
4205
+ const state = stateOf(where);
4206
+ setSaid(`${where.label}. ${values}.${state ? ` ${state}.` : ""}${picked === column ? " Selected." : ""}`);
4207
+ }, [columns, marks, format, pick]);
4208
+ const step = (next) => {
4209
+ const column = Math.max(0, Math.min(next, count - 1));
4210
+ setCursor(column);
4211
+ show(lead(column)?.id);
4212
+ announce(column);
4213
+ const el = scroller.current;
4214
+ if (el && el.scrollWidth > el.clientWidth) {
4215
+ el.scrollLeft = Math.max(0, centre(column) - el.clientWidth / 2);
4216
+ syncEdges();
4217
+ }
4218
+ };
4219
+ const choose = (column) => {
4220
+ if (!selectable) return;
4221
+ if (selected === void 0) setOwnPick(column);
4222
+ onSelect?.(column);
4223
+ };
4224
+ const onKeyDown = (event) => {
4225
+ const steps2 = {
4226
+ ArrowLeft: cursor - 1,
4227
+ ArrowRight: cursor + 1,
4228
+ Home: 0,
4229
+ End: count - 1
4230
+ };
4231
+ if (event.key in steps2) {
4232
+ event.preventDefault();
4233
+ step(steps2[event.key]);
4234
+ return;
4235
+ }
4236
+ if ((event.key === "Enter" || event.key === " ") && selectable) {
4237
+ event.preventDefault();
4238
+ choose(cursor);
4239
+ announce(cursor, cursor);
4240
+ }
4241
+ };
4242
+ const estimated = columns.filter((c) => c.estimated);
4243
+ const estimatedTone = marks.find((m) => m.estimated && m.kind === "bar")?.tone ?? keys[0]?.tone ?? "neutral";
4244
+ const hitBand = (band) => band === "up" ? { y: PAD, h: Math.max(zero - PAD, TARGET) } : band === "down" ? { y: zero, h: Math.max(plotHeight - PAD - zero, TARGET) } : { y: PAD, h: Math.max(plotHeight - PAD * 2, TARGET) };
4245
+ return /* @__PURE__ */ jsxs33(
4246
+ "div",
4247
+ {
4248
+ className: cx8("ui-chart", `ui-chart--${variant}`, selectable && "ui-chart--pick", className),
4249
+ style: {
4250
+ "--ui-chart-h": `${plotHeight}px`,
4251
+ "--ui-chart-box": `${px(boxHeight)}px`,
4252
+ "--ui-chart-cols": count,
4253
+ "--ui-chart-pad": `${PAD}px`
4254
+ },
4255
+ children: [
4256
+ !spark && keys.length > 0 && /* @__PURE__ */ jsxs33("ul", { className: "ui-chart__legend", children: [
4257
+ keys.map((key) => /* @__PURE__ */ jsxs33("li", { className: cx8("ui-chart__key", `ui-chart__tone--${key.tone}`), children: [
4258
+ key.shape === "line" ? /* @__PURE__ */ jsx38("svg", { className: "ui-chart__key-mark", width: "18", height: "12", viewBox: "0 0 18 12", "aria-hidden": "true", children: /* @__PURE__ */ jsx38("path", { className: "ui-chart__key-line", d: "M1 9 L6 5 L12 7 L17 3" }) }) : /* @__PURE__ */ jsx38("svg", { className: "ui-chart__key-mark", width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: /* @__PURE__ */ jsx38(
4259
+ "rect",
4260
+ {
4261
+ className: "ui-chart__key-swatch",
4262
+ x: "1",
4263
+ width: "10",
4264
+ height: "5",
4265
+ rx: "1",
4266
+ y: key.shape === "bars-below" ? 6 : 1
4267
+ }
4268
+ ) }),
4269
+ key.name
4270
+ ] }, key.id)),
4271
+ estimated.length > 0 && /* @__PURE__ */ jsxs33("li", { className: cx8("ui-chart__key", `ui-chart__tone--${estimatedTone}`), children: [
4272
+ /* @__PURE__ */ jsx38("svg", { className: "ui-chart__key-mark", width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: /* @__PURE__ */ jsx38("rect", { className: "ui-chart__key-estimated", x: "1.5", y: "1.5", width: "9", height: "9", rx: "1" }) }),
4273
+ "Estimated"
4274
+ ] })
4275
+ ] }),
4276
+ /* @__PURE__ */ jsxs33(
4277
+ "div",
4278
+ {
4279
+ ref: frameEl,
4280
+ className: "ui-chart__frame ui-tip-host ui-focusable",
4281
+ tabIndex: 0,
4282
+ role: "group",
4283
+ "aria-label": `${title}. Arrow keys step through the ${columns.length} columns.`,
4284
+ onKeyDown,
4285
+ onFocus: (event) => {
4286
+ if (event.target !== event.currentTarget) return;
4287
+ if (pointerFocus.current) {
4288
+ pointerFocus.current = false;
4289
+ return;
4290
+ }
4291
+ show(lead(cursor)?.id);
4292
+ announce(cursor);
4293
+ },
4294
+ onBlur: (event) => {
4295
+ if (event.target !== event.currentTarget) return;
4296
+ setOpenId(null);
4297
+ setSaid("");
4298
+ },
4299
+ children: [
4300
+ !spark && scale.ticks.length > 0 && /* @__PURE__ */ jsx38("div", { className: "ui-chart__axis", "aria-hidden": "true", children: [...scale.ticks].reverse().map((tick) => /* @__PURE__ */ jsx38("span", { className: "ui-chart__tick", children: formatAxis(tick) }, tick)) }),
4301
+ /* @__PURE__ */ jsx38("div", { ref: scroller, className: "ui-chart__scroll", tabIndex: -1, onScroll: () => {
4302
+ syncEdges();
4303
+ const anchor = [...svgEl.current?.querySelectorAll("[data-anchor]") ?? []].find((el) => el.getAttribute("data-anchor") === openId);
4304
+ if (!anchor || !scroller.current || !frameEl.current || !tipEl.current) return;
4305
+ const mark = anchor.getBoundingClientRect();
4306
+ const view = scroller.current.getBoundingClientRect();
4307
+ if (mark.right <= view.left || mark.left >= view.right) setOpenId(null);
4308
+ else placeTip(frameEl.current, anchor, tipEl.current, frameEl.current);
4309
+ }, children: /* @__PURE__ */ jsxs33("div", { ref: plot, className: "ui-chart__plot", children: [
4310
+ /* @__PURE__ */ jsxs33(
4311
+ "svg",
4312
+ {
4313
+ ref: svgEl,
4314
+ className: "ui-chart__svg",
4315
+ width: px(plotWidth),
4316
+ height: px(boxHeight),
4317
+ viewBox: `0 ${px(boxTop)} ${px(plotWidth)} ${px(boxHeight)}`,
4318
+ role: "img",
4319
+ "aria-label": spark && marks.length > 0 ? `${title}. ${format(marks[0].value)} to ${format(marks[marks.length - 1].value)}.` : title,
4320
+ onMouseMove: (event) => {
4321
+ if (Date.now() < ignoreMouseUntil.current) return;
4322
+ show(markAt(event.target));
4323
+ },
4324
+ onMouseLeave: () => {
4325
+ if (Date.now() < ignoreMouseUntil.current) return;
4326
+ dismissed.current = null;
4327
+ setOpenId(null);
4328
+ },
4329
+ onTouchStart: () => {
4330
+ ignoreMouseUntil.current = Date.now() + 800;
4331
+ touchMoved.current = false;
4332
+ },
4333
+ onTouchMove: () => {
4334
+ touchMoved.current = true;
4335
+ },
4336
+ onTouchCancel: () => {
4337
+ touchMoved.current = true;
4338
+ },
4339
+ onTouchEnd: (event) => {
4340
+ ignoreMouseUntil.current = Date.now() + 800;
4341
+ if (touchMoved.current) return;
4342
+ const id = markAt(event.target);
4343
+ if (id && id === openId) {
4344
+ opening.current = false;
4345
+ setOpenId(null);
4346
+ } else {
4347
+ opening.current = Boolean(id) && dismissed.current !== id;
4348
+ show(id);
4349
+ }
4350
+ },
4351
+ onPointerDown: () => {
4352
+ pointerFocus.current = true;
4353
+ },
4354
+ onClick: (event) => {
4355
+ frameEl.current?.focus();
4356
+ pointerFocus.current = false;
4357
+ const opened = opening.current;
4358
+ opening.current = false;
4359
+ const mark = byId.get(markAt(event.target) ?? "");
4360
+ if (!mark) return;
4361
+ setCursor(mark.column);
4362
+ if (opened) {
4363
+ announce(mark.column);
4364
+ return;
4365
+ }
4366
+ choose(mark.column);
4367
+ announce(mark.column, selectable ? mark.column : pick);
4368
+ },
4369
+ children: [
4370
+ !spark && pick !== null && pick >= 0 && pick < count && /* @__PURE__ */ jsx38(
4371
+ "rect",
4372
+ {
4373
+ className: "ui-chart__band",
4374
+ x: px(pick * colWidth),
4375
+ width: px(colWidth),
4376
+ y: "0",
4377
+ height: px(plotHeight)
4378
+ }
4379
+ ),
4380
+ frame.links && frame.bars.slice(0, -1).map((bar, i) => {
4381
+ const from = byId.get(bar.id);
4382
+ const to = byId.get(frame.bars[i + 1].id);
4383
+ return from && to ? /* @__PURE__ */ jsx38(
4384
+ "line",
4385
+ {
4386
+ className: "ui-chart__carry",
4387
+ x1: px(from.x + from.w),
4388
+ x2: px(to.x),
4389
+ y1: px(y(bar.to)),
4390
+ y2: px(y(bar.to))
4391
+ },
4392
+ `carry-${bar.id}`
4393
+ ) : null;
4394
+ }),
4395
+ !spark && scale.ticks.filter((tick) => tick !== 0).map((tick) => /* @__PURE__ */ jsx38(
4396
+ "line",
4397
+ {
4398
+ className: "ui-chart__grid",
4399
+ x1: "0",
4400
+ x2: px(plotWidth),
4401
+ y1: px(y(tick)),
4402
+ y2: px(y(tick))
4403
+ },
4404
+ `grid-${tick}`
4405
+ )),
4406
+ !spark && scale.ticks.includes(0) && /* @__PURE__ */ jsx38(
4407
+ "line",
4408
+ {
4409
+ className: "ui-chart__zero",
4410
+ x1: "0",
4411
+ x2: px(plotWidth),
4412
+ y1: px(y(0)),
4413
+ y2: px(y(0))
4414
+ }
4415
+ ),
4416
+ marks.filter((m) => m.kind === "bar").map((mark) => /* @__PURE__ */ jsx38(
4417
+ "path",
4418
+ {
4419
+ className: cx8(
4420
+ "ui-chart__bar",
4421
+ `ui-chart__tone--${mark.tone}`,
4422
+ mark.estimated && "is-estimated"
4423
+ ),
4424
+ d: barPath(mark)
4425
+ },
4426
+ `bar-${mark.id}`
4427
+ )),
4428
+ ["casing", "stroke"].map((layer) => lines.flatMap(
4429
+ (line) => line.values.slice(1).map((value, i) => Number.isFinite(value) && Number.isFinite(line.values[i]) ? /* @__PURE__ */ jsx38(
4430
+ "line",
4431
+ {
4432
+ className: layer === "casing" ? "ui-chart__line-casing" : cx8(
4433
+ "ui-chart__line",
4434
+ `ui-chart__tone--${line.tone}`,
4435
+ (columns[i]?.estimated || columns[i + 1]?.estimated) && "is-estimated"
4436
+ ),
4437
+ x1: px(centre(i)),
4438
+ y1: px(y(line.values[i])),
4439
+ x2: px(centre(i + 1)),
4440
+ y2: px(y(value))
4441
+ },
4442
+ `${line.id}-${layer}-${i}`
4443
+ ) : null)
4444
+ )),
4445
+ lines.flatMap((line) => line.values.map((value, i) => Number.isFinite(value) && !Number.isFinite(line.values[i - 1]) && !Number.isFinite(line.values[i + 1]) ? /* @__PURE__ */ jsx38(
4446
+ "circle",
4447
+ {
4448
+ className: cx8("ui-chart__isolated", `ui-chart__tone--${line.tone}`),
4449
+ cx: px(centre(i)),
4450
+ cy: px(y(value)),
4451
+ r: "2"
4452
+ },
4453
+ `isolated-${line.id}-${i}`
4454
+ ) : null)),
4455
+ marks.map((mark) => /* @__PURE__ */ jsxs33("g", { "data-mark": mark.id, children: [
4456
+ mark.kind === "bar" ? /* @__PURE__ */ jsx38(
4457
+ "rect",
4458
+ {
4459
+ className: "ui-chart__hit",
4460
+ x: px(mark.column * colWidth + mark.lane * colWidth / mark.lanes),
4461
+ width: px(colWidth / mark.lanes),
4462
+ y: px(hitBand(mark.band).y),
4463
+ height: px(hitBand(mark.band).h)
4464
+ }
4465
+ ) : /* @__PURE__ */ jsx38(
4466
+ "rect",
4467
+ {
4468
+ className: "ui-chart__hit",
4469
+ x: px(mark.hit?.x ?? mark.x),
4470
+ y: px(mark.hit?.y ?? mark.y),
4471
+ width: px(mark.hit?.w ?? TARGET),
4472
+ height: px(mark.hit?.h ?? TARGET)
4473
+ }
4474
+ ),
4475
+ /* @__PURE__ */ jsx38(
4476
+ "rect",
4477
+ {
4478
+ "data-anchor": mark.id,
4479
+ className: "ui-chart__anchor",
4480
+ x: px(mark.x),
4481
+ y: px(mark.y),
4482
+ width: px(mark.w),
4483
+ height: "0"
4484
+ }
4485
+ )
4486
+ ] }, `hit-${mark.id}`))
4487
+ ]
4488
+ }
4489
+ ),
4490
+ !spark && /* @__PURE__ */ jsx38("div", { className: "ui-chart__periods", "aria-hidden": "true", children: columns.map((column, i) => /* @__PURE__ */ jsx38(
4491
+ "span",
4492
+ {
4493
+ className: cx8("ui-chart__period", pick === i && "is-selected"),
4494
+ children: column.short
4495
+ },
4496
+ `${column.label}-${i}`
4497
+ )) })
4498
+ ] }) }),
4499
+ /* @__PURE__ */ jsx38("span", { ref: tipEl, className: cx8("ui-tip", openId && "is-open"), role: open ? "tooltip" : void 0, children: open && /* @__PURE__ */ jsxs33(Fragment12, { children: [
4500
+ /* @__PURE__ */ jsx38("span", { className: "ui-tip__label", children: spark ? columns[open.column]?.label : `${open.name}, ${columns[open.column]?.label}` }),
4501
+ /* @__PURE__ */ jsx38("span", { className: "ui-tip__value", children: format(open.value) }),
4502
+ (stateOf(columns[open.column]) || open.detail) && /* @__PURE__ */ jsx38("span", { className: "ui-tip__detail", children: stateOf(columns[open.column]) || open.detail })
4503
+ ] }) })
4504
+ ]
4505
+ }
4506
+ ),
4507
+ /* @__PURE__ */ jsx38("div", { className: "ui-sr", role: "status", children: said }),
4508
+ showTable && /* @__PURE__ */ jsxs33("details", { className: "ui-chart__data", children: [
4509
+ /* @__PURE__ */ jsx38("summary", { className: "ui-focusable", children: "Values as a table" }),
4510
+ /* @__PURE__ */ jsx38(
4511
+ "div",
4512
+ {
4513
+ className: "ui-table-scroll",
4514
+ role: "region",
4515
+ tabIndex: 0,
4516
+ "aria-label": `${title}, as a table. Scroll for more columns.`,
4517
+ children: /* @__PURE__ */ jsxs33("table", { className: "ui-table", "aria-label": title, children: [
4518
+ /* @__PURE__ */ jsx38("thead", { children: /* @__PURE__ */ jsx38("tr", { children: tableHead.map((head, i) => /* @__PURE__ */ jsx38("th", { scope: "col", className: cx8(i > 0 && "ui-table__num"), children: head }, head)) }) }),
4519
+ /* @__PURE__ */ jsx38("tbody", { children: tableRows.map((row, i) => /* @__PURE__ */ jsxs33("tr", { "aria-current": pick === i ? true : void 0, children: [
4520
+ /* @__PURE__ */ jsxs33("th", { scope: "row", children: [
4521
+ row.column.label,
4522
+ row.column.estimated && /* @__PURE__ */ jsxs33(Fragment12, { children: [
4523
+ " ",
4524
+ /* @__PURE__ */ jsx38("span", { className: "ui-chart__state", children: stateOf(row.column) })
4525
+ ] })
4526
+ ] }),
4527
+ row.cells.map((cell, j) => /* @__PURE__ */ jsx38("td", { className: "ui-table__num", children: cell }, tableHead[j + 1]))
4528
+ ] }, `${row.column.label}-${i}`)) })
4529
+ ] })
4530
+ }
4531
+ )
4532
+ ] })
4533
+ ]
4534
+ }
4535
+ );
4536
+ }
4537
+
3799
4538
  // src/AccentPicker.tsx
3800
4539
  import { forwardRef as forwardRef10 } from "react";
3801
4540
  import { ACCENTS, accentSwatchStyle } from "@apliteni/apliteni-ui";
3802
- import { jsx as jsx38 } from "react/jsx-runtime";
4541
+ import { jsx as jsx39 } from "react/jsx-runtime";
3803
4542
  var AccentPicker = forwardRef10(function AccentPicker2({
3804
4543
  value,
3805
4544
  onChange,
@@ -3808,7 +4547,7 @@ var AccentPicker = forwardRef10(function AccentPicker2({
3808
4547
  "aria-label": label = "Accent",
3809
4548
  ...rest
3810
4549
  }, ref) {
3811
- return /* @__PURE__ */ jsx38(
4550
+ return /* @__PURE__ */ jsx39(
3812
4551
  "div",
3813
4552
  {
3814
4553
  ...rest,
@@ -3819,7 +4558,7 @@ var AccentPicker = forwardRef10(function AccentPicker2({
3819
4558
  "aria-label": label,
3820
4559
  children: options.map((accent) => {
3821
4560
  const title = accent.charAt(0).toUpperCase() + accent.slice(1);
3822
- return /* @__PURE__ */ jsx38(
4561
+ return /* @__PURE__ */ jsx39(
3823
4562
  "button",
3824
4563
  {
3825
4564
  type: "button",
@@ -3849,6 +4588,7 @@ export {
3849
4588
  Button,
3850
4589
  Callout,
3851
4590
  Card,
4591
+ Chart,
3852
4592
  Checkbox,
3853
4593
  CommandPalette,
3854
4594
  Confirm,