@actioneer/ads 0.5.2 → 0.5.4

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.
@@ -0,0 +1,62 @@
1
+ import * as React from "react";
2
+ export interface VizSeries {
3
+ name: string;
4
+ /** Omit to take the palette color for its index. */
5
+ color?: string;
6
+ values: number[];
7
+ /** Render dashed — a 2nd+ event line, not the primary. */
8
+ dashed?: boolean;
9
+ /** Faded dashed ghost — a "previous period" compare series. */
10
+ ghost?: boolean;
11
+ }
12
+ export interface SeriesChartProps {
13
+ type?: "line" | "area" | "bar";
14
+ labels: string[];
15
+ series: VizSeries[];
16
+ height?: number;
17
+ /** Formatter for the value axis + tooltip; defaults to abbr / commafy. */
18
+ formatAxis?: (v: number) => string;
19
+ formatValue?: (v: number) => string;
20
+ className?: string;
21
+ }
22
+ export declare function SeriesChart({ type, labels, series, height, formatAxis, formatValue, className, }: SeriesChartProps): React.JSX.Element;
23
+ export interface FunnelStep {
24
+ label: string;
25
+ value: number;
26
+ /** Median seconds from the previous step. */
27
+ medianTime?: number | null;
28
+ avgTime?: number | null;
29
+ }
30
+ export interface FunnelVizProps {
31
+ steps: FunnelStep[];
32
+ height?: number;
33
+ className?: string;
34
+ }
35
+ export declare function FunnelViz({ steps, height, className }: FunnelVizProps): React.JSX.Element;
36
+ export interface FunnelBarsHorizontalProps {
37
+ steps: FunnelStep[];
38
+ className?: string;
39
+ }
40
+ export declare function FunnelBarsHorizontal({ steps, className }: FunnelBarsHorizontalProps): React.JSX.Element;
41
+ export interface RetentionPoint {
42
+ label: string;
43
+ v: number;
44
+ }
45
+ export interface RetentionCurveProps {
46
+ points: RetentionPoint[];
47
+ height?: number;
48
+ className?: string;
49
+ }
50
+ export declare function RetentionCurve({ points, height, className }: RetentionCurveProps): React.JSX.Element;
51
+ export interface RetentionCohort {
52
+ label: string;
53
+ size: number;
54
+ /** One per column; null = period not yet elapsed. */
55
+ values: (number | null)[];
56
+ }
57
+ export interface RetentionGridProps {
58
+ cols: string[];
59
+ cohorts: RetentionCohort[];
60
+ className?: string;
61
+ }
62
+ export declare function RetentionGrid({ cols, cohorts, className }: RetentionGridProps): React.JSX.Element;
@@ -0,0 +1,408 @@
1
+ "use client";
2
+ import { jsx as r, jsxs as c, Fragment as J } from "react/jsx-runtime";
3
+ import * as $ from "react";
4
+ import { cn as D } from "../lib/cn.js";
5
+ import { fmtDuration as B, abbr as V, pct1 as z, commafy as H } from "../lib/viz-format.js";
6
+ const G = [
7
+ "var(--color-chart-1)",
8
+ "var(--color-chart-2)",
9
+ "var(--color-chart-3)",
10
+ "var(--color-chart-4)",
11
+ "var(--color-chart-5)",
12
+ "var(--color-chart-6)"
13
+ ], O = (e) => G[e % G.length], A = "var(--color-chart-1)", E = "var(--color-ink-400)", U = "var(--border)", Q = "var(--foreground)";
14
+ function Y(e, s = 4) {
15
+ if (e <= 0) return { max: 1, ticks: [0, 1] };
16
+ const d = e / s, i = Math.pow(10, Math.floor(Math.log10(d))), o = ([1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].find((w) => w * i >= d) ?? 10) * i, a = Math.ceil(e / o) * o, y = [];
17
+ for (let w = 0; w <= a + o / 1e6; w += o) y.push(w);
18
+ return { max: a, ticks: y };
19
+ }
20
+ function Z(e, s = 8) {
21
+ if (e.length <= s) return e;
22
+ const d = Math.ceil(e.length / s);
23
+ return e.map((i, u) => u % d === 0 || u === e.length - 1 ? i : null);
24
+ }
25
+ function P(e) {
26
+ if (!e.length) return "";
27
+ if (e.length === 1) return `M${e[0][0]},${e[0][1]}`;
28
+ let s = `M${e[0][0]},${e[0][1]}`;
29
+ for (let d = 1; d < e.length; d++) {
30
+ const [i, u] = e[d - 1], [o, a] = e[d], y = (i + o) / 2;
31
+ s += `C${y},${u} ${y},${a} ${o},${a}`;
32
+ }
33
+ return s;
34
+ }
35
+ function _({
36
+ x: e,
37
+ y: s,
38
+ title: d,
39
+ rows: i,
40
+ flip: u
41
+ }) {
42
+ return /* @__PURE__ */ c(
43
+ "div",
44
+ {
45
+ className: "pointer-events-none absolute z-10 min-w-[184px] max-w-[248px] rounded-lg bg-foreground px-3 py-2.5 text-background shadow-elevation",
46
+ style: {
47
+ left: e,
48
+ top: s,
49
+ transform: `translate(${u ? "calc(-100% - 12px)" : "12px"}, -50%)`
50
+ },
51
+ children: [
52
+ /* @__PURE__ */ r("div", { className: "mb-1.5 text-xs text-background/60", children: d }),
53
+ /* @__PURE__ */ r("div", { className: "flex flex-col gap-1", children: i.map((o, a) => /* @__PURE__ */ c("div", { className: "flex items-center gap-2 text-xs", children: [
54
+ o.dashed ? /* @__PURE__ */ r(
55
+ "span",
56
+ {
57
+ "aria-hidden": !0,
58
+ className: "h-0 w-3 shrink-0 border-t border-dashed",
59
+ style: { borderColor: o.swatch ?? "currentColor" }
60
+ }
61
+ ) : o.swatch ? /* @__PURE__ */ r("span", { "aria-hidden": !0, className: "size-2 shrink-0 rounded-dot", style: { background: o.swatch } }) : null,
62
+ /* @__PURE__ */ r("span", { className: "min-w-0 flex-1 truncate text-background/70", children: o.label }),
63
+ /* @__PURE__ */ r("span", { className: "shrink-0 font-semibold tabular-nums", children: o.value })
64
+ ] }, a)) })
65
+ ]
66
+ }
67
+ );
68
+ }
69
+ function ee({
70
+ items: e
71
+ }) {
72
+ return /* @__PURE__ */ r("div", { className: "mb-3 flex flex-wrap items-center justify-end gap-x-4 gap-y-1", children: e.map((s) => /* @__PURE__ */ c("span", { className: "inline-flex items-center gap-1.5 text-xs text-secondary", children: [
73
+ /* @__PURE__ */ r(
74
+ "span",
75
+ {
76
+ "aria-hidden": !0,
77
+ className: D("h-0 w-4 shrink-0", s.dashed ? "border-t-2 border-dashed" : "border-t-[3px]"),
78
+ style: { borderColor: s.color }
79
+ }
80
+ ),
81
+ s.label
82
+ ] }, s.label)) });
83
+ }
84
+ const b = { l: 56, r: 18, t: 18, b: 34 };
85
+ function ne({
86
+ type: e = "line",
87
+ labels: s,
88
+ series: d,
89
+ height: i = 300,
90
+ formatAxis: u = V,
91
+ formatValue: o = H,
92
+ className: a
93
+ }) {
94
+ const [y, w] = $.useState(720), N = $.useRef(null);
95
+ $.useEffect(() => {
96
+ if (!N.current) return;
97
+ const t = new ResizeObserver(([m]) => w(m.contentRect.width));
98
+ return t.observe(N.current), () => t.disconnect();
99
+ }, []);
100
+ const [h, f] = $.useState(null), x = Math.max(0, y - b.l - b.r), M = Math.max(0, i - b.t - b.b), l = d.filter((t) => !t.ghost), { max: n, ticks: p } = Y(Math.max(1, ...d.flatMap((t) => t.values))), k = (t) => b.t + M - t / n * M, R = s.length, v = (t) => e === "bar" ? b.l + (t + 0.5) / R * x : b.l + (R <= 1 ? x / 2 : t / (R - 1) * x), j = [...p].reverse(), C = l.map((t, m) => ({ label: t.name, color: t.color ?? O(m), dashed: t.dashed }));
101
+ return /* @__PURE__ */ c("div", { ref: N, className: D("relative w-full font-sans", a), children: [
102
+ C.length > 1 && /* @__PURE__ */ r(ee, { items: C }),
103
+ /* @__PURE__ */ c(
104
+ "svg",
105
+ {
106
+ width: y,
107
+ height: i,
108
+ className: "block select-none",
109
+ onMouseLeave: () => f(null),
110
+ onMouseMove: (t) => {
111
+ const m = t.currentTarget.getBoundingClientRect(), T = t.clientX - m.left, L = Math.round((T - b.l) / x * (R - 1));
112
+ f(Math.max(0, Math.min(R - 1, L)));
113
+ },
114
+ children: [
115
+ j.map((t, m) => /* @__PURE__ */ c("g", { children: [
116
+ /* @__PURE__ */ r(
117
+ "line",
118
+ {
119
+ x1: b.l,
120
+ x2: b.l + x,
121
+ y1: k(t),
122
+ y2: k(t),
123
+ stroke: U,
124
+ strokeWidth: 1,
125
+ strokeDasharray: t === 0 ? void 0 : "3 4",
126
+ opacity: t === 0 ? 0.9 : 0.6
127
+ }
128
+ ),
129
+ /* @__PURE__ */ r("text", { x: b.l - 10, y: k(t), dy: "0.32em", textAnchor: "end", className: "fill-tertiary text-[11px] tabular-nums", children: u(t) })
130
+ ] }, m)),
131
+ Z(s).map(
132
+ (t, m) => t == null ? null : /* @__PURE__ */ r("text", { x: v(m), y: i - 12, textAnchor: "middle", className: "fill-tertiary text-[11px] tabular-nums", children: t }, m)
133
+ ),
134
+ e === "bar" && l.map((t, m) => {
135
+ const T = x / R, L = Math.min(T * 0.7, 64), F = (L - (l.length - 1)) / l.length;
136
+ return t.values.map((I, S) => {
137
+ const K = b.l + S * T + (T - L) / 2 + m * (F + 1), q = h != null && h !== S;
138
+ return /* @__PURE__ */ r(
139
+ "rect",
140
+ {
141
+ x: K,
142
+ y: k(I),
143
+ width: Math.max(0, F),
144
+ height: b.t + M - k(I),
145
+ rx: 2,
146
+ fill: t.color ?? O(m),
147
+ opacity: q ? 0.4 : 1
148
+ },
149
+ `${m}-${S}`
150
+ );
151
+ });
152
+ }),
153
+ e === "area" && l[0] && /* @__PURE__ */ r(
154
+ "path",
155
+ {
156
+ d: `${P(l[0].values.map((t, m) => [v(m), k(t)]))} L${v(R - 1)},${k(0)} L${v(0)},${k(0)} Z`,
157
+ fill: l[0].color ?? O(0),
158
+ opacity: 0.12
159
+ }
160
+ ),
161
+ e !== "bar" && d.map((t, m) => {
162
+ const T = t.color ?? O(l.indexOf(t)), L = t.ghost ? "2 4" : t.dashed ? "6 5" : void 0, F = t.ghost ? 1.75 : m === 0 ? 2.5 : 2;
163
+ return /* @__PURE__ */ r(
164
+ "path",
165
+ {
166
+ d: P(t.values.map((I, S) => [v(S), k(I)])),
167
+ fill: "none",
168
+ stroke: T,
169
+ strokeWidth: F,
170
+ strokeDasharray: L,
171
+ strokeLinecap: "round",
172
+ strokeLinejoin: "round",
173
+ opacity: t.ghost ? 0.4 : 1
174
+ },
175
+ m
176
+ );
177
+ }),
178
+ h != null && e !== "bar" && /* @__PURE__ */ c(J, { children: [
179
+ /* @__PURE__ */ r("line", { x1: v(h), x2: v(h), y1: b.t, y2: b.t + M, stroke: Q, strokeWidth: 1, opacity: 0.18 }),
180
+ l.map((t, m) => /* @__PURE__ */ r(
181
+ "circle",
182
+ {
183
+ cx: v(h),
184
+ cy: k(t.values[h]),
185
+ r: 4.5,
186
+ fill: t.color ?? O(m),
187
+ stroke: "var(--card)",
188
+ strokeWidth: 2
189
+ },
190
+ m
191
+ ))
192
+ ] })
193
+ ]
194
+ }
195
+ ),
196
+ h != null && /* @__PURE__ */ r(
197
+ _,
198
+ {
199
+ x: v(h),
200
+ y: b.t + M / 2,
201
+ flip: v(h) > b.l + x * 0.66,
202
+ title: s[h],
203
+ rows: d.map((t) => ({
204
+ swatch: t.color ?? O(l.indexOf(t)),
205
+ dashed: t.ghost,
206
+ label: t.name,
207
+ value: o(t.values[h])
208
+ }))
209
+ }
210
+ )
211
+ ] });
212
+ }
213
+ const g = { l: 22, r: 16, t: 28, b: 98 }, X = "viz-funnel-hatch";
214
+ function oe({ steps: e, height: s = 320, className: d }) {
215
+ var l;
216
+ const [i, u] = $.useState(720), o = $.useRef(null);
217
+ $.useEffect(() => {
218
+ if (!o.current) return;
219
+ const n = new ResizeObserver(([p]) => u(p.contentRect.width));
220
+ return n.observe(o.current), () => n.disconnect();
221
+ }, []);
222
+ const [a, y] = $.useState(null), w = Math.max(0, i - g.l - g.r), N = Math.max(0, s - g.t - g.b), h = ((l = e[0]) == null ? void 0 : l.value) || 1, f = (n) => g.t + N - n * N, x = w / Math.max(1, e.length), M = Math.min(x * 0.56, 120);
223
+ return /* @__PURE__ */ c("div", { ref: o, className: D("relative w-full font-sans", d), children: [
224
+ /* @__PURE__ */ c("svg", { width: i, height: s, className: "block select-none", children: [
225
+ /* @__PURE__ */ r("defs", { children: /* @__PURE__ */ c("pattern", { id: X, width: 6, height: 6, patternTransform: "rotate(45)", patternUnits: "userSpaceOnUse", children: [
226
+ /* @__PURE__ */ r("rect", { width: 6, height: 6, fill: E, opacity: 0.07 }),
227
+ /* @__PURE__ */ r("line", { x1: 0, y1: 0, x2: 0, y2: 6, stroke: E, strokeWidth: 2.5, opacity: 0.22 })
228
+ ] }) }),
229
+ [1, 0.75, 0.5, 0.25, 0].map((n) => /* @__PURE__ */ c("g", { children: [
230
+ /* @__PURE__ */ r("line", { x1: g.l, x2: g.l + w, y1: f(n), y2: f(n), stroke: U, strokeWidth: 1, strokeDasharray: n === 0 ? void 0 : "3 4", opacity: n === 0 ? 0.9 : 0.6 }),
231
+ /* @__PURE__ */ r("text", { x: g.l, y: f(n) - 4, className: "fill-tertiary text-[10px] tabular-nums", children: n * 100 })
232
+ ] }, n)),
233
+ e.map((n, p) => {
234
+ const k = n.value / h, R = p === 0 ? 1 : e[p - 1].value / h, v = g.l + p * x + x / 2, j = v - M / 2, C = f(k), t = a != null && a !== p;
235
+ return /* @__PURE__ */ c("g", { opacity: t ? 0.55 : 1, onMouseEnter: () => y(p), onMouseLeave: () => y(null), children: [
236
+ p > 0 && /* @__PURE__ */ r("rect", { x: j, y: f(R), width: M, height: Math.max(0, C - f(R)), rx: 4, fill: `url(#${X})` }),
237
+ /* @__PURE__ */ r("rect", { x: j, y: C, width: M, height: Math.max(0, g.t + N - C), rx: 4, fill: A }),
238
+ /* @__PURE__ */ r("text", { x: v, y: C + 16, textAnchor: "middle", className: "fill-[var(--card)] text-[12px] font-semibold tabular-nums", children: V(n.value) }),
239
+ /* @__PURE__ */ r("text", { x: v, y: g.t + N + 24, textAnchor: "middle", className: "fill-secondary text-[12.5px] font-semibold", children: n.label }),
240
+ /* @__PURE__ */ c("text", { x: v, y: g.t + N + 45, textAnchor: "middle", className: "fill-foreground text-[12px] font-semibold tabular-nums", children: [
241
+ H(n.value),
242
+ " ",
243
+ /* @__PURE__ */ r("tspan", { className: "fill-tertiary", children: p === 0 ? "entered" : `→ ${z(k * 100)}` })
244
+ ] }),
245
+ p > 0 && /* @__PURE__ */ c("text", { x: v, y: g.t + N + 66, textAnchor: "middle", className: "fill-tertiary text-[12px] font-semibold tabular-nums", children: [
246
+ "↘ ",
247
+ z((1 - n.value / e[p - 1].value) * 100)
248
+ ] }),
249
+ p > 0 && n.medianTime != null && /* @__PURE__ */ c("text", { x: v, y: g.t + N + 86, textAnchor: "middle", className: "fill-tertiary text-[11px] tabular-nums", children: [
250
+ "◷ ",
251
+ B(n.medianTime)
252
+ ] })
253
+ ] }, p);
254
+ })
255
+ ] }),
256
+ a != null && e[a] && /* @__PURE__ */ r(
257
+ _,
258
+ {
259
+ x: g.l + a * x + x / 2,
260
+ y: g.t + N / 2,
261
+ flip: a >= e.length - 2,
262
+ title: e[a].label,
263
+ rows: [
264
+ { swatch: A, label: "Converted", value: H(e[a].value) },
265
+ ...a > 0 ? [
266
+ { swatch: E, label: "Dropped off", value: `−${H(e[a - 1].value - e[a].value)}` },
267
+ { label: "Conversion so far", value: z(e[a].value / h * 100) },
268
+ { label: "From previous", value: z(e[a].value / e[a - 1].value * 100) },
269
+ { label: "Median time", value: B(e[a].medianTime) }
270
+ ] : []
271
+ ]
272
+ }
273
+ )
274
+ ] });
275
+ }
276
+ function ce({ steps: e, className: s }) {
277
+ var i;
278
+ const d = ((i = e[0]) == null ? void 0 : i.value) || 1;
279
+ return /* @__PURE__ */ r("div", { className: D("flex w-full flex-col gap-[30px] font-sans", s), children: e.map((u, o) => {
280
+ const a = u.value / d, y = o === 0 ? 1 : e[o - 1].value / d;
281
+ return /* @__PURE__ */ c("div", { className: "flex items-center gap-3", children: [
282
+ /* @__PURE__ */ c("div", { className: "min-w-0 flex-1", children: [
283
+ /* @__PURE__ */ c("div", { className: "mb-1.5 flex items-center justify-between gap-3", children: [
284
+ /* @__PURE__ */ c("span", { className: "truncate text-[12.5px] font-semibold text-secondary", children: [
285
+ /* @__PURE__ */ c("span", { className: "text-tertiary tabular-nums", children: [
286
+ o + 1,
287
+ "."
288
+ ] }),
289
+ " ",
290
+ u.label
291
+ ] }),
292
+ o > 0 && u.avgTime != null && /* @__PURE__ */ c("span", { className: "shrink-0 text-[11px] text-tertiary tabular-nums", children: [
293
+ "Avg time ",
294
+ B(u.avgTime)
295
+ ] })
296
+ ] }),
297
+ /* @__PURE__ */ c("div", { className: "relative h-[46px] overflow-hidden rounded-lg bg-muted-foreground/[0.06]", children: [
298
+ o > 0 && /* @__PURE__ */ r(
299
+ "div",
300
+ {
301
+ className: "absolute inset-y-0 left-0",
302
+ style: {
303
+ width: `${y * 100}%`,
304
+ backgroundImage: `repeating-linear-gradient(45deg, color-mix(in srgb, ${E} 9%, transparent) 0 4px, color-mix(in srgb, ${E} 22%, transparent) 4px 5px)`
305
+ }
306
+ }
307
+ ),
308
+ /* @__PURE__ */ r("div", { className: "absolute inset-y-0 left-0 flex items-center rounded-lg", style: { width: `${a * 100}%`, background: A }, children: /* @__PURE__ */ r("span", { className: "ml-3 text-[12px] font-semibold text-[var(--card)] tabular-nums", children: V(u.value) }) })
309
+ ] })
310
+ ] }),
311
+ /* @__PURE__ */ r("span", { className: "w-[60px] shrink-0 text-right text-base font-bold text-foreground tabular-nums", children: z(a * 100) })
312
+ ] }, o);
313
+ }) });
314
+ }
315
+ const W = { l: 48, r: 24, t: 34, b: 34 };
316
+ function se({ points: e, height: s = 300, className: d }) {
317
+ const [i, u] = $.useState(720), o = $.useRef(null);
318
+ $.useEffect(() => {
319
+ if (!o.current) return;
320
+ const l = new ResizeObserver(([n]) => u(n.contentRect.width));
321
+ return l.observe(o.current), () => l.disconnect();
322
+ }, []);
323
+ const [a, y] = $.useState(null), w = Math.max(0, i - W.l - W.r), N = Math.max(0, s - W.t - W.b), h = e.length, f = (l) => W.l + (h <= 1 ? w / 2 : l / (h - 1) * w), x = (l) => W.t + N - l / 100 * N, M = e.map((l, n) => [f(n), x(l.v)]);
324
+ return /* @__PURE__ */ c("div", { ref: o, className: D("relative w-full font-sans", d), children: [
325
+ /* @__PURE__ */ c(
326
+ "svg",
327
+ {
328
+ width: i,
329
+ height: s,
330
+ className: "block select-none",
331
+ onMouseLeave: () => y(null),
332
+ onMouseMove: (l) => {
333
+ const n = l.currentTarget.getBoundingClientRect(), p = Math.round((l.clientX - n.left - W.l) / w * (h - 1));
334
+ y(Math.max(0, Math.min(h - 1, p)));
335
+ },
336
+ children: [
337
+ [100, 75, 50, 25, 0].map((l) => /* @__PURE__ */ c("g", { children: [
338
+ /* @__PURE__ */ r("line", { x1: W.l, x2: W.l + w, y1: x(l), y2: x(l), stroke: U, strokeWidth: 1, strokeDasharray: l === 0 ? void 0 : "3 4", opacity: l === 0 ? 0.9 : 0.6 }),
339
+ /* @__PURE__ */ c("text", { x: W.l - 10, y: x(l), dy: "0.32em", textAnchor: "end", className: "fill-tertiary text-[11px] tabular-nums", children: [
340
+ l,
341
+ "%"
342
+ ] })
343
+ ] }, l)),
344
+ Z(e).map(
345
+ (l, n) => l == null ? null : /* @__PURE__ */ r("text", { x: f(n), y: s - 12, textAnchor: "middle", className: "fill-tertiary text-[11px] tabular-nums", children: l.label }, n)
346
+ ),
347
+ /* @__PURE__ */ r("path", { d: `${P(M)} L${f(h - 1)},${x(0)} L${f(0)},${x(0)} Z`, fill: A, opacity: 0.12 }),
348
+ /* @__PURE__ */ r("path", { d: P(M), fill: "none", stroke: A, strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round" }),
349
+ e.map((l, n) => /* @__PURE__ */ c("g", { children: [
350
+ /* @__PURE__ */ c("text", { x: f(n), y: x(l.v) - 12, textAnchor: "middle", className: "fill-foreground text-[11.5px] font-semibold tabular-nums", children: [
351
+ l.v,
352
+ "%"
353
+ ] }),
354
+ /* @__PURE__ */ r("circle", { cx: f(n), cy: x(l.v), r: a === n ? 5 : 3.5, fill: "var(--card)", stroke: A, strokeWidth: 2 })
355
+ ] }, n))
356
+ ]
357
+ }
358
+ ),
359
+ a != null && e[a] && /* @__PURE__ */ r(
360
+ _,
361
+ {
362
+ x: f(a),
363
+ y: x(e[a].v),
364
+ flip: a >= h - 2,
365
+ title: e[a].label,
366
+ rows: [{ swatch: A, label: "Retention", value: `${e[a].v}%` }]
367
+ }
368
+ )
369
+ ] });
370
+ }
371
+ function te(e) {
372
+ const s = 10 + Math.max(0, Math.min(100, e)) / 100 * 78;
373
+ return `color-mix(in srgb, ${A} ${s}%, transparent)`;
374
+ }
375
+ function ie({ cols: e, cohorts: s, className: d }) {
376
+ return /* @__PURE__ */ r("div", { className: D("w-full overflow-x-auto font-sans", d), children: /* @__PURE__ */ c("table", { className: "w-full border-collapse text-left", children: [
377
+ /* @__PURE__ */ r("thead", { children: /* @__PURE__ */ c("tr", { className: "text-xs font-semibold text-tertiary", children: [
378
+ /* @__PURE__ */ r("th", { className: "sticky left-0 w-[1%] bg-card py-2 pr-4 whitespace-nowrap", children: "Cohort" }),
379
+ /* @__PURE__ */ r("th", { className: "w-[1%] py-2 pr-4 text-right whitespace-nowrap", children: "Users" }),
380
+ e.map((i) => /* @__PURE__ */ r("th", { className: "px-1.5 py-2 text-center tabular-nums", children: i }, i))
381
+ ] }) }),
382
+ /* @__PURE__ */ r("tbody", { children: s.map((i) => /* @__PURE__ */ c("tr", { children: [
383
+ /* @__PURE__ */ r("td", { className: "sticky left-0 w-[1%] bg-card py-1.5 pr-4 text-[12.5px] text-secondary whitespace-nowrap", children: i.label }),
384
+ /* @__PURE__ */ r("td", { className: "w-[1%] py-1.5 pr-4 text-right text-[12.5px] text-secondary tabular-nums whitespace-nowrap", children: H(i.size) }),
385
+ i.values.map((u, o) => /* @__PURE__ */ r("td", { className: "px-1 py-1", children: u == null ? /* @__PURE__ */ r("div", { className: "py-[9px] text-center text-[12.5px] text-tertiary/45 tabular-nums", children: "—" }) : /* @__PURE__ */ c(
386
+ "div",
387
+ {
388
+ className: D(
389
+ "rounded-md py-[9px] text-center text-[12.5px] tabular-nums",
390
+ u >= 50 ? "text-[var(--card)]" : "text-secondary"
391
+ ),
392
+ style: { background: te(u) },
393
+ children: [
394
+ u,
395
+ "%"
396
+ ]
397
+ }
398
+ ) }, o))
399
+ ] }, i.label)) })
400
+ ] }) });
401
+ }
402
+ export {
403
+ ce as FunnelBarsHorizontal,
404
+ oe as FunnelViz,
405
+ se as RetentionCurve,
406
+ ie as RetentionGrid,
407
+ ne as SeriesChart
408
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Number & date formatting for the viz (segmentation) charts — India-first,
3
+ * per the Visualisations handoff. Kept separate from the components so both
4
+ * the renderers and their data tables format identically.
5
+ *
6
+ * Rule of the surface: every numeral is tabular-nums in the ADS face (never
7
+ * a mono font — EXTENDING §2.2), prose labels are sans. These helpers only
8
+ * produce the numeral strings.
9
+ */
10
+ /** Tooltip / table grouping — lakh grouping. 1042318 → "10,42,318". */
11
+ export declare function commafy(n: number): string;
12
+ /**
13
+ * Axis abbreviation with the Indian ladder — capital K, then L (lakh),
14
+ * then Cr (crore). 145000 → "1.5L", 15000000 → "1.5Cr", 2300 → "2.3K".
15
+ */
16
+ export declare function abbr(n: number): string;
17
+ /**
18
+ * Duration as the two most-significant units; "—" for null/0.
19
+ * 21310 → "5h 55m", 305 → "5m 5s", 42 → "42s".
20
+ */
21
+ export declare function fmtDuration(seconds: number | null | undefined): string;
22
+ /** Percentage to one decimal place, the surface default. 12.83 → "12.8%". */
23
+ export declare function pct1(x: number): string;
24
+ /** Percentage to two decimals — funnel-trends tooltip. 12.8 → "12.80%". */
25
+ export declare function pct2(x: number): string;
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ function s(t) {
3
+ return t.toLocaleString("en-IN");
4
+ }
5
+ function o(t, n) {
6
+ return t.toFixed(n).replace(/\.?0+$/, "");
7
+ }
8
+ function l(t) {
9
+ const n = t < 0 ? "-" : "", r = Math.abs(t);
10
+ return r >= 1e7 ? `${n}${o(r / 1e7, 1)}Cr` : r >= 1e5 ? `${n}${o(r / 1e5, 1)}L` : r >= 1e3 ? `${n}${o(r / 1e3, 1)}K` : `${n}${r}`;
11
+ }
12
+ function $(t) {
13
+ if (t == null || t === 0) return "—";
14
+ const n = Math.floor(t), r = [
15
+ [86400, "d"],
16
+ [3600, "h"],
17
+ [60, "m"],
18
+ [1, "s"]
19
+ ], e = [];
20
+ let i = n;
21
+ for (const [u, c] of r)
22
+ if (i >= u || e.length) {
23
+ const f = Math.floor(i / u);
24
+ if ((e.length || f > 0) && (e.push(`${f}${c}`), i -= f * u), e.length === 2) break;
25
+ }
26
+ return e.slice(0, 2).join(" ") || `${n}s`;
27
+ }
28
+ function a(t) {
29
+ return `${o(t, 1)}%`;
30
+ }
31
+ function h(t) {
32
+ return `${t.toFixed(2)}%`;
33
+ }
34
+ export {
35
+ l as abbr,
36
+ s as commafy,
37
+ $ as fmtDuration,
38
+ a as pct1,
39
+ h as pct2
40
+ };
package/llms.txt CHANGED
@@ -332,7 +332,9 @@ When tabs and a search field control the same content, they share one responsive
332
332
  - [ConnectorCard] — Use ConnectorCard to represent one data source/destination on an integrations screen — identity, connection status, health strip, and a usage/action footer. [docs](https://ads.actioneer.dev/#/connector-card)
333
333
  - [EdgeLabelRenderer] — component [docs](https://ads.actioneer.dev/#/infinite-canvas)
334
334
  - [ExplanationAnswer] — component (variants: tone: default|primary|success|warning|danger|info) [docs](https://ads.actioneer.dev/#/ai/answer)
335
+ - [FunnelBarsHorizontal] — component [docs](https://ads.actioneer.dev/#/viz-charts)
335
336
  - [FunnelChart] — component [docs](https://ads.actioneer.dev/#/ai/chart)
337
+ - [FunnelViz] — component [docs](https://ads.actioneer.dev/#/viz-charts)
336
338
  - [Handle] — component [docs](https://ads.actioneer.dev/#/infinite-canvas)
337
339
  - [InfiniteCanvas] — Lay out a node graph or workflow on a pan/zoom surface — pass nodes, edges, and your own nodeTypes; the canvas owns the grid, edges, zoom cluster, snap toggle, and toolbar slots. Use readOnly for a canvas that explains rather than edits. [docs](https://ads.actioneer.dev/#/infinite-canvas)
338
340
  - [LineChart] — component [docs](https://ads.actioneer.dev/#/ai/chart)
@@ -342,7 +344,10 @@ When tabs and a search field control the same content, they share one responsive
342
344
  - [RadarChart] — component [docs](https://ads.actioneer.dev/#/ai/chart)
343
345
  - [RadialBarChart] — component [docs](https://ads.actioneer.dev/#/ai/chart)
344
346
  - [ReactFlowProvider] — component [docs](https://ads.actioneer.dev/#/infinite-canvas)
347
+ - [RetentionCurve] — component [docs](https://ads.actioneer.dev/#/viz-charts)
348
+ - [RetentionGrid] — component [docs](https://ads.actioneer.dev/#/viz-charts)
345
349
  - [ScatterChart] — component [docs](https://ads.actioneer.dev/#/ai/chart)
350
+ - [SeriesChart] — component [docs](https://ads.actioneer.dev/#/viz-charts)
346
351
  - [StatusAnswer] — component (variants: tone: default|primary|success|warning|danger|info) [docs](https://ads.actioneer.dev/#/ai/answer)
347
352
  - [Tab] — component (variants: variant: primary|secondary; orientation: horizontal|vertical; size: sm|md|lg; selected: true|false) [docs](https://ads.actioneer.dev/#/tabs)
348
353
  - [Table] — Use Table for structured tabular data that may need row selection, column sorting, sticky columns, or a paginated footer — not for simple key-value lists. Interactive rows inherit the shared soft neutral hover/focus sink and keep their normal table geometry; do not add a brand tint or hover-only corner rounding. Primary record links stay text-only in every state: never reveal an arrow, chevron, or other directional icon only on hover. Persistent row actions use a final sticky-right column plus elevatedActionColumn so one body-column shadow lifts the action surface instead of repeating on individual cells. The empty matching header cell remains transparent and non-sticky. [docs](https://ads.actioneer.dev/#/table)
@@ -410,9 +415,12 @@ When tabs and a search field control the same content, they share one responsive
410
415
  - [EdgeTypes] — TypeScript type
411
416
  - [ExplanationAnswerProps] — TypeScript type
412
417
  - [ExplanationAnswerSpec] — TypeScript type
418
+ - [FunnelBarsHorizontalProps] — TypeScript type
413
419
  - [FunnelChartProps] — TypeScript type
414
420
  - [FunnelLabelContext] — TypeScript type
415
421
  - [FunnelLabelRenderer] — TypeScript type
422
+ - [FunnelStep] — TypeScript type
423
+ - [FunnelVizProps] — TypeScript type
416
424
  - [InfiniteCanvasProps] — TypeScript type
417
425
  - [LineChartProps] — TypeScript type
418
426
  - [Node] — TypeScript type
@@ -424,9 +432,14 @@ When tabs and a search field control the same content, they share one responsive
424
432
  - [RadarChartProps] — TypeScript type
425
433
  - [RadialBarChartProps] — TypeScript type
426
434
  - [ReactFlowInstance] — TypeScript type
435
+ - [RetentionCohort] — TypeScript type
436
+ - [RetentionCurveProps] — TypeScript type
437
+ - [RetentionGridProps] — TypeScript type
438
+ - [RetentionPoint] — TypeScript type
427
439
  - [ScatterChartProps] — TypeScript type
428
440
  - [Selection] — TypeScript type
429
441
  - [SelectionMode] — TypeScript type
442
+ - [SeriesChartProps] — TypeScript type
430
443
  - [SortDescriptor] — TypeScript type
431
444
  - [SortDirection] — TypeScript type
432
445
  - [StatusAnswerProps] — TypeScript type
@@ -447,6 +460,7 @@ When tabs and a search field control the same content, they share one responsive
447
460
  - [TrendAnswerProps] — TypeScript type
448
461
  - [TrendAnswerSpec] — TypeScript type
449
462
  - [Viewport] — TypeScript type
463
+ - [VizSeries] — TypeScript type
450
464
  - [useEdgesState] — React hook
451
465
  - [useNodesState] — React hook
452
466
  - [useReactFlow] — React hook
@@ -466,6 +480,7 @@ When tabs and a search field control the same content, they share one responsive
466
480
  - [getBezierPath] — utility
467
481
  - [getSmoothStepPath] — utility
468
482
  - [getStraightPath] — utility
483
+ - [MIN_HEIGHT] — utility
469
484
  - [StatusAnswerSchema] — utility
470
485
  - [TrendAnswerSchema] — utility
471
486