@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.
- package/guidelines/labels-and-titles.md +18 -6
- package/package.json +1 -1
- package/react/README.md +69 -1
- package/react/dist/index.d.ts +92 -2
- package/react/dist/index.js +789 -49
- package/src/components/dropdown.js +53 -19
- package/src/components/filter-bar.js +6 -3
- package/src/index.css +1 -0
- package/src/index.d.ts +30 -6
- package/src/index.js +1 -0
- package/src/inline.js +2 -0
- package/src/logic/chart.js +143 -0
- package/src/logic/filter-bar.js +34 -16
- package/src/styles/chart.css +282 -0
- package/src/styles/dropdown.css +2 -2
- package/src/styles/filter-bar.css +55 -16
- package/src/styles/topbar.css +1 -1
package/react/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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__ */
|
|
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__ */
|
|
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,
|