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 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;