@apliteni/apliteni-ui 0.113.0 → 0.114.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]);
@@ -3796,10 +3806,690 @@ var Success = forwardRef9(function Success2({
3796
3806
  );
3797
3807
  });
3798
3808
 
3809
+ // src/Chart.tsx
3810
+ import {
3811
+ useCallback as useCallback5,
3812
+ useEffect as useEffect15,
3813
+ useId as useId12,
3814
+ useLayoutEffect as useLayoutEffect5,
3815
+ useMemo as useMemo4,
3816
+ useRef as useRef19,
3817
+ useState as useState16
3818
+ } from "react";
3819
+ import { CHART_FLOOR, bridgeWalk, chartScale, pointTarget } from "@apliteni/apliteni-ui";
3820
+ import { Fragment as Fragment11, jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
3821
+ var HEIGHT = { months: 216, bridge: 216, spark: 32 };
3822
+ var PAD = 2;
3823
+ var ZERO_INSET = 1.5;
3824
+ var TARGET = 24;
3825
+ var TAP_FLOOR = 44;
3826
+ var PHONE_MAX2 = 560;
3827
+ var FALLBACK_COL = 48;
3828
+ var BAR_SHARE = 0.5;
3829
+ var BAR_MAX = 24;
3830
+ var BAR_MAX_BRIDGE = 40;
3831
+ var LANE_GAP = 2;
3832
+ var BAR_RADIUS = 4;
3833
+ var MISSING = "\u2014";
3834
+ var cx8 = (...parts) => parts.filter(Boolean).join(" ");
3835
+ var px = (n) => Math.round(n * 10) / 10;
3836
+ var columnOf = (p, fallback) => ({
3837
+ label: p.label,
3838
+ short: p.short || fallback,
3839
+ estimated: Boolean(p.estimated),
3840
+ note: p.note
3841
+ });
3842
+ function onPrevious(value, previous, when) {
3843
+ if (previous === void 0 || previous === 0 || !Number.isFinite(previous) || !when) return "";
3844
+ const pct = (value - previous) / Math.abs(previous) * 100;
3845
+ return `${pct < 0 ? "\u2212" : "+"}${Math.abs(pct).toFixed(1)}% on ${when}`;
3846
+ }
3847
+ function barPath(m) {
3848
+ const r = Math.min(BAR_RADIUS, m.w / 2, m.h / (m.foot ? 1 : 4));
3849
+ const { x, y: top, w, h } = m;
3850
+ const bottom = top + h;
3851
+ const arc = (dx, dy, sweep) => `a${px(r)} ${px(r)} 0 0 ${sweep} ${px(dx)} ${px(dy)}`;
3852
+ if (r <= 0) return `M${px(x)} ${px(top)}h${px(w)}v${px(h)}h${px(-w)}z`;
3853
+ const roundTop = m.foot !== "top";
3854
+ const roundBottom = m.foot !== "bottom";
3855
+ 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";
3856
+ }
3857
+ function useTapFloor() {
3858
+ const [floor, setFloor] = useState16(TARGET);
3859
+ useEffect15(() => {
3860
+ const query = typeof window === "undefined" ? null : window.matchMedia?.(`(max-width: ${PHONE_MAX2}px) and (pointer: coarse)`);
3861
+ if (!query) return;
3862
+ const read = () => setFloor(query.matches ? TAP_FLOOR : TARGET);
3863
+ read();
3864
+ query.addEventListener?.("change", read);
3865
+ return () => query.removeEventListener?.("change", read);
3866
+ }, []);
3867
+ return floor;
3868
+ }
3869
+ function stateOf(column) {
3870
+ if (!column?.estimated) return "";
3871
+ return column.note ? `Estimated \u2014 ${column.note}` : "Estimated";
3872
+ }
3873
+ function seriesFrame(periods, series, spark, format) {
3874
+ const columns = periods.map((p) => columnOf(p, p.label.split(" ")[0]));
3875
+ const shapeOf = (s) => s.shape ?? "bars";
3876
+ const split = series.some((s) => shapeOf(s) === "bars-below");
3877
+ const reach = series.flatMap((s) => s.values.map((v) => shapeOf(s) === "bars-below" ? -v : v));
3878
+ const scale = chartScale(spark ? reach : [...reach, 0], spark ? { zero: false, nice: false, floor: CHART_FLOOR } : { zero: true, ticks: 4 });
3879
+ const bars = [];
3880
+ const lines = [];
3881
+ const bandOf = (s) => split ? shapeOf(s) === "bars-below" ? "down" : "up" : "full";
3882
+ const inBand = (band) => series.filter((s) => shapeOf(s) !== "line" && bandOf(s) === band);
3883
+ for (const s of series) {
3884
+ const shape = shapeOf(s);
3885
+ const tone = s.tone ?? (shape === "line" ? "neutral" : "accent");
3886
+ if (shape === "line") {
3887
+ lines.push({ id: s.id, tone, values: s.values.map((v) => Number.isFinite(v) ? v : Number.NaN) });
3888
+ continue;
3889
+ }
3890
+ const below = shape === "bars-below";
3891
+ const band = bandOf(s);
3892
+ const lanes = inBand(band).length;
3893
+ const lane = inBand(band).indexOf(s);
3894
+ s.values.forEach((raw, i) => {
3895
+ if (!Number.isFinite(raw) || i >= columns.length) return;
3896
+ const value = raw;
3897
+ bars.push({
3898
+ id: `${s.id}-${i}`,
3899
+ name: s.name,
3900
+ tone,
3901
+ column: i,
3902
+ from: 0,
3903
+ to: below ? -value : value,
3904
+ detail: onPrevious(value, s.values[i - 1], columns[i - 1]?.short ?? ""),
3905
+ band,
3906
+ lane,
3907
+ lanes
3908
+ });
3909
+ });
3910
+ }
3911
+ return {
3912
+ columns,
3913
+ scale,
3914
+ bars,
3915
+ lines,
3916
+ links: false,
3917
+ keys: series.map((s) => ({
3918
+ id: s.id,
3919
+ name: s.name,
3920
+ shape: shapeOf(s),
3921
+ tone: s.tone ?? (shapeOf(s) === "line" ? "neutral" : "accent")
3922
+ })),
3923
+ tableHead: ["Period", ...series.map((s) => s.name)],
3924
+ tableRows: columns.map((column, i) => ({
3925
+ column,
3926
+ cells: series.map((s) => Number.isFinite(s.values[i]) ? format(s.values[i]) : MISSING)
3927
+ }))
3928
+ };
3929
+ }
3930
+ function bridgeFrame(steps, tones, format) {
3931
+ const rise = tones?.rise ?? "good";
3932
+ const fall = tones?.fall ?? "bad";
3933
+ const total = tones?.total ?? "neutral";
3934
+ const walk = bridgeWalk(steps);
3935
+ const columns = walk.map((step) => columnOf(step, step.label));
3936
+ const toneOf = (step) => step.kind === "total" ? total : step.value < 0 ? fall : rise;
3937
+ const named = [];
3938
+ for (const step of walk) {
3939
+ const tone = toneOf(step);
3940
+ const name = step.kind === "total" ? "Totals" : step.value < 0 ? "Decreases" : "Increases";
3941
+ if (!named.some((k) => k.id === tone)) named.push({ id: tone, name, tone, shape: "bars" });
3942
+ }
3943
+ return {
3944
+ columns,
3945
+ scale: chartScale([...walk.flatMap((s) => [s.from, s.to]), 0], { zero: true, ticks: 4 }),
3946
+ bars: walk.map((step, i) => ({
3947
+ id: `step-${i}`,
3948
+ name: step.label,
3949
+ tone: toneOf(step),
3950
+ column: i,
3951
+ from: step.from,
3952
+ to: step.to,
3953
+ detail: step.kind === "total" ? "" : `Running total ${format(step.to)}`,
3954
+ band: "full",
3955
+ lane: 0,
3956
+ lanes: 1
3957
+ })),
3958
+ lines: [],
3959
+ links: true,
3960
+ keys: named,
3961
+ tableHead: ["Step", "Amount", "Running total"],
3962
+ tableRows: walk.map((step, i) => ({ column: columns[i], cells: [format(step.value), format(step.to)] }))
3963
+ };
3964
+ }
3965
+ function Chart(props) {
3966
+ const {
3967
+ title,
3968
+ format,
3969
+ formatAxis = format,
3970
+ height,
3971
+ table,
3972
+ selectable,
3973
+ selected,
3974
+ onSelect,
3975
+ className
3976
+ } = props;
3977
+ const variant = props.variant ?? "months";
3978
+ const spark = variant === "spark";
3979
+ const plotHeight = height ?? HEIGHT[variant];
3980
+ const showTable = table ?? !spark;
3981
+ const uid = useId12().replace(/[^\w-]/g, "");
3982
+ const frameEl = useRef19(null);
3983
+ const scroller = useRef19(null);
3984
+ const plot = useRef19(null);
3985
+ const tipEl = useRef19(null);
3986
+ const svgEl = useRef19(null);
3987
+ const ignoreMouseUntil = useRef19(0);
3988
+ const touchMoved = useRef19(false);
3989
+ const dismissed = useRef19(null);
3990
+ const opening = useRef19(false);
3991
+ const pointerFocus = useRef19(false);
3992
+ const floor = useTapFloor();
3993
+ const [width, setWidth] = useState16(0);
3994
+ const [openId, setOpenId] = useState16(null);
3995
+ const [rawCursor, setCursor] = useState16(0);
3996
+ const [said, setSaid] = useState16("");
3997
+ const [ownPick, setOwnPick] = useState16(null);
3998
+ const pick = selected === void 0 ? ownPick : selected;
3999
+ const steps = props.variant === "bridge" ? props.steps : void 0;
4000
+ const bridgeTones = props.variant === "bridge" ? props.tones : void 0;
4001
+ const periods = props.variant === "bridge" ? void 0 : props.periods;
4002
+ const series = props.variant === "bridge" ? void 0 : props.series;
4003
+ const frame = useMemo4(() => steps ? bridgeFrame(steps, bridgeTones, format) : seriesFrame(periods ?? [], series ?? [], spark, format), [steps, bridgeTones, periods, series, spark, format]);
4004
+ const { columns, scale, lines, keys, tableHead, tableRows } = frame;
4005
+ const count = columns.length || 1;
4006
+ const cursor = Math.min(rawCursor, Math.max(columns.length - 1, 0));
4007
+ const plotWidth = width || count * FALLBACK_COL;
4008
+ const colWidth = plotWidth / count;
4009
+ const boxHeight = Math.max(plotHeight, floor);
4010
+ const boxTop = -(boxHeight - plotHeight) / 2;
4011
+ const span = scale.max - scale.min || 1;
4012
+ const y = useCallback5(
4013
+ (value) => PAD + (scale.max - value) / span * (plotHeight - PAD * 2),
4014
+ [scale.max, span, plotHeight]
4015
+ );
4016
+ const zero = y(Math.min(Math.max(0, scale.min), scale.max));
4017
+ const lanes = Math.max(1, ...frame.bars.map((b) => b.lanes));
4018
+ const cap = variant === "bridge" ? BAR_MAX_BRIDGE : BAR_MAX;
4019
+ const groupWidth = Math.max(4, Math.min(
4020
+ colWidth * BAR_SHARE,
4021
+ colWidth - 6,
4022
+ cap * lanes + LANE_GAP * (lanes - 1)
4023
+ ));
4024
+ const barWidth = (groupWidth - LANE_GAP * (lanes - 1)) / lanes;
4025
+ const centre = useCallback5((column) => column * colWidth + colWidth / 2, [colWidth]);
4026
+ const marks = useMemo4(() => {
4027
+ const out = frame.bars.map((bar) => {
4028
+ let top = Math.min(y(bar.from), y(bar.to));
4029
+ let bottom = Math.max(y(bar.from), y(bar.to));
4030
+ let foot = null;
4031
+ if (bottom - top > ZERO_INSET + 1) {
4032
+ if (Math.abs(bottom - zero) < 0.5) {
4033
+ bottom -= ZERO_INSET;
4034
+ foot = "bottom";
4035
+ }
4036
+ if (Math.abs(top - zero) < 0.5) {
4037
+ top += ZERO_INSET;
4038
+ foot = "top";
4039
+ }
4040
+ }
4041
+ return {
4042
+ id: bar.id,
4043
+ column: bar.column,
4044
+ name: bar.name,
4045
+ tone: bar.tone,
4046
+ value: bar.band === "down" ? bar.from - bar.to : bar.to - bar.from,
4047
+ detail: bar.detail,
4048
+ kind: "bar",
4049
+ band: bar.band,
4050
+ foot,
4051
+ lane: bar.lane,
4052
+ lanes: bar.lanes,
4053
+ // Every bar in the chart is drawn at one width, and each band centres
4054
+ // the lanes it actually has: a chart with two series above the line and
4055
+ // one below puts the single one in the middle of its column rather than
4056
+ // in the left half of a group it does not fill.
4057
+ x: centre(bar.column) - (bar.lanes * barWidth + LANE_GAP * (bar.lanes - 1)) / 2 + bar.lane * (barWidth + LANE_GAP),
4058
+ y: top,
4059
+ w: barWidth,
4060
+ h: Math.max(bottom - top, 1),
4061
+ estimated: Boolean(frame.columns[bar.column]?.estimated)
4062
+ };
4063
+ });
4064
+ const dots = [];
4065
+ for (const line of lines) {
4066
+ line.values.forEach((value, i) => {
4067
+ if (!Number.isFinite(value)) return;
4068
+ dots.push({
4069
+ id: `${line.id}-${i}`,
4070
+ column: i,
4071
+ name: keys.find((k) => k.id === line.id)?.name ?? line.id,
4072
+ tone: line.tone,
4073
+ value,
4074
+ detail: onPrevious(value, line.values[i - 1], columns[i - 1]?.short ?? ""),
4075
+ kind: "dot",
4076
+ band: "full",
4077
+ foot: null,
4078
+ lane: 0,
4079
+ lanes: 1,
4080
+ x: centre(i) - TARGET / 2,
4081
+ y: y(value) - TARGET / 2,
4082
+ w: TARGET,
4083
+ h: TARGET,
4084
+ estimated: Boolean(columns[i]?.estimated)
4085
+ });
4086
+ });
4087
+ }
4088
+ const box = { left: 0, right: plotWidth, top: boxTop, bottom: boxTop + boxHeight };
4089
+ for (const dot of dots) {
4090
+ const apart = dots.filter((d) => d !== dot && d.column === dot.column).map((d) => d.y + d.h / 2);
4091
+ dot.hit = pointTarget(
4092
+ { x: dot.x + dot.w / 2, y: dot.y + dot.h / 2 },
4093
+ { floor, min: Math.min(TARGET, colWidth), box, column: [dot.column * colWidth, (dot.column + 1) * colWidth], apart }
4094
+ );
4095
+ }
4096
+ return [...out, ...dots];
4097
+ }, [frame, lines, keys, columns, centre, barWidth, groupWidth, y, floor, colWidth, plotWidth, boxTop, boxHeight]);
4098
+ const byId = useMemo4(() => new Map(marks.map((m) => [m.id, m])), [marks]);
4099
+ const lead = useCallback5((column) => marks.find((m) => m.column === column), [marks]);
4100
+ const open = openId ? byId.get(openId) : void 0;
4101
+ useEffect15(() => {
4102
+ const el = plot.current;
4103
+ if (!el) return;
4104
+ const read = () => setWidth(el.clientWidth);
4105
+ read();
4106
+ if (typeof ResizeObserver === "undefined") return;
4107
+ const observer = new ResizeObserver(read);
4108
+ observer.observe(el);
4109
+ return () => observer.disconnect();
4110
+ }, []);
4111
+ const syncEdges = useCallback5(() => {
4112
+ const el = scroller.current;
4113
+ if (!el) return;
4114
+ const last = el.scrollWidth - el.clientWidth;
4115
+ el.classList.toggle("is-more-before", el.scrollLeft > 1);
4116
+ el.classList.toggle("is-more-after", el.scrollLeft < last - 1);
4117
+ }, []);
4118
+ useEffect15(syncEdges, [syncEdges, plotWidth, count]);
4119
+ useLayoutEffect5(() => {
4120
+ if (!openId || !frameEl.current || !tipEl.current || !svgEl.current) return;
4121
+ const anchor = [...svgEl.current.querySelectorAll("[data-anchor]")].find((el) => el.getAttribute("data-anchor") === openId);
4122
+ if (anchor) placeTip(frameEl.current, anchor, tipEl.current, frameEl.current);
4123
+ }, [openId, plotWidth, plotHeight]);
4124
+ useEffect15(() => {
4125
+ if (!openId) return;
4126
+ const doc = frameEl.current?.ownerDocument;
4127
+ const view = doc?.defaultView;
4128
+ if (!doc || !view) return;
4129
+ const hide = () => setOpenId(null);
4130
+ const escape = (event) => {
4131
+ if (event.key !== "Escape") return;
4132
+ dismissed.current = openId;
4133
+ hide();
4134
+ };
4135
+ doc.addEventListener("keydown", escape);
4136
+ view.addEventListener("resize", hide);
4137
+ return () => {
4138
+ doc.removeEventListener("keydown", escape);
4139
+ view.removeEventListener("resize", hide);
4140
+ };
4141
+ }, [openId]);
4142
+ const show = (id) => {
4143
+ if (!id) {
4144
+ setOpenId(null);
4145
+ return;
4146
+ }
4147
+ if (dismissed.current === id) return;
4148
+ dismissed.current = null;
4149
+ setOpenId(id);
4150
+ };
4151
+ const markAt = (target) => target.closest?.("[data-mark]")?.getAttribute("data-mark") ?? null;
4152
+ const announce = useCallback5((column, picked = pick) => {
4153
+ const where = columns[column];
4154
+ if (!where) return;
4155
+ const values = marks.filter((m) => m.column === column).map((m) => `${m.name} ${format(m.value)}`).join(". ") || "No value";
4156
+ const state = stateOf(where);
4157
+ setSaid(`${where.label}. ${values}.${state ? ` ${state}.` : ""}${picked === column ? " Selected." : ""}`);
4158
+ }, [columns, marks, format, pick]);
4159
+ const step = (next) => {
4160
+ const column = Math.max(0, Math.min(next, count - 1));
4161
+ setCursor(column);
4162
+ show(lead(column)?.id);
4163
+ announce(column);
4164
+ const el = scroller.current;
4165
+ if (el && el.scrollWidth > el.clientWidth) {
4166
+ el.scrollLeft = Math.max(0, centre(column) - el.clientWidth / 2);
4167
+ syncEdges();
4168
+ }
4169
+ };
4170
+ const choose = (column) => {
4171
+ if (!selectable) return;
4172
+ if (selected === void 0) setOwnPick(column);
4173
+ onSelect?.(column);
4174
+ };
4175
+ const onKeyDown = (event) => {
4176
+ const steps2 = {
4177
+ ArrowLeft: cursor - 1,
4178
+ ArrowRight: cursor + 1,
4179
+ Home: 0,
4180
+ End: count - 1
4181
+ };
4182
+ if (event.key in steps2) {
4183
+ event.preventDefault();
4184
+ step(steps2[event.key]);
4185
+ return;
4186
+ }
4187
+ if ((event.key === "Enter" || event.key === " ") && selectable) {
4188
+ event.preventDefault();
4189
+ choose(cursor);
4190
+ announce(cursor, cursor);
4191
+ }
4192
+ };
4193
+ const estimated = columns.filter((c) => c.estimated);
4194
+ const estimatedTone = marks.find((m) => m.estimated && m.kind === "bar")?.tone ?? keys[0]?.tone ?? "neutral";
4195
+ 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) };
4196
+ return /* @__PURE__ */ jsxs33(
4197
+ "div",
4198
+ {
4199
+ className: cx8("ui-chart", `ui-chart--${variant}`, selectable && "ui-chart--pick", className),
4200
+ style: {
4201
+ "--ui-chart-h": `${plotHeight}px`,
4202
+ "--ui-chart-box": `${px(boxHeight)}px`,
4203
+ "--ui-chart-cols": count,
4204
+ "--ui-chart-pad": `${PAD}px`
4205
+ },
4206
+ children: [
4207
+ !spark && keys.length > 0 && /* @__PURE__ */ jsxs33("ul", { className: "ui-chart__legend", children: [
4208
+ keys.map((key) => /* @__PURE__ */ jsxs33("li", { className: cx8("ui-chart__key", `ui-chart__tone--${key.tone}`), children: [
4209
+ 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(
4210
+ "rect",
4211
+ {
4212
+ className: "ui-chart__key-swatch",
4213
+ x: "1",
4214
+ width: "10",
4215
+ height: "5",
4216
+ rx: "1",
4217
+ y: key.shape === "bars-below" ? 6 : 1
4218
+ }
4219
+ ) }),
4220
+ key.name
4221
+ ] }, key.id)),
4222
+ estimated.length > 0 && /* @__PURE__ */ jsxs33("li", { className: cx8("ui-chart__key", `ui-chart__tone--${estimatedTone}`), children: [
4223
+ /* @__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" }) }),
4224
+ "Estimated"
4225
+ ] })
4226
+ ] }),
4227
+ /* @__PURE__ */ jsxs33(
4228
+ "div",
4229
+ {
4230
+ ref: frameEl,
4231
+ className: "ui-chart__frame ui-tip-host ui-focusable",
4232
+ tabIndex: 0,
4233
+ role: "group",
4234
+ "aria-label": `${title}. Arrow keys step through the ${columns.length} columns.`,
4235
+ onKeyDown,
4236
+ onFocus: (event) => {
4237
+ if (event.target !== event.currentTarget) return;
4238
+ if (pointerFocus.current) {
4239
+ pointerFocus.current = false;
4240
+ return;
4241
+ }
4242
+ show(lead(cursor)?.id);
4243
+ announce(cursor);
4244
+ },
4245
+ onBlur: (event) => {
4246
+ if (event.target !== event.currentTarget) return;
4247
+ setOpenId(null);
4248
+ setSaid("");
4249
+ },
4250
+ children: [
4251
+ !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)) }),
4252
+ /* @__PURE__ */ jsx38("div", { ref: scroller, className: "ui-chart__scroll", tabIndex: -1, onScroll: () => {
4253
+ syncEdges();
4254
+ const anchor = [...svgEl.current?.querySelectorAll("[data-anchor]") ?? []].find((el) => el.getAttribute("data-anchor") === openId);
4255
+ if (!anchor || !scroller.current || !frameEl.current || !tipEl.current) return;
4256
+ const mark = anchor.getBoundingClientRect();
4257
+ const view = scroller.current.getBoundingClientRect();
4258
+ if (mark.right <= view.left || mark.left >= view.right) setOpenId(null);
4259
+ else placeTip(frameEl.current, anchor, tipEl.current, frameEl.current);
4260
+ }, children: /* @__PURE__ */ jsxs33("div", { ref: plot, className: "ui-chart__plot", children: [
4261
+ /* @__PURE__ */ jsxs33(
4262
+ "svg",
4263
+ {
4264
+ ref: svgEl,
4265
+ className: "ui-chart__svg",
4266
+ width: px(plotWidth),
4267
+ height: px(boxHeight),
4268
+ viewBox: `0 ${px(boxTop)} ${px(plotWidth)} ${px(boxHeight)}`,
4269
+ role: "img",
4270
+ "aria-label": spark && marks.length > 0 ? `${title}. ${format(marks[0].value)} to ${format(marks[marks.length - 1].value)}.` : title,
4271
+ onMouseMove: (event) => {
4272
+ if (Date.now() < ignoreMouseUntil.current) return;
4273
+ show(markAt(event.target));
4274
+ },
4275
+ onMouseLeave: () => {
4276
+ if (Date.now() < ignoreMouseUntil.current) return;
4277
+ dismissed.current = null;
4278
+ setOpenId(null);
4279
+ },
4280
+ onTouchStart: () => {
4281
+ ignoreMouseUntil.current = Date.now() + 800;
4282
+ touchMoved.current = false;
4283
+ },
4284
+ onTouchMove: () => {
4285
+ touchMoved.current = true;
4286
+ },
4287
+ onTouchCancel: () => {
4288
+ touchMoved.current = true;
4289
+ },
4290
+ onTouchEnd: (event) => {
4291
+ ignoreMouseUntil.current = Date.now() + 800;
4292
+ if (touchMoved.current) return;
4293
+ const id = markAt(event.target);
4294
+ if (id && id === openId) {
4295
+ opening.current = false;
4296
+ setOpenId(null);
4297
+ } else {
4298
+ opening.current = Boolean(id) && dismissed.current !== id;
4299
+ show(id);
4300
+ }
4301
+ },
4302
+ onPointerDown: () => {
4303
+ pointerFocus.current = true;
4304
+ },
4305
+ onClick: (event) => {
4306
+ frameEl.current?.focus();
4307
+ pointerFocus.current = false;
4308
+ const opened = opening.current;
4309
+ opening.current = false;
4310
+ const mark = byId.get(markAt(event.target) ?? "");
4311
+ if (!mark) return;
4312
+ setCursor(mark.column);
4313
+ if (opened) {
4314
+ announce(mark.column);
4315
+ return;
4316
+ }
4317
+ choose(mark.column);
4318
+ announce(mark.column, selectable ? mark.column : pick);
4319
+ },
4320
+ children: [
4321
+ !spark && pick !== null && pick >= 0 && pick < count && /* @__PURE__ */ jsx38(
4322
+ "rect",
4323
+ {
4324
+ className: "ui-chart__band",
4325
+ x: px(pick * colWidth),
4326
+ width: px(colWidth),
4327
+ y: "0",
4328
+ height: px(plotHeight)
4329
+ }
4330
+ ),
4331
+ frame.links && frame.bars.slice(0, -1).map((bar, i) => {
4332
+ const from = byId.get(bar.id);
4333
+ const to = byId.get(frame.bars[i + 1].id);
4334
+ return from && to ? /* @__PURE__ */ jsx38(
4335
+ "line",
4336
+ {
4337
+ className: "ui-chart__carry",
4338
+ x1: px(from.x + from.w),
4339
+ x2: px(to.x),
4340
+ y1: px(y(bar.to)),
4341
+ y2: px(y(bar.to))
4342
+ },
4343
+ `carry-${bar.id}`
4344
+ ) : null;
4345
+ }),
4346
+ !spark && scale.ticks.filter((tick) => tick !== 0).map((tick) => /* @__PURE__ */ jsx38(
4347
+ "line",
4348
+ {
4349
+ className: "ui-chart__grid",
4350
+ x1: "0",
4351
+ x2: px(plotWidth),
4352
+ y1: px(y(tick)),
4353
+ y2: px(y(tick))
4354
+ },
4355
+ `grid-${tick}`
4356
+ )),
4357
+ !spark && scale.ticks.includes(0) && /* @__PURE__ */ jsx38(
4358
+ "line",
4359
+ {
4360
+ className: "ui-chart__zero",
4361
+ x1: "0",
4362
+ x2: px(plotWidth),
4363
+ y1: px(y(0)),
4364
+ y2: px(y(0))
4365
+ }
4366
+ ),
4367
+ marks.filter((m) => m.kind === "bar").map((mark) => /* @__PURE__ */ jsx38(
4368
+ "path",
4369
+ {
4370
+ className: cx8(
4371
+ "ui-chart__bar",
4372
+ `ui-chart__tone--${mark.tone}`,
4373
+ mark.estimated && "is-estimated"
4374
+ ),
4375
+ d: barPath(mark)
4376
+ },
4377
+ `bar-${mark.id}`
4378
+ )),
4379
+ ["casing", "stroke"].map((layer) => lines.flatMap(
4380
+ (line) => line.values.slice(1).map((value, i) => Number.isFinite(value) && Number.isFinite(line.values[i]) ? /* @__PURE__ */ jsx38(
4381
+ "line",
4382
+ {
4383
+ className: layer === "casing" ? "ui-chart__line-casing" : cx8(
4384
+ "ui-chart__line",
4385
+ `ui-chart__tone--${line.tone}`,
4386
+ (columns[i]?.estimated || columns[i + 1]?.estimated) && "is-estimated"
4387
+ ),
4388
+ x1: px(centre(i)),
4389
+ y1: px(y(line.values[i])),
4390
+ x2: px(centre(i + 1)),
4391
+ y2: px(y(value))
4392
+ },
4393
+ `${line.id}-${layer}-${i}`
4394
+ ) : null)
4395
+ )),
4396
+ 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(
4397
+ "circle",
4398
+ {
4399
+ className: cx8("ui-chart__isolated", `ui-chart__tone--${line.tone}`),
4400
+ cx: px(centre(i)),
4401
+ cy: px(y(value)),
4402
+ r: "2"
4403
+ },
4404
+ `isolated-${line.id}-${i}`
4405
+ ) : null)),
4406
+ marks.map((mark) => /* @__PURE__ */ jsxs33("g", { "data-mark": mark.id, children: [
4407
+ mark.kind === "bar" ? /* @__PURE__ */ jsx38(
4408
+ "rect",
4409
+ {
4410
+ className: "ui-chart__hit",
4411
+ x: px(mark.column * colWidth + mark.lane * colWidth / mark.lanes),
4412
+ width: px(colWidth / mark.lanes),
4413
+ y: px(hitBand(mark.band).y),
4414
+ height: px(hitBand(mark.band).h)
4415
+ }
4416
+ ) : /* @__PURE__ */ jsx38(
4417
+ "rect",
4418
+ {
4419
+ className: "ui-chart__hit",
4420
+ x: px(mark.hit?.x ?? mark.x),
4421
+ y: px(mark.hit?.y ?? mark.y),
4422
+ width: px(mark.hit?.w ?? TARGET),
4423
+ height: px(mark.hit?.h ?? TARGET)
4424
+ }
4425
+ ),
4426
+ /* @__PURE__ */ jsx38(
4427
+ "rect",
4428
+ {
4429
+ "data-anchor": mark.id,
4430
+ className: "ui-chart__anchor",
4431
+ x: px(mark.x),
4432
+ y: px(mark.y),
4433
+ width: px(mark.w),
4434
+ height: "0"
4435
+ }
4436
+ )
4437
+ ] }, `hit-${mark.id}`))
4438
+ ]
4439
+ }
4440
+ ),
4441
+ !spark && /* @__PURE__ */ jsx38("div", { className: "ui-chart__periods", "aria-hidden": "true", children: columns.map((column, i) => /* @__PURE__ */ jsx38(
4442
+ "span",
4443
+ {
4444
+ className: cx8("ui-chart__period", pick === i && "is-selected"),
4445
+ children: column.short
4446
+ },
4447
+ `${column.label}-${i}`
4448
+ )) })
4449
+ ] }) }),
4450
+ /* @__PURE__ */ jsx38("span", { ref: tipEl, className: cx8("ui-tip", openId && "is-open"), role: open ? "tooltip" : void 0, children: open && /* @__PURE__ */ jsxs33(Fragment11, { children: [
4451
+ /* @__PURE__ */ jsx38("span", { className: "ui-tip__label", children: spark ? columns[open.column]?.label : `${open.name}, ${columns[open.column]?.label}` }),
4452
+ /* @__PURE__ */ jsx38("span", { className: "ui-tip__value", children: format(open.value) }),
4453
+ (stateOf(columns[open.column]) || open.detail) && /* @__PURE__ */ jsx38("span", { className: "ui-tip__detail", children: stateOf(columns[open.column]) || open.detail })
4454
+ ] }) })
4455
+ ]
4456
+ }
4457
+ ),
4458
+ /* @__PURE__ */ jsx38("div", { className: "ui-sr", role: "status", children: said }),
4459
+ showTable && /* @__PURE__ */ jsxs33("details", { className: "ui-chart__data", children: [
4460
+ /* @__PURE__ */ jsx38("summary", { className: "ui-focusable", children: "Values as a table" }),
4461
+ /* @__PURE__ */ jsx38(
4462
+ "div",
4463
+ {
4464
+ className: "ui-table-scroll",
4465
+ role: "region",
4466
+ tabIndex: 0,
4467
+ "aria-label": `${title}, as a table. Scroll for more columns.`,
4468
+ children: /* @__PURE__ */ jsxs33("table", { className: "ui-table", "aria-label": title, children: [
4469
+ /* @__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)) }) }),
4470
+ /* @__PURE__ */ jsx38("tbody", { children: tableRows.map((row, i) => /* @__PURE__ */ jsxs33("tr", { "aria-current": pick === i ? true : void 0, children: [
4471
+ /* @__PURE__ */ jsxs33("th", { scope: "row", children: [
4472
+ row.column.label,
4473
+ row.column.estimated && /* @__PURE__ */ jsxs33(Fragment11, { children: [
4474
+ " ",
4475
+ /* @__PURE__ */ jsx38("span", { className: "ui-chart__state", children: stateOf(row.column) })
4476
+ ] })
4477
+ ] }),
4478
+ row.cells.map((cell, j) => /* @__PURE__ */ jsx38("td", { className: "ui-table__num", children: cell }, tableHead[j + 1]))
4479
+ ] }, `${row.column.label}-${i}`)) })
4480
+ ] })
4481
+ }
4482
+ )
4483
+ ] })
4484
+ ]
4485
+ }
4486
+ );
4487
+ }
4488
+
3799
4489
  // src/AccentPicker.tsx
3800
4490
  import { forwardRef as forwardRef10 } from "react";
3801
4491
  import { ACCENTS, accentSwatchStyle } from "@apliteni/apliteni-ui";
3802
- import { jsx as jsx38 } from "react/jsx-runtime";
4492
+ import { jsx as jsx39 } from "react/jsx-runtime";
3803
4493
  var AccentPicker = forwardRef10(function AccentPicker2({
3804
4494
  value,
3805
4495
  onChange,
@@ -3808,7 +4498,7 @@ var AccentPicker = forwardRef10(function AccentPicker2({
3808
4498
  "aria-label": label = "Accent",
3809
4499
  ...rest
3810
4500
  }, ref) {
3811
- return /* @__PURE__ */ jsx38(
4501
+ return /* @__PURE__ */ jsx39(
3812
4502
  "div",
3813
4503
  {
3814
4504
  ...rest,
@@ -3819,7 +4509,7 @@ var AccentPicker = forwardRef10(function AccentPicker2({
3819
4509
  "aria-label": label,
3820
4510
  children: options.map((accent) => {
3821
4511
  const title = accent.charAt(0).toUpperCase() + accent.slice(1);
3822
- return /* @__PURE__ */ jsx38(
4512
+ return /* @__PURE__ */ jsx39(
3823
4513
  "button",
3824
4514
  {
3825
4515
  type: "button",
@@ -3849,6 +4539,7 @@ export {
3849
4539
  Button,
3850
4540
  Callout,
3851
4541
  Card,
4542
+ Chart,
3852
4543
  Checkbox,
3853
4544
  CommandPalette,
3854
4545
  Confirm,