@liiift-studio/sanity-visitor-insights 0.8.2 → 0.9.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/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -51,7 +61,7 @@ var import_sanity2 = require("sanity");
51
61
  var import_icons = require("@liiift-studio/sanity-ui-compat/icons");
52
62
 
53
63
  // src/studio/VisitorInsightsTool.tsx
54
- var import_react2 = require("react");
64
+ var import_react3 = require("react");
55
65
  var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
56
66
 
57
67
  // src/studio/useReport.ts
@@ -106,6 +116,7 @@ function useReport({ apiBaseUrl, report, range }) {
106
116
  }
107
117
 
108
118
  // src/studio/Figure.tsx
119
+ var import_react2 = __toESM(require("react"));
109
120
  var import_sanity_ui_compat = require("@liiift-studio/sanity-ui-compat");
110
121
  var import_jsx_runtime = require("react/jsx-runtime");
111
122
  var REASON_TEXT = {
@@ -164,6 +175,18 @@ function ComparisonBar({ label, metric, max, tone = "default" }) {
164
175
  )
165
176
  ] });
166
177
  }
178
+ var chartFrame = { position: "relative", width: "100%" };
179
+ var readout = {
180
+ position: "absolute",
181
+ top: 0,
182
+ right: 0,
183
+ padding: "6px 10px",
184
+ borderRadius: 3,
185
+ background: "var(--card-bg-color, rgba(0,0,0,0.55))",
186
+ border: "1px solid var(--card-border-color, rgba(128,128,128,0.35))",
187
+ pointerEvents: "none",
188
+ maxWidth: "70%"
189
+ };
167
190
  var barHeader = {
168
191
  display: "flex",
169
192
  alignItems: "center",
@@ -191,17 +214,30 @@ function NoticeList({ notices }) {
191
214
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: notice })
192
215
  ] }) }) }, notice)) });
193
216
  }
217
+ var CHART = { w: 760, h: 240, left: 52, right: 12, top: 16, bottom: 30 };
218
+ function niceCeiling(value) {
219
+ if (value <= 0) return 1;
220
+ const magnitude = 10 ** Math.floor(Math.log10(value));
221
+ const normalised = value / magnitude;
222
+ const step = normalised <= 1 ? 1 : normalised <= 2 ? 2 : normalised <= 5 ? 5 : 10;
223
+ return step * magnitude;
224
+ }
225
+ function shortDate(iso) {
226
+ const parsed = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
227
+ if (Number.isNaN(parsed.getTime())) return iso;
228
+ return `${parsed.getUTCDate()} ${parsed.toLocaleString("en-GB", { month: "short", timeZone: "UTC" })}`;
229
+ }
194
230
  function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
231
+ const [hovered, setHovered] = import_react2.default.useState(null);
195
232
  if (points.length < 3) return null;
196
- const W = 720;
197
- const H = 132;
198
- const PAD = 4;
199
233
  const values = points.flatMap((p) => [p.ga4, p.vercel]).filter((v) => typeof v === "number");
200
234
  if (values.length === 0) return null;
201
- const max = Math.max(...values, 1);
202
- const x = (i) => PAD + i / Math.max(1, points.length - 1) * (W - PAD * 2);
203
- const y = (v) => H - PAD - v / max * (H - PAD * 2);
204
- const path = (pick) => {
235
+ const max = niceCeiling(Math.max(...values, 1));
236
+ const plotW = CHART.w - CHART.left - CHART.right;
237
+ const plotH = CHART.h - CHART.top - CHART.bottom;
238
+ const x = (i) => CHART.left + i / Math.max(1, points.length - 1) * plotW;
239
+ const y = (v) => CHART.top + plotH - v / max * plotH;
240
+ const line = (pick) => {
205
241
  let d = "";
206
242
  let penDown = false;
207
243
  points.forEach((p, i) => {
@@ -215,40 +251,145 @@ function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
215
251
  });
216
252
  return d.trim();
217
253
  };
218
- const vercelPath = path((p) => p.vercel);
219
- const ga4Path = path((p) => p.ga4);
220
- const first = points[0]?.date ?? "";
221
- const last = points[points.length - 1]?.date ?? "";
222
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
223
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
224
- "svg",
225
- {
226
- viewBox: `0 0 ${W} ${H}`,
227
- width: "100%",
228
- height: H,
229
- role: "img",
230
- "aria-label": `${label}, ${first} to ${last}. Peak ${Math.round(max)} pageviews in a day.`,
231
- preserveAspectRatio: "none",
232
- style: { display: "block", overflow: "visible" },
233
- children: [
234
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: vercelPath, fill: "none", stroke: "currentColor", strokeOpacity: "0.85", strokeWidth: "2", vectorEffect: "non-scaling-stroke" }),
235
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: ga4Path, fill: "none", stroke: "currentColor", strokeOpacity: "0.85", strokeWidth: "2", strokeDasharray: "5 3", vectorEffect: "non-scaling-stroke" })
236
- ]
237
- }
238
- ) }),
254
+ const bands = [];
255
+ let run = [];
256
+ const flushRun = () => {
257
+ if (run.length > 1) {
258
+ const top = run.map((i) => `${x(i).toFixed(1)} ${y(points[i].vercel).toFixed(1)}`);
259
+ const bottom = [...run].reverse().map((i) => `${x(i).toFixed(1)} ${y(points[i].ga4).toFixed(1)}`);
260
+ bands.push(`M${top.join(" L")} L${bottom.join(" L")} Z`);
261
+ }
262
+ run = [];
263
+ };
264
+ points.forEach((p, i) => {
265
+ if (typeof p.ga4 === "number" && typeof p.vercel === "number") run.push(i);
266
+ else flushRun();
267
+ });
268
+ flushRun();
269
+ const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => ({ value: max * f, y: y(max * f) }));
270
+ const labelEvery = Math.max(1, Math.ceil(points.length / 6));
271
+ const dateTicks = points.map((p, i) => ({ i, date: p.date })).filter(({ i }) => i % labelEvery === 0 || i === points.length - 1);
272
+ const active = hovered !== null ? points[hovered] : null;
273
+ const activeGap = active && typeof active.ga4 === "number" && typeof active.vercel === "number" && active.vercel > 0 ? (active.vercel - active.ga4) / active.vercel : null;
274
+ const onKeyDown = (event) => {
275
+ const last = points.length - 1;
276
+ const current = hovered ?? 0;
277
+ const move = (next) => {
278
+ event.preventDefault();
279
+ setHovered(Math.max(0, Math.min(last, next)));
280
+ };
281
+ if (event.key === "ArrowRight") return move(hovered === null ? 0 : current + 1);
282
+ if (event.key === "ArrowLeft") return move(hovered === null ? last : current - 1);
283
+ if (event.key === "Home") return move(0);
284
+ if (event.key === "End") return move(last);
285
+ if (event.key === "Escape") {
286
+ setHovered(null);
287
+ return;
288
+ }
289
+ };
290
+ const onMove = (event) => {
291
+ const box = event.currentTarget.getBoundingClientRect();
292
+ if (box.width === 0) return;
293
+ const svgX = (event.clientX - box.left) / box.width * CHART.w;
294
+ const ratio = (svgX - CHART.left) / plotW;
295
+ const index = Math.round(ratio * (points.length - 1));
296
+ setHovered(index >= 0 && index < points.length ? index : null);
297
+ };
298
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
299
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: chartFrame, children: [
300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
301
+ "svg",
302
+ {
303
+ viewBox: `0 0 ${CHART.w} ${CHART.h}`,
304
+ width: "100%",
305
+ style: { display: "block", height: "auto" },
306
+ role: "img",
307
+ "aria-label": `${label}, ${points[0]?.date} to ${points[points.length - 1]?.date}. Peak ${formatCount(max)} pageviews in a day.`,
308
+ onMouseMove: onMove,
309
+ onMouseLeave: () => setHovered(null),
310
+ onKeyDown,
311
+ onBlur: () => setHovered(null),
312
+ tabIndex: 0,
313
+ "aria-describedby": active ? "trend-readout" : void 0,
314
+ children: [
315
+ ticks.map((tick) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
316
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
317
+ "line",
318
+ {
319
+ x1: CHART.left,
320
+ x2: CHART.w - CHART.right,
321
+ y1: tick.y,
322
+ y2: tick.y,
323
+ stroke: "currentColor",
324
+ strokeOpacity: tick.value === 0 ? 0.35 : 0.12,
325
+ strokeWidth: "1"
326
+ }
327
+ ),
328
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
329
+ "text",
330
+ {
331
+ x: CHART.left - 8,
332
+ y: tick.y + 4,
333
+ textAnchor: "end",
334
+ fontSize: "11",
335
+ fill: "currentColor",
336
+ fillOpacity: "0.55",
337
+ children: formatCount(tick.value)
338
+ }
339
+ )
340
+ ] }, tick.value)),
341
+ dateTicks.map(({ i, date }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
342
+ "text",
343
+ {
344
+ x: x(i),
345
+ y: CHART.h - 10,
346
+ textAnchor: "middle",
347
+ fontSize: "11",
348
+ fill: "currentColor",
349
+ fillOpacity: "0.55",
350
+ children: shortDate(date)
351
+ },
352
+ date
353
+ )),
354
+ bands.map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d, fill: "currentColor", fillOpacity: "0.09", stroke: "none" }, d.slice(0, 24))),
355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.vercel), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2" }),
356
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.ga4), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2", strokeDasharray: "5 3" }),
357
+ hovered !== null && active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
358
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
359
+ "line",
360
+ {
361
+ x1: x(hovered),
362
+ x2: x(hovered),
363
+ y1: CHART.top,
364
+ y2: CHART.top + plotH,
365
+ stroke: "currentColor",
366
+ strokeOpacity: "0.4",
367
+ strokeWidth: "1"
368
+ }
369
+ ),
370
+ typeof active.vercel === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.vercel), r: "4", fill: "currentColor" }),
371
+ typeof active.ga4 === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.ga4), r: "4", fill: "currentColor" })
372
+ ] })
373
+ ]
374
+ }
375
+ ),
376
+ active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: readout, id: "trend-readout", role: "status", "aria-live": "polite", children: [
377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: shortDate(active.date) }),
378
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: [
379
+ "Vercel ",
380
+ typeof active.vercel === "number" ? formatCount(active.vercel) : "\u2014",
381
+ " \xB7 ",
382
+ "GA4 ",
383
+ typeof active.ga4 === "number" ? formatCount(active.ga4) : "\u2014",
384
+ activeGap !== null ? ` \xB7 gap ${formatPercent(activeGap, 0)}` : ""
385
+ ] })
386
+ ] })
387
+ ] }),
239
388
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: legendRow, children: [
240
389
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2501\u2501 Vercel" }),
241
390
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2504\u2504 GA4" }),
242
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
243
- first,
244
- " to ",
245
- last
246
- ] }),
247
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
248
- "peak ",
249
- formatCount(max),
250
- "/day"
251
- ] })
391
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "shaded band is the gap" }),
392
+ !active && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "hover, or focus and use arrow keys, for a day" })
252
393
  ] })
253
394
  ] });
254
395
  }
@@ -437,8 +578,8 @@ var PANELS = [
437
578
  { report: "diagnostics", label: "Diagnostics", blurb: "What to fix before trusting the numbers above" }
438
579
  ];
439
580
  function RangeSelector({ value, onChange }) {
440
- const refs = (0, import_react2.useRef)([]);
441
- const onKeyDown = (0, import_react2.useCallback)(
581
+ const refs = (0, import_react3.useRef)([]);
582
+ const onKeyDown = (0, import_react3.useCallback)(
442
583
  (event) => {
443
584
  const currentIndex = RANGES.findIndex((r) => r.key === value);
444
585
  let nextIndex = null;
@@ -494,8 +635,8 @@ function SourceStatusRow({ sources }) {
494
635
  ] }) });
495
636
  }
496
637
  function PanelTabs({ value, onChange }) {
497
- const refs = (0, import_react2.useRef)([]);
498
- const onKeyDown = (0, import_react2.useCallback)(
638
+ const refs = (0, import_react3.useRef)([]);
639
+ const onKeyDown = (0, import_react3.useCallback)(
499
640
  (event) => {
500
641
  const currentIndex = PANELS.findIndex((p) => p.report === value);
501
642
  let nextIndex = null;
@@ -571,8 +712,8 @@ function ReportPanel({ report, apiBaseUrl, range }) {
571
712
  function VisitorInsightsTool(props) {
572
713
  const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
573
714
  const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
574
- const [range, setRange] = (0, import_react2.useState)("week");
575
- const [activePanel, setActivePanel] = (0, import_react2.useState)("measurement-health");
715
+ const [range, setRange] = (0, import_react3.useState)("week");
716
+ const [activePanel, setActivePanel] = (0, import_react3.useState)("measurement-health");
576
717
  const active = PANELS.find((p) => p.report === activePanel) ?? PANELS[0];
577
718
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Container, { width: 4, padding: 4, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 5, children: [
578
719
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "flex-start", justify: "space-between", gap: 4, wrap: "wrap", children: [