hazo_ui 6.18.0 → 6.19.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/CHANGE_LOG.md +16 -0
- package/dist/index.cjs +342 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +341 -1
- package/dist/index.js.map +1 -1
- package/dist/index.utils.cjs +75 -0
- package/dist/index.utils.cjs.map +1 -1
- package/dist/index.utils.d.cts +1 -1
- package/dist/index.utils.d.ts +1 -1
- package/dist/index.utils.js +75 -1
- package/dist/index.utils.js.map +1 -1
- package/dist/{announce-DAnQDTld.d.cts → trend_stats-BAatnIt-.d.cts} +45 -1
- package/dist/{announce-DAnQDTld.d.ts → trend_stats-BAatnIt-.d.ts} +45 -1
- package/package.json +2 -2
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v6.19.0 (2026-10-23)
|
|
9
|
+
|
|
10
|
+
### Added — TrendSummary + computeTrendStats
|
|
11
|
+
|
|
12
|
+
- **`TrendSummary`** — reusable trend widget: rolling-window stat cards (All
|
|
13
|
+
time, 7d, 30d, 3mo, 6mo, YTD) with % change deltas vs the prior equal period,
|
|
14
|
+
plus an interactive YTD monthly bar chart with axes, gridlines, and hover
|
|
15
|
+
tooltips. Pure inline SVG (no chart library). Data-driven via props — pair with
|
|
16
|
+
`computeTrendStats` to produce the data from any timestamped record list.
|
|
17
|
+
Configurable labels (`chartTitle`, `chartTooltipSuffix`, `totalLabel`,
|
|
18
|
+
`prevTemplate`).
|
|
19
|
+
- **`computeTrendStats(items, now?, options?)`** — pure, dependency-free helper
|
|
20
|
+
that computes rolling-window counts + % change + YTD monthly series from a
|
|
21
|
+
list of `{ created_at }` records. Also available from `hazo_ui/utils` for
|
|
22
|
+
Server Component usage.
|
|
23
|
+
|
|
8
24
|
## v6.18.0 (2026-10-19)
|
|
9
25
|
|
|
10
26
|
### Changed — widened `hazo_i18n` peer range
|
package/dist/index.cjs
CHANGED
|
@@ -13728,6 +13728,346 @@ function EditableDataTable({
|
|
|
13728
13728
|
)
|
|
13729
13729
|
);
|
|
13730
13730
|
}
|
|
13731
|
+
function TrendDeltaBadge({ pct }) {
|
|
13732
|
+
if (pct === null) {
|
|
13733
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" });
|
|
13734
|
+
}
|
|
13735
|
+
const up = pct >= 0;
|
|
13736
|
+
const cls = up ? "text-emerald-600" : "text-red-600";
|
|
13737
|
+
const arrow = up ? "\u25B2" : "\u25BC";
|
|
13738
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `text-xs font-medium ${cls}`, title: "vs previous period", children: [
|
|
13739
|
+
arrow,
|
|
13740
|
+
" ",
|
|
13741
|
+
up ? "+" : "",
|
|
13742
|
+
pct,
|
|
13743
|
+
"%"
|
|
13744
|
+
] });
|
|
13745
|
+
}
|
|
13746
|
+
var CARD_HEIGHT_PX = 168;
|
|
13747
|
+
function TrendStatCard({
|
|
13748
|
+
w,
|
|
13749
|
+
totalLabel,
|
|
13750
|
+
prevTemplate
|
|
13751
|
+
}) {
|
|
13752
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13753
|
+
"div",
|
|
13754
|
+
{
|
|
13755
|
+
className: "cls_trend_stat_card flex min-w-0 flex-col justify-between rounded-lg border bg-card p-4",
|
|
13756
|
+
style: { height: CARD_HEIGHT_PX },
|
|
13757
|
+
children: [
|
|
13758
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-muted-foreground", children: w.label }),
|
|
13759
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline justify-between gap-1", children: [
|
|
13760
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-3xl font-bold tabular-nums", children: w.count.toLocaleString() }),
|
|
13761
|
+
w.key !== "all" && /* @__PURE__ */ jsxRuntime.jsx(TrendDeltaBadge, { pct: w.pctChange })
|
|
13762
|
+
] }),
|
|
13763
|
+
w.key !== "all" && w.prevCount !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-muted-foreground", children: prevTemplate.replace("{count}", w.prevCount.toLocaleString()) }),
|
|
13764
|
+
w.key === "all" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-muted-foreground", children: totalLabel })
|
|
13765
|
+
]
|
|
13766
|
+
}
|
|
13767
|
+
);
|
|
13768
|
+
}
|
|
13769
|
+
function niceMax(n) {
|
|
13770
|
+
if (n <= 0) return 1;
|
|
13771
|
+
const exp = Math.floor(Math.log10(n));
|
|
13772
|
+
const base = Math.pow(10, exp);
|
|
13773
|
+
const frac = n / base;
|
|
13774
|
+
let niceFrac;
|
|
13775
|
+
if (frac <= 1) niceFrac = 1;
|
|
13776
|
+
else if (frac <= 2) niceFrac = 2;
|
|
13777
|
+
else if (frac <= 5) niceFrac = 5;
|
|
13778
|
+
else niceFrac = 10;
|
|
13779
|
+
return niceFrac * base;
|
|
13780
|
+
}
|
|
13781
|
+
function TrendMiniChart({
|
|
13782
|
+
data,
|
|
13783
|
+
title,
|
|
13784
|
+
tooltipSuffix
|
|
13785
|
+
}) {
|
|
13786
|
+
const [hoverIndex, setHoverIndex] = React27.useState(null);
|
|
13787
|
+
const total = data.reduce((n2, d) => n2 + d.count, 0);
|
|
13788
|
+
const rawMax = Math.max(1, ...data.map((d) => d.count));
|
|
13789
|
+
const max = niceMax(rawMax);
|
|
13790
|
+
const mid = Math.round(max / 2);
|
|
13791
|
+
const W = 480;
|
|
13792
|
+
const H = 120;
|
|
13793
|
+
const marginLeft = 34;
|
|
13794
|
+
const marginRight = 8;
|
|
13795
|
+
const marginTop = 16;
|
|
13796
|
+
const marginBottom = 20;
|
|
13797
|
+
const plotW = W - marginLeft - marginRight;
|
|
13798
|
+
const plotH = H - marginTop - marginBottom;
|
|
13799
|
+
const gap = 6;
|
|
13800
|
+
const n = Math.max(1, data.length);
|
|
13801
|
+
const colW = plotW / n;
|
|
13802
|
+
const bw = Math.max(1, colW - gap);
|
|
13803
|
+
const yFor = (value) => marginTop + plotH - value / max * plotH;
|
|
13804
|
+
const hovered = hoverIndex !== null ? data[hoverIndex] : null;
|
|
13805
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13806
|
+
"div",
|
|
13807
|
+
{
|
|
13808
|
+
className: "cls_trend_mini_chart col-span-6 flex min-w-0 flex-col rounded-lg border bg-card p-3",
|
|
13809
|
+
style: { height: CARD_HEIGHT_PX },
|
|
13810
|
+
children: [
|
|
13811
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-baseline justify-between", children: [
|
|
13812
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[11px] text-muted-foreground", children: title }),
|
|
13813
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold tabular-nums", children: total.toLocaleString() })
|
|
13814
|
+
] }),
|
|
13815
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative mt-2 min-h-0 flex-1", children: [
|
|
13816
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
13817
|
+
"svg",
|
|
13818
|
+
{
|
|
13819
|
+
role: "img",
|
|
13820
|
+
"aria-label": data.map((d) => `${d.label} ${d.count}`).join(", "),
|
|
13821
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
13822
|
+
preserveAspectRatio: "none",
|
|
13823
|
+
className: "h-full w-full",
|
|
13824
|
+
children: [
|
|
13825
|
+
[0, mid, max].map((tick) => {
|
|
13826
|
+
const y = yFor(tick);
|
|
13827
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
13828
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13829
|
+
"line",
|
|
13830
|
+
{
|
|
13831
|
+
x1: marginLeft,
|
|
13832
|
+
x2: W - marginRight,
|
|
13833
|
+
y1: y,
|
|
13834
|
+
y2: y,
|
|
13835
|
+
stroke: "currentColor",
|
|
13836
|
+
strokeOpacity: 0.1
|
|
13837
|
+
}
|
|
13838
|
+
),
|
|
13839
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13840
|
+
"text",
|
|
13841
|
+
{
|
|
13842
|
+
x: marginLeft - 6,
|
|
13843
|
+
y,
|
|
13844
|
+
dy: 3,
|
|
13845
|
+
textAnchor: "end",
|
|
13846
|
+
fontSize: 9,
|
|
13847
|
+
className: "fill-muted-foreground",
|
|
13848
|
+
children: tick.toLocaleString()
|
|
13849
|
+
}
|
|
13850
|
+
)
|
|
13851
|
+
] }, tick);
|
|
13852
|
+
}),
|
|
13853
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13854
|
+
"line",
|
|
13855
|
+
{
|
|
13856
|
+
x1: marginLeft,
|
|
13857
|
+
x2: W - marginRight,
|
|
13858
|
+
y1: marginTop + plotH,
|
|
13859
|
+
y2: marginTop + plotH,
|
|
13860
|
+
stroke: "currentColor",
|
|
13861
|
+
strokeOpacity: 0.2
|
|
13862
|
+
}
|
|
13863
|
+
),
|
|
13864
|
+
data.map((d, i) => {
|
|
13865
|
+
const barH = Math.round(d.count / max * plotH);
|
|
13866
|
+
const colX = marginLeft + i * colW;
|
|
13867
|
+
const barX = colX + (colW - bw) / 2;
|
|
13868
|
+
const barY = marginTop + plotH - barH;
|
|
13869
|
+
const centerX = colX + colW / 2;
|
|
13870
|
+
const isDimmed = hoverIndex !== null && hoverIndex !== i;
|
|
13871
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13872
|
+
"g",
|
|
13873
|
+
{
|
|
13874
|
+
onMouseEnter: () => setHoverIndex(i),
|
|
13875
|
+
onMouseLeave: () => setHoverIndex(null),
|
|
13876
|
+
children: [
|
|
13877
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13878
|
+
"rect",
|
|
13879
|
+
{
|
|
13880
|
+
x: colX,
|
|
13881
|
+
y: marginTop,
|
|
13882
|
+
width: colW,
|
|
13883
|
+
height: plotH,
|
|
13884
|
+
fill: "transparent"
|
|
13885
|
+
}
|
|
13886
|
+
),
|
|
13887
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13888
|
+
"rect",
|
|
13889
|
+
{
|
|
13890
|
+
x: barX,
|
|
13891
|
+
y: barY,
|
|
13892
|
+
width: bw,
|
|
13893
|
+
height: Math.max(0, barH),
|
|
13894
|
+
rx: 2,
|
|
13895
|
+
className: "fill-primary",
|
|
13896
|
+
fillOpacity: isDimmed ? 0.45 : 1,
|
|
13897
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("title", { children: `${d.label}: ${d.count} ${tooltipSuffix}` })
|
|
13898
|
+
}
|
|
13899
|
+
),
|
|
13900
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13901
|
+
"text",
|
|
13902
|
+
{
|
|
13903
|
+
x: centerX,
|
|
13904
|
+
y: barY - 4,
|
|
13905
|
+
textAnchor: "middle",
|
|
13906
|
+
fontSize: 10,
|
|
13907
|
+
fontWeight: 600,
|
|
13908
|
+
className: "fill-foreground",
|
|
13909
|
+
children: d.count
|
|
13910
|
+
}
|
|
13911
|
+
),
|
|
13912
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13913
|
+
"text",
|
|
13914
|
+
{
|
|
13915
|
+
x: centerX,
|
|
13916
|
+
y: marginTop + plotH + 14,
|
|
13917
|
+
textAnchor: "middle",
|
|
13918
|
+
fontSize: 9,
|
|
13919
|
+
className: "fill-muted-foreground",
|
|
13920
|
+
children: d.label
|
|
13921
|
+
}
|
|
13922
|
+
)
|
|
13923
|
+
]
|
|
13924
|
+
},
|
|
13925
|
+
d.label
|
|
13926
|
+
);
|
|
13927
|
+
})
|
|
13928
|
+
]
|
|
13929
|
+
}
|
|
13930
|
+
),
|
|
13931
|
+
hovered && hoverIndex !== null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13932
|
+
"div",
|
|
13933
|
+
{
|
|
13934
|
+
className: "pointer-events-none absolute z-10 -translate-x-1/2 -translate-y-full rounded-md border bg-popover px-2 py-1 text-[10px] font-medium text-popover-foreground shadow-sm",
|
|
13935
|
+
style: {
|
|
13936
|
+
left: `${Math.min(
|
|
13937
|
+
92,
|
|
13938
|
+
Math.max(
|
|
13939
|
+
8,
|
|
13940
|
+
(marginLeft + hoverIndex * (plotW / n) + plotW / n / 2) / W * 100
|
|
13941
|
+
)
|
|
13942
|
+
)}%`,
|
|
13943
|
+
top: `${(marginTop + plotH - hovered.count / max * plotH) / H * 100}%`
|
|
13944
|
+
},
|
|
13945
|
+
children: [
|
|
13946
|
+
hovered.label,
|
|
13947
|
+
": ",
|
|
13948
|
+
hovered.count,
|
|
13949
|
+
" ",
|
|
13950
|
+
tooltipSuffix
|
|
13951
|
+
]
|
|
13952
|
+
}
|
|
13953
|
+
)
|
|
13954
|
+
] })
|
|
13955
|
+
]
|
|
13956
|
+
}
|
|
13957
|
+
);
|
|
13958
|
+
}
|
|
13959
|
+
function TrendSummary({
|
|
13960
|
+
windows,
|
|
13961
|
+
chartData,
|
|
13962
|
+
chartTitle = "Activity (YTD)",
|
|
13963
|
+
chartTooltipSuffix = "",
|
|
13964
|
+
totalLabel = "total",
|
|
13965
|
+
prevTemplate = "prev {count}",
|
|
13966
|
+
className
|
|
13967
|
+
}) {
|
|
13968
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13969
|
+
"div",
|
|
13970
|
+
{
|
|
13971
|
+
className: cn(
|
|
13972
|
+
"cls_trend_summary mb-4 grid grid-cols-6 items-start gap-2 md:grid-cols-12",
|
|
13973
|
+
className
|
|
13974
|
+
),
|
|
13975
|
+
children: [
|
|
13976
|
+
windows.map((w) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
13977
|
+
TrendStatCard,
|
|
13978
|
+
{
|
|
13979
|
+
w,
|
|
13980
|
+
totalLabel,
|
|
13981
|
+
prevTemplate
|
|
13982
|
+
},
|
|
13983
|
+
w.key
|
|
13984
|
+
)),
|
|
13985
|
+
chartData && chartData.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
13986
|
+
TrendMiniChart,
|
|
13987
|
+
{
|
|
13988
|
+
data: chartData,
|
|
13989
|
+
title: chartTitle,
|
|
13990
|
+
tooltipSuffix: chartTooltipSuffix
|
|
13991
|
+
}
|
|
13992
|
+
)
|
|
13993
|
+
]
|
|
13994
|
+
}
|
|
13995
|
+
);
|
|
13996
|
+
}
|
|
13997
|
+
|
|
13998
|
+
// src/lib/trend_stats.ts
|
|
13999
|
+
var DAY_MS = 864e5;
|
|
14000
|
+
var MONTHS = [
|
|
14001
|
+
"Jan",
|
|
14002
|
+
"Feb",
|
|
14003
|
+
"Mar",
|
|
14004
|
+
"Apr",
|
|
14005
|
+
"May",
|
|
14006
|
+
"Jun",
|
|
14007
|
+
"Jul",
|
|
14008
|
+
"Aug",
|
|
14009
|
+
"Sep",
|
|
14010
|
+
"Oct",
|
|
14011
|
+
"Nov",
|
|
14012
|
+
"Dec"
|
|
14013
|
+
];
|
|
14014
|
+
function pctOf(count, prev) {
|
|
14015
|
+
if (prev === null || prev === 0) return null;
|
|
14016
|
+
return Math.round((count - prev) / prev * 100);
|
|
14017
|
+
}
|
|
14018
|
+
var DEFAULT_WINDOWS = [
|
|
14019
|
+
{ key: "7d", label: "7 days", days: 7 },
|
|
14020
|
+
{ key: "30d", label: "30 days", days: 30 },
|
|
14021
|
+
{ key: "90d", label: "3 months", days: 90 },
|
|
14022
|
+
{ key: "180d", label: "6 months", days: 180 }
|
|
14023
|
+
];
|
|
14024
|
+
function computeTrendStats(items, now = /* @__PURE__ */ new Date(), options) {
|
|
14025
|
+
const nowMs = now.getTime();
|
|
14026
|
+
const times = [];
|
|
14027
|
+
for (const item of items) {
|
|
14028
|
+
if (!item || !item.created_at) continue;
|
|
14029
|
+
const t = new Date(item.created_at).getTime();
|
|
14030
|
+
if (!Number.isNaN(t)) times.push(t);
|
|
14031
|
+
}
|
|
14032
|
+
const countBetween = (startMs, endMs) => times.reduce((n, t) => t >= startMs && t < endMs ? n + 1 : n, 0);
|
|
14033
|
+
const total = times.length;
|
|
14034
|
+
const rollingDefs = options?.windows ?? DEFAULT_WINDOWS;
|
|
14035
|
+
const windows = [
|
|
14036
|
+
{ key: "all", label: options?.totalLabel ?? "All time", count: total, prevCount: null, pctChange: null }
|
|
14037
|
+
];
|
|
14038
|
+
for (const w of rollingDefs) {
|
|
14039
|
+
const span = w.days * DAY_MS;
|
|
14040
|
+
const count = countBetween(nowMs - span, nowMs);
|
|
14041
|
+
const prevCount = countBetween(nowMs - 2 * span, nowMs - span);
|
|
14042
|
+
windows.push({
|
|
14043
|
+
key: w.key,
|
|
14044
|
+
label: w.label,
|
|
14045
|
+
count,
|
|
14046
|
+
prevCount,
|
|
14047
|
+
pctChange: pctOf(count, prevCount)
|
|
14048
|
+
});
|
|
14049
|
+
}
|
|
14050
|
+
const ytdStart = new Date(now.getFullYear(), 0, 1).getTime();
|
|
14051
|
+
const elapsed = nowMs - ytdStart;
|
|
14052
|
+
const ytdCount = countBetween(ytdStart, nowMs);
|
|
14053
|
+
const prevYtdStart = new Date(now.getFullYear() - 1, 0, 1).getTime();
|
|
14054
|
+
const prevYtdCount = countBetween(prevYtdStart, prevYtdStart + elapsed);
|
|
14055
|
+
windows.push({
|
|
14056
|
+
key: "ytd",
|
|
14057
|
+
label: "YTD",
|
|
14058
|
+
count: ytdCount,
|
|
14059
|
+
prevCount: prevYtdCount,
|
|
14060
|
+
pctChange: pctOf(ytdCount, prevYtdCount)
|
|
14061
|
+
});
|
|
14062
|
+
const ytdMonthly = [];
|
|
14063
|
+
const year = now.getFullYear();
|
|
14064
|
+
for (let m = 0; m <= now.getMonth(); m++) {
|
|
14065
|
+
const monthStart = new Date(year, m, 1).getTime();
|
|
14066
|
+
const monthEnd = new Date(year, m + 1, 1).getTime();
|
|
14067
|
+
ytdMonthly.push({ label: MONTHS[m], count: countBetween(monthStart, monthEnd) });
|
|
14068
|
+
}
|
|
14069
|
+
return { total, windows, ytdMonthly };
|
|
14070
|
+
}
|
|
13731
14071
|
|
|
13732
14072
|
Object.defineProperty(exports, "rawToast", {
|
|
13733
14073
|
enumerable: true,
|
|
@@ -13930,12 +14270,14 @@ exports.Tooltip = Tooltip;
|
|
|
13930
14270
|
exports.TooltipContent = TooltipContent;
|
|
13931
14271
|
exports.TooltipProvider = TooltipProvider;
|
|
13932
14272
|
exports.TooltipTrigger = TooltipTrigger;
|
|
14273
|
+
exports.TrendSummary = TrendSummary;
|
|
13933
14274
|
exports.announce = announce;
|
|
13934
14275
|
exports.applyKanbanFilter = applyKanbanFilter;
|
|
13935
14276
|
exports.buttonGroupVariants = buttonGroupVariants;
|
|
13936
14277
|
exports.celebrate = celebrate;
|
|
13937
14278
|
exports.cn = cn;
|
|
13938
14279
|
exports.computeEta = computeEta;
|
|
14280
|
+
exports.computeTrendStats = computeTrendStats;
|
|
13939
14281
|
exports.create_command_suggestion_extension = create_command_suggestion_extension;
|
|
13940
14282
|
exports.defaultHazoUiT = defaultHazoUiT;
|
|
13941
14283
|
exports.easeToward = easeToward;
|