@liiift-studio/sanity-visitor-insights 0.8.2 → 0.10.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 +531 -134
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +515 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/Figure.tsx +440 -57
- package/src/studio/VisitorInsightsTool.tsx +71 -17
- package/src/studio/panels.test.tsx +163 -7
- package/src/studio/panels.tsx +213 -93
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
|
|
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",
|
|
@@ -177,6 +200,21 @@ var legendRow = {
|
|
|
177
200
|
alignItems: "center",
|
|
178
201
|
flexWrap: "wrap"
|
|
179
202
|
};
|
|
203
|
+
var disclosure = {
|
|
204
|
+
appearance: "none",
|
|
205
|
+
background: "transparent",
|
|
206
|
+
border: "none",
|
|
207
|
+
color: "inherit",
|
|
208
|
+
opacity: 0.75,
|
|
209
|
+
font: "inherit",
|
|
210
|
+
fontSize: "0.85em",
|
|
211
|
+
padding: "2px 0",
|
|
212
|
+
textDecoration: "underline",
|
|
213
|
+
textUnderlineOffset: 3,
|
|
214
|
+
cursor: "pointer",
|
|
215
|
+
justifySelf: "start",
|
|
216
|
+
width: "fit-content"
|
|
217
|
+
};
|
|
180
218
|
var noticeRow = {
|
|
181
219
|
display: "flex",
|
|
182
220
|
gap: 12,
|
|
@@ -185,23 +223,55 @@ var noticeRow = {
|
|
|
185
223
|
};
|
|
186
224
|
var noticeBadge = { flex: "0 0 auto" };
|
|
187
225
|
function NoticeList({ notices }) {
|
|
226
|
+
const [expanded, setExpanded] = import_react2.default.useState(false);
|
|
188
227
|
if (notices.length === 0) return null;
|
|
189
|
-
|
|
228
|
+
const alwaysShown = notices.slice(0, 2);
|
|
229
|
+
const hidden = notices.slice(2);
|
|
230
|
+
const item = (notice) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: noticeRow, children: [
|
|
190
231
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: noticeBadge, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "caution", fontSize: 0, children: "Caveat" }) }),
|
|
191
232
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: notice })
|
|
192
|
-
] }) }) }, notice)
|
|
233
|
+
] }) }) }, notice);
|
|
234
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
235
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "grid", gap: 8 }, children: [
|
|
236
|
+
alwaysShown.map(item),
|
|
237
|
+
expanded && hidden.map(item)
|
|
238
|
+
] }),
|
|
239
|
+
hidden.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
240
|
+
"button",
|
|
241
|
+
{
|
|
242
|
+
type: "button",
|
|
243
|
+
style: disclosure,
|
|
244
|
+
onClick: () => setExpanded((open) => !open),
|
|
245
|
+
"aria-expanded": expanded,
|
|
246
|
+
children: expanded ? "Show fewer caveats" : `${hidden.length} more ${hidden.length === 1 ? "caveat" : "caveats"}`
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
] });
|
|
250
|
+
}
|
|
251
|
+
var CHART = { w: 760, h: 240, left: 52, right: 12, top: 16, bottom: 30 };
|
|
252
|
+
function niceCeiling(value) {
|
|
253
|
+
if (value <= 0) return 1;
|
|
254
|
+
const magnitude = 10 ** Math.floor(Math.log10(value));
|
|
255
|
+
const normalised = value / magnitude;
|
|
256
|
+
const step = normalised <= 1 ? 1 : normalised <= 2 ? 2 : normalised <= 5 ? 5 : 10;
|
|
257
|
+
return step * magnitude;
|
|
258
|
+
}
|
|
259
|
+
function shortDate(iso) {
|
|
260
|
+
const parsed = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
|
|
261
|
+
if (Number.isNaN(parsed.getTime())) return iso;
|
|
262
|
+
return `${parsed.getUTCDate()} ${parsed.toLocaleString("en-GB", { month: "short", timeZone: "UTC" })}`;
|
|
193
263
|
}
|
|
194
264
|
function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
|
|
265
|
+
const [hovered, setHovered] = import_react2.default.useState(null);
|
|
195
266
|
if (points.length < 3) return null;
|
|
196
|
-
const W = 720;
|
|
197
|
-
const H = 132;
|
|
198
|
-
const PAD = 4;
|
|
199
267
|
const values = points.flatMap((p) => [p.ga4, p.vercel]).filter((v) => typeof v === "number");
|
|
200
268
|
if (values.length === 0) return null;
|
|
201
|
-
const max = Math.max(...values, 1);
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
269
|
+
const max = niceCeiling(Math.max(...values, 1));
|
|
270
|
+
const plotW = CHART.w - CHART.left - CHART.right;
|
|
271
|
+
const plotH = CHART.h - CHART.top - CHART.bottom;
|
|
272
|
+
const x = (i) => CHART.left + i / Math.max(1, points.length - 1) * plotW;
|
|
273
|
+
const y = (v) => CHART.top + plotH - v / max * plotH;
|
|
274
|
+
const line = (pick) => {
|
|
205
275
|
let d = "";
|
|
206
276
|
let penDown = false;
|
|
207
277
|
points.forEach((p, i) => {
|
|
@@ -215,43 +285,246 @@ function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
|
|
|
215
285
|
});
|
|
216
286
|
return d.trim();
|
|
217
287
|
};
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
288
|
+
const bands = [];
|
|
289
|
+
let run = [];
|
|
290
|
+
const flushRun = () => {
|
|
291
|
+
if (run.length > 1) {
|
|
292
|
+
const top = run.map((i) => `${x(i).toFixed(1)} ${y(points[i].vercel).toFixed(1)}`);
|
|
293
|
+
const bottom = [...run].reverse().map((i) => `${x(i).toFixed(1)} ${y(points[i].ga4).toFixed(1)}`);
|
|
294
|
+
bands.push(`M${top.join(" L")} L${bottom.join(" L")} Z`);
|
|
295
|
+
}
|
|
296
|
+
run = [];
|
|
297
|
+
};
|
|
298
|
+
points.forEach((p, i) => {
|
|
299
|
+
if (typeof p.ga4 === "number" && typeof p.vercel === "number") run.push(i);
|
|
300
|
+
else flushRun();
|
|
301
|
+
});
|
|
302
|
+
flushRun();
|
|
303
|
+
const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => ({ value: max * f, y: y(max * f) }));
|
|
304
|
+
const labelEvery = Math.max(1, Math.ceil(points.length / 6));
|
|
305
|
+
const dateTicks = points.map((p, i) => ({ i, date: p.date })).filter(({ i }) => i % labelEvery === 0 || i === points.length - 1);
|
|
306
|
+
const active = hovered !== null ? points[hovered] : null;
|
|
307
|
+
const activeGap = active && typeof active.ga4 === "number" && typeof active.vercel === "number" && active.vercel > 0 ? (active.vercel - active.ga4) / active.vercel : null;
|
|
308
|
+
const onKeyDown = (event) => {
|
|
309
|
+
const last = points.length - 1;
|
|
310
|
+
const current = hovered ?? 0;
|
|
311
|
+
const move = (next) => {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
setHovered(Math.max(0, Math.min(last, next)));
|
|
314
|
+
};
|
|
315
|
+
if (event.key === "ArrowRight") return move(hovered === null ? 0 : current + 1);
|
|
316
|
+
if (event.key === "ArrowLeft") return move(hovered === null ? last : current - 1);
|
|
317
|
+
if (event.key === "Home") return move(0);
|
|
318
|
+
if (event.key === "End") return move(last);
|
|
319
|
+
if (event.key === "Escape") {
|
|
320
|
+
setHovered(null);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const onMove = (event) => {
|
|
325
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
326
|
+
if (box.width === 0) return;
|
|
327
|
+
const svgX = (event.clientX - box.left) / box.width * CHART.w;
|
|
328
|
+
const ratio = (svgX - CHART.left) / plotW;
|
|
329
|
+
const index = Math.round(ratio * (points.length - 1));
|
|
330
|
+
setHovered(index >= 0 && index < points.length ? index : null);
|
|
331
|
+
};
|
|
332
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
|
|
333
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: chartFrame, children: [
|
|
334
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
335
|
+
"svg",
|
|
336
|
+
{
|
|
337
|
+
viewBox: `0 0 ${CHART.w} ${CHART.h}`,
|
|
338
|
+
width: "100%",
|
|
339
|
+
style: { display: "block", height: "auto" },
|
|
340
|
+
role: "img",
|
|
341
|
+
"aria-label": `${label}, ${points[0]?.date} to ${points[points.length - 1]?.date}. Peak ${formatCount(max)} pageviews in a day.`,
|
|
342
|
+
onMouseMove: onMove,
|
|
343
|
+
onMouseLeave: () => setHovered(null),
|
|
344
|
+
onKeyDown,
|
|
345
|
+
onBlur: () => setHovered(null),
|
|
346
|
+
tabIndex: 0,
|
|
347
|
+
"aria-describedby": active ? "trend-readout" : void 0,
|
|
348
|
+
children: [
|
|
349
|
+
ticks.map((tick) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
|
|
350
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
351
|
+
"line",
|
|
352
|
+
{
|
|
353
|
+
x1: CHART.left,
|
|
354
|
+
x2: CHART.w - CHART.right,
|
|
355
|
+
y1: tick.y,
|
|
356
|
+
y2: tick.y,
|
|
357
|
+
stroke: "currentColor",
|
|
358
|
+
strokeOpacity: tick.value === 0 ? 0.35 : 0.12,
|
|
359
|
+
strokeWidth: "1"
|
|
360
|
+
}
|
|
361
|
+
),
|
|
362
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
363
|
+
"text",
|
|
364
|
+
{
|
|
365
|
+
x: CHART.left - 8,
|
|
366
|
+
y: tick.y + 4,
|
|
367
|
+
textAnchor: "end",
|
|
368
|
+
fontSize: "11",
|
|
369
|
+
fill: "currentColor",
|
|
370
|
+
fillOpacity: "0.55",
|
|
371
|
+
children: formatCount(tick.value)
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
] }, tick.value)),
|
|
375
|
+
dateTicks.map(({ i, date }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
376
|
+
"text",
|
|
377
|
+
{
|
|
378
|
+
x: x(i),
|
|
379
|
+
y: CHART.h - 10,
|
|
380
|
+
textAnchor: "middle",
|
|
381
|
+
fontSize: "11",
|
|
382
|
+
fill: "currentColor",
|
|
383
|
+
fillOpacity: "0.55",
|
|
384
|
+
children: shortDate(date)
|
|
385
|
+
},
|
|
386
|
+
date
|
|
387
|
+
)),
|
|
388
|
+
bands.map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d, fill: "currentColor", fillOpacity: "0.09", stroke: "none" }, d.slice(0, 24))),
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.vercel), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2" }),
|
|
390
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.ga4), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2", strokeDasharray: "5 3" }),
|
|
391
|
+
hovered !== null && active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
|
|
392
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
393
|
+
"line",
|
|
394
|
+
{
|
|
395
|
+
x1: x(hovered),
|
|
396
|
+
x2: x(hovered),
|
|
397
|
+
y1: CHART.top,
|
|
398
|
+
y2: CHART.top + plotH,
|
|
399
|
+
stroke: "currentColor",
|
|
400
|
+
strokeOpacity: "0.4",
|
|
401
|
+
strokeWidth: "1"
|
|
402
|
+
}
|
|
403
|
+
),
|
|
404
|
+
typeof active.vercel === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.vercel), r: "4", fill: "currentColor" }),
|
|
405
|
+
typeof active.ga4 === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.ga4), r: "4", fill: "currentColor" })
|
|
406
|
+
] })
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
),
|
|
410
|
+
active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: readout, id: "trend-readout", role: "status", "aria-live": "polite", children: [
|
|
411
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: shortDate(active.date) }),
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: [
|
|
413
|
+
"Vercel ",
|
|
414
|
+
typeof active.vercel === "number" ? formatCount(active.vercel) : "\u2014",
|
|
415
|
+
" \xB7 ",
|
|
416
|
+
"GA4 ",
|
|
417
|
+
typeof active.ga4 === "number" ? formatCount(active.ga4) : "\u2014",
|
|
418
|
+
activeGap !== null ? ` \xB7 gap ${formatPercent(activeGap, 0)}` : ""
|
|
419
|
+
] })
|
|
420
|
+
] })
|
|
421
|
+
] }),
|
|
239
422
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: legendRow, children: [
|
|
240
423
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2501\u2501 Vercel" }),
|
|
241
424
|
/* @__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.
|
|
243
|
-
|
|
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
|
-
] })
|
|
425
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "shaded band is the gap" }),
|
|
426
|
+
!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
427
|
] })
|
|
253
428
|
] });
|
|
254
429
|
}
|
|
430
|
+
function SortableTable({ caption, columns, rows, rowKey, initialSort }) {
|
|
431
|
+
const [sort, setSort] = import_react2.default.useState(
|
|
432
|
+
initialSort ? { key: initialSort, desc: true } : null
|
|
433
|
+
);
|
|
434
|
+
const active = sort ? columns.find((c) => c.key === sort.key) : void 0;
|
|
435
|
+
const ordered = import_react2.default.useMemo(() => {
|
|
436
|
+
if (!active || !sort) return rows;
|
|
437
|
+
const copy = [...rows];
|
|
438
|
+
copy.sort((a, b) => {
|
|
439
|
+
const av = active.sortValue(a);
|
|
440
|
+
const bv = active.sortValue(b);
|
|
441
|
+
if (av === null && bv === null) return 0;
|
|
442
|
+
if (av === null) return 1;
|
|
443
|
+
if (bv === null) return -1;
|
|
444
|
+
const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv));
|
|
445
|
+
return sort.desc ? -cmp : cmp;
|
|
446
|
+
});
|
|
447
|
+
return copy;
|
|
448
|
+
}, [rows, active, sort]);
|
|
449
|
+
const toggle = (column) => {
|
|
450
|
+
setSort((current) => {
|
|
451
|
+
if (current?.key !== column.key) return { key: column.key, desc: Boolean(column.numeric) };
|
|
452
|
+
return { key: column.key, desc: !current.desc };
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Card, { radius: 2, tone: "transparent", border: true, style: tableWrapper, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: tableBase, children: [
|
|
456
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("caption", { style: visuallyHidden, children: caption }),
|
|
457
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: columns.map((column) => {
|
|
458
|
+
const isActive = sort?.key === column.key;
|
|
459
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
460
|
+
"th",
|
|
461
|
+
{
|
|
462
|
+
scope: "col",
|
|
463
|
+
style: column.numeric ? headCellNumeric : headCell,
|
|
464
|
+
"aria-sort": isActive ? sort?.desc ? "descending" : "ascending" : "none",
|
|
465
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", style: sortButton(Boolean(column.numeric)), onClick: () => toggle(column), children: [
|
|
466
|
+
column.label,
|
|
467
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: sortMark, children: isActive ? sort?.desc ? "\u25BC" : "\u25B2" : "\u2195" })
|
|
468
|
+
] })
|
|
469
|
+
},
|
|
470
|
+
column.key
|
|
471
|
+
);
|
|
472
|
+
}) }) }),
|
|
473
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: ordered.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: columns.map((column, index) => {
|
|
474
|
+
const content = column.render(row);
|
|
475
|
+
return index === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "row", style: bodyCell, children: content }, column.key) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: column.numeric ? bodyCellNumeric : bodyCell, children: content }, column.key);
|
|
476
|
+
}) }, rowKey(row))) })
|
|
477
|
+
] }) });
|
|
478
|
+
}
|
|
479
|
+
var tableWrapper = { overflowX: "auto", width: "100%" };
|
|
480
|
+
var tableBase = { width: "100%", borderCollapse: "collapse", minWidth: 420 };
|
|
481
|
+
var headCell = {
|
|
482
|
+
padding: 0,
|
|
483
|
+
textAlign: "left",
|
|
484
|
+
borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
|
|
485
|
+
whiteSpace: "nowrap"
|
|
486
|
+
};
|
|
487
|
+
var headCellNumeric = { ...headCell, textAlign: "right" };
|
|
488
|
+
var bodyCell = {
|
|
489
|
+
padding: "8px 12px",
|
|
490
|
+
textAlign: "left",
|
|
491
|
+
fontWeight: 400,
|
|
492
|
+
borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.18))"
|
|
493
|
+
};
|
|
494
|
+
var bodyCellNumeric = {
|
|
495
|
+
...bodyCell,
|
|
496
|
+
textAlign: "right",
|
|
497
|
+
fontVariantNumeric: "tabular-nums"
|
|
498
|
+
};
|
|
499
|
+
function sortButton(numeric) {
|
|
500
|
+
return {
|
|
501
|
+
appearance: "none",
|
|
502
|
+
background: "transparent",
|
|
503
|
+
border: "none",
|
|
504
|
+
color: "inherit",
|
|
505
|
+
font: "inherit",
|
|
506
|
+
fontSize: "0.78em",
|
|
507
|
+
letterSpacing: "0.06em",
|
|
508
|
+
textTransform: "uppercase",
|
|
509
|
+
opacity: 0.7,
|
|
510
|
+
padding: "8px 12px",
|
|
511
|
+
width: "100%",
|
|
512
|
+
display: "flex",
|
|
513
|
+
gap: 6,
|
|
514
|
+
alignItems: "center",
|
|
515
|
+
justifyContent: numeric ? "flex-end" : "flex-start",
|
|
516
|
+
cursor: "pointer"
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
var sortMark = { opacity: 0.7, fontSize: "0.9em" };
|
|
520
|
+
var visuallyHidden = {
|
|
521
|
+
position: "absolute",
|
|
522
|
+
width: 1,
|
|
523
|
+
height: 1,
|
|
524
|
+
overflow: "hidden",
|
|
525
|
+
clip: "rect(0 0 0 0)",
|
|
526
|
+
whiteSpace: "nowrap"
|
|
527
|
+
};
|
|
255
528
|
|
|
256
529
|
// src/studio/VisitorInsightsTool.tsx
|
|
257
530
|
var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
|
|
@@ -259,23 +532,34 @@ var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
|
|
|
259
532
|
// src/studio/panels.tsx
|
|
260
533
|
var import_sanity_ui_compat2 = require("@liiift-studio/sanity-ui-compat");
|
|
261
534
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
535
|
+
function metricSortValue(metric) {
|
|
536
|
+
return metric.status === "unavailable" ? null : metric.value;
|
|
537
|
+
}
|
|
262
538
|
function maxOf(metrics) {
|
|
263
539
|
return metrics.reduce((max, metric) => metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
|
|
264
540
|
}
|
|
265
|
-
var
|
|
541
|
+
var sectionHeading = { margin: "0 0 2px", lineHeight: 1.3 };
|
|
542
|
+
var sourceLink = {
|
|
543
|
+
color: "inherit",
|
|
544
|
+
textDecoration: "underline",
|
|
545
|
+
textUnderlineOffset: 3,
|
|
546
|
+
textDecorationThickness: 1
|
|
547
|
+
};
|
|
548
|
+
function isLinkableHost(source) {
|
|
549
|
+
return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(source);
|
|
550
|
+
}
|
|
266
551
|
var cardGrid = {
|
|
267
552
|
display: "grid",
|
|
268
553
|
gridTemplateColumns: "repeat(auto-fit, minmax(min(14rem, 100%), 1fr))",
|
|
269
554
|
gap: 16
|
|
270
555
|
};
|
|
271
|
-
var table = { width: "100%", borderCollapse: "collapse", minWidth: 420 };
|
|
272
556
|
var cell = { padding: "8px 12px", textAlign: "left", borderBottom: "1px solid var(--card-border-color)" };
|
|
273
557
|
var numericCell = { ...cell, textAlign: "right" };
|
|
274
558
|
function MeasurementHealthPanel({ data }) {
|
|
275
559
|
const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
|
|
276
560
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
|
|
277
561
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
278
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Pageviews, source against source" }),
|
|
562
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Pageviews, source against source" }),
|
|
279
563
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "The same unit on both sides. Vercel is cookieless and ungated; GA4 is consent-gated and blockable, so GA4 seeing fewer is expected." }),
|
|
280
564
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax, tone: "primary" }),
|
|
281
565
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax, tone: "default" })
|
|
@@ -285,12 +569,12 @@ function MeasurementHealthPanel({ data }) {
|
|
|
285
569
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: data.interpretation })
|
|
286
570
|
] }) }),
|
|
287
571
|
(data.daily?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
288
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Day by day" }),
|
|
572
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Day by day" }),
|
|
289
573
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "A gap that has always been there is consent and blocking. A gap that opens on one day is an incident." }),
|
|
290
574
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrendChart, { points: data.daily ?? [] })
|
|
291
575
|
] }),
|
|
292
576
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
293
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Context" }),
|
|
577
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Context" }),
|
|
294
578
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Different units to the figures above, and to each other. Shown for scale, never differenced." }),
|
|
295
579
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: cardGrid, children: [
|
|
296
580
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
@@ -325,72 +609,157 @@ function AcquisitionPanel({ data }) {
|
|
|
325
609
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 4, children: formatPercent(data.unattributedShare, 1) })
|
|
326
610
|
] }) })
|
|
327
611
|
] }),
|
|
328
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
329
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
330
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
612
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
613
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Traffic sources" }),
|
|
614
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Sort by any column. Sessions is the default." }),
|
|
615
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
616
|
+
SortableTable,
|
|
617
|
+
{
|
|
618
|
+
caption: "Traffic sources by sessions",
|
|
619
|
+
initialSort: "sessions",
|
|
620
|
+
rows: data.rows ?? [],
|
|
621
|
+
rowKey: (row) => `${row.source}-${row.channel}`,
|
|
622
|
+
columns: [
|
|
623
|
+
{
|
|
624
|
+
key: "source",
|
|
625
|
+
label: "Source",
|
|
626
|
+
sortValue: (row) => row.source,
|
|
627
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [
|
|
628
|
+
isLinkableHost(row.source) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
629
|
+
"a",
|
|
630
|
+
{
|
|
631
|
+
href: `https://${row.source}`,
|
|
632
|
+
target: "_blank",
|
|
633
|
+
rel: "noopener noreferrer",
|
|
634
|
+
style: sourceLink,
|
|
635
|
+
children: row.source
|
|
636
|
+
}
|
|
637
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: row.source }),
|
|
638
|
+
row.designIndustry && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Badge, { tone: "primary", fontSize: 0, children: "Design" }),
|
|
639
|
+
row.unattributed && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Badge, { tone: "caution", fontSize: 0, children: "Unattributed" })
|
|
640
|
+
] })
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
key: "channel",
|
|
644
|
+
label: "Channel",
|
|
645
|
+
sortValue: (row) => row.channel,
|
|
646
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: row.channel })
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
key: "sessions",
|
|
650
|
+
label: "Sessions",
|
|
651
|
+
numeric: true,
|
|
652
|
+
sortValue: (row) => row.sessions,
|
|
653
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(row.sessions) })
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
)
|
|
658
|
+
] }),
|
|
345
659
|
data.rowsWithheld && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NoticeList, { notices: ["GA4 withheld some low-traffic rows for privacy, so this list is shorter than reality."] })
|
|
346
660
|
] });
|
|
347
661
|
}
|
|
348
662
|
function JourneyPanel({ data }) {
|
|
349
|
-
const
|
|
663
|
+
const allSteps = data.steps ?? [];
|
|
664
|
+
const shown = allSteps.filter((step) => step.count.status !== "unavailable");
|
|
665
|
+
const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
|
|
666
|
+
const max = maxOf(shown.map((step) => step.count));
|
|
350
667
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
|
|
351
668
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: data.approximationNote }) }),
|
|
352
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children:
|
|
669
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children: shown.map((step, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
|
|
353
670
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: step.label, metric: step.count, max, tone: "primary" }),
|
|
354
|
-
step.conversionFromPrevious !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: [
|
|
671
|
+
step.conversionFromPrevious !== null && index > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: [
|
|
355
672
|
formatPercent(step.conversionFromPrevious, 1),
|
|
356
|
-
" of
|
|
673
|
+
" of ",
|
|
674
|
+
shown[index - 1]?.label.toLowerCase()
|
|
357
675
|
] })
|
|
358
676
|
] }, step.key)) }),
|
|
677
|
+
hiddenSteps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: [
|
|
678
|
+
"Not shown: ",
|
|
679
|
+
hiddenSteps.map((step) => step.label.toLowerCase()).join(", "),
|
|
680
|
+
" \u2014 not instrumented on this site, so there is no figure to place in the funnel."
|
|
681
|
+
] }),
|
|
359
682
|
(data.topLandingPages?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
360
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Where sessions began" }),
|
|
361
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
683
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Where sessions began" }),
|
|
684
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
685
|
+
SortableTable,
|
|
686
|
+
{
|
|
687
|
+
caption: "Landing pages by sessions",
|
|
688
|
+
initialSort: "sessions",
|
|
689
|
+
rows: data.topLandingPages ?? [],
|
|
690
|
+
rowKey: (page) => page.path,
|
|
691
|
+
columns: [
|
|
692
|
+
{
|
|
693
|
+
key: "path",
|
|
694
|
+
label: "Page",
|
|
695
|
+
sortValue: (page) => page.path,
|
|
696
|
+
render: (page) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: page.path })
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
key: "sessions",
|
|
700
|
+
label: "Sessions",
|
|
701
|
+
numeric: true,
|
|
702
|
+
sortValue: (page) => page.sessions,
|
|
703
|
+
render: (page) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(page.sessions) })
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
)
|
|
371
708
|
] })
|
|
372
709
|
] });
|
|
373
710
|
}
|
|
374
711
|
function TypefaceInterestPanel({ data }) {
|
|
375
712
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
|
|
376
713
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: data.interpretationNote }) }),
|
|
377
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
378
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
379
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
714
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
715
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Engagement by typeface" }),
|
|
716
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Sort by any column. Views is the default." }),
|
|
717
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
718
|
+
SortableTable,
|
|
719
|
+
{
|
|
720
|
+
caption: "Engagement by typeface",
|
|
721
|
+
initialSort: "viewed",
|
|
722
|
+
rows: data.rows ?? [],
|
|
723
|
+
rowKey: (row) => row.typeface,
|
|
724
|
+
columns: [
|
|
725
|
+
{
|
|
726
|
+
key: "typeface",
|
|
727
|
+
label: "Typeface",
|
|
728
|
+
sortValue: (row) => row.typeface,
|
|
729
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: row.typeface })
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
key: "viewed",
|
|
733
|
+
label: "Viewed",
|
|
734
|
+
numeric: true,
|
|
735
|
+
sortValue: (row) => metricSortValue(row.viewed),
|
|
736
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.viewed, label: `${row.typeface} viewed`, size: 1 })
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
key: "tested",
|
|
740
|
+
label: "Tested",
|
|
741
|
+
numeric: true,
|
|
742
|
+
sortValue: (row) => metricSortValue(row.tested),
|
|
743
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.tested, label: `${row.typeface} tested`, size: 1 })
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
key: "bought",
|
|
747
|
+
label: "Bought",
|
|
748
|
+
numeric: true,
|
|
749
|
+
sortValue: (row) => metricSortValue(row.bought),
|
|
750
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.bought, label: `${row.typeface} bought`, size: 1 })
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
key: "testRate",
|
|
754
|
+
label: "Test rate",
|
|
755
|
+
numeric: true,
|
|
756
|
+
sortValue: (row) => row.testRate,
|
|
757
|
+
render: (row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, "aria-label": row.testRate === null ? `${row.typeface} test rate unavailable` : void 0, children: row.testRate === null ? "\u2014" : formatPercent(row.testRate, 1) })
|
|
758
|
+
}
|
|
759
|
+
]
|
|
760
|
+
}
|
|
761
|
+
)
|
|
762
|
+
] })
|
|
394
763
|
] });
|
|
395
764
|
}
|
|
396
765
|
var CHECK_TONE = {
|
|
@@ -406,15 +775,16 @@ var CHECK_WORD = {
|
|
|
406
775
|
skipped: "Skipped"
|
|
407
776
|
};
|
|
408
777
|
function DiagnosticsPanel({ data }) {
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
const
|
|
778
|
+
const checks = data.checks ?? [];
|
|
779
|
+
const failing = checks.filter((c) => c.status === "fail").length;
|
|
780
|
+
const warning = checks.filter((c) => c.status === "warn").length;
|
|
781
|
+
const summary = checks.length === 0 ? "No checks ran. Nothing here has been verified either way." : data.verdict === "pass" ? "Configuration and credentials check out. That covers the wiring, not whether the figures are worth trusting \u2014 the caveats on each panel still apply." : `${failing} failing, ${warning} worth a look. Panels depending on these will be wrong or incomplete until they are resolved.`;
|
|
412
782
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
|
|
413
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: CHECK_TONE[data.verdict], border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: summary }) }),
|
|
414
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children:
|
|
783
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: CHECK_TONE[data.verdict] ?? "default", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: summary }) }),
|
|
784
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children: checks.map((item) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
415
785
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Flex, { align: "center", justify: "space-between", gap: 3, children: [
|
|
416
786
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: item.label }),
|
|
417
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Badge, { tone: CHECK_TONE[item.status], fontSize: 0, children: CHECK_WORD[item.status] })
|
|
787
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Badge, { tone: CHECK_TONE[item.status] ?? "default", fontSize: 0, children: CHECK_WORD[item.status] ?? item.status })
|
|
418
788
|
] }),
|
|
419
789
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: item.detail }),
|
|
420
790
|
item.remedy && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: item.remedy })
|
|
@@ -424,21 +794,44 @@ function DiagnosticsPanel({ data }) {
|
|
|
424
794
|
|
|
425
795
|
// src/studio/VisitorInsightsTool.tsx
|
|
426
796
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
797
|
+
var tabStrip = {
|
|
798
|
+
display: "flex",
|
|
799
|
+
gap: 2,
|
|
800
|
+
alignItems: "stretch",
|
|
801
|
+
flexWrap: "wrap",
|
|
802
|
+
borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.25))"
|
|
803
|
+
};
|
|
804
|
+
function tabStyle(selected) {
|
|
805
|
+
return {
|
|
806
|
+
appearance: "none",
|
|
807
|
+
background: "transparent",
|
|
808
|
+
border: "none",
|
|
809
|
+
borderBottom: `2px solid ${selected ? "currentColor" : "transparent"}`,
|
|
810
|
+
color: "inherit",
|
|
811
|
+
opacity: selected ? 1 : 0.62,
|
|
812
|
+
font: "inherit",
|
|
813
|
+
fontWeight: selected ? 600 : 400,
|
|
814
|
+
padding: "8px 12px",
|
|
815
|
+
marginBottom: -1,
|
|
816
|
+
cursor: "pointer",
|
|
817
|
+
whiteSpace: "nowrap"
|
|
818
|
+
};
|
|
819
|
+
}
|
|
427
820
|
var RANGES = [
|
|
428
|
-
{ key: "week", label: "
|
|
429
|
-
{ key: "quarter", label: "
|
|
430
|
-
{ key: "year", label: "
|
|
821
|
+
{ key: "week", label: "7 days", span: "the last 7 days" },
|
|
822
|
+
{ key: "quarter", label: "91 days", span: "the last 91 days" },
|
|
823
|
+
{ key: "year", label: "365 days", span: "the last 365 days" }
|
|
431
824
|
];
|
|
432
825
|
var PANELS = [
|
|
433
|
-
{ report: "measurement-health", label: "Measurement health", blurb: "How much of reality each source actually sees" },
|
|
434
826
|
{ report: "acquisition", label: "Acquisition", blurb: "Where visitors come from" },
|
|
435
|
-
{ report: "journey", label: "Journey", blurb: "How far visitors get
|
|
827
|
+
{ report: "journey", label: "Journey", blurb: "How far visitors get" },
|
|
436
828
|
{ report: "typeface-interest", label: "Typeface interest", blurb: "Viewed, tested and bought, by family" },
|
|
829
|
+
{ report: "measurement-health", label: "Measurement health", blurb: "How much of reality each source actually sees" },
|
|
437
830
|
{ report: "diagnostics", label: "Diagnostics", blurb: "What to fix before trusting the numbers above" }
|
|
438
831
|
];
|
|
439
832
|
function RangeSelector({ value, onChange }) {
|
|
440
|
-
const refs = (0,
|
|
441
|
-
const onKeyDown = (0,
|
|
833
|
+
const refs = (0, import_react3.useRef)([]);
|
|
834
|
+
const onKeyDown = (0, import_react3.useCallback)(
|
|
442
835
|
(event) => {
|
|
443
836
|
const currentIndex = RANGES.findIndex((r) => r.key === value);
|
|
444
837
|
let nextIndex = null;
|
|
@@ -469,6 +862,7 @@ function RangeSelector({ value, onChange }) {
|
|
|
469
862
|
mode: selected ? "default" : "bleed",
|
|
470
863
|
tone: selected ? "primary" : "default",
|
|
471
864
|
text: range.label,
|
|
865
|
+
title: `Ending today, covering ${range.span}`,
|
|
472
866
|
onClick: () => onChange(range.key),
|
|
473
867
|
fontSize: 1,
|
|
474
868
|
padding: 3
|
|
@@ -494,8 +888,8 @@ function SourceStatusRow({ sources }) {
|
|
|
494
888
|
] }) });
|
|
495
889
|
}
|
|
496
890
|
function PanelTabs({ value, onChange }) {
|
|
497
|
-
const refs = (0,
|
|
498
|
-
const onKeyDown = (0,
|
|
891
|
+
const refs = (0, import_react3.useRef)([]);
|
|
892
|
+
const onKeyDown = (0, import_react3.useCallback)(
|
|
499
893
|
(event) => {
|
|
500
894
|
const currentIndex = PANELS.findIndex((p) => p.report === value);
|
|
501
895
|
let nextIndex = null;
|
|
@@ -512,27 +906,30 @@ function PanelTabs({ value, onChange }) {
|
|
|
512
906
|
},
|
|
513
907
|
[value, onChange]
|
|
514
908
|
);
|
|
515
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
909
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "tablist", "aria-label": "Report", style: tabStrip, onKeyDown, children: PANELS.map((panel, index) => {
|
|
516
910
|
const selected = panel.report === value;
|
|
517
|
-
return
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
911
|
+
return (
|
|
912
|
+
// A plain button rather than the UI kit's, so the label is real children and
|
|
913
|
+
// cannot vanish: the compat shim's DOM fallback does not forward `text`, which
|
|
914
|
+
// would leave five blank tabs on any Studio version where it falls back.
|
|
915
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
916
|
+
"button",
|
|
917
|
+
{
|
|
918
|
+
type: "button",
|
|
919
|
+
ref: (el) => {
|
|
920
|
+
refs.current[index] = el;
|
|
921
|
+
},
|
|
922
|
+
id: `tab-${panel.report}`,
|
|
923
|
+
role: "tab",
|
|
924
|
+
"aria-selected": selected,
|
|
925
|
+
"aria-controls": `panel-${panel.report}`,
|
|
926
|
+
tabIndex: selected ? 0 : -1,
|
|
927
|
+
style: tabStyle(selected),
|
|
928
|
+
onClick: () => onChange(panel.report),
|
|
929
|
+
children: panel.label
|
|
522
930
|
},
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
"aria-selected": selected,
|
|
526
|
-
"aria-controls": `panel-${panel.report}`,
|
|
527
|
-
tabIndex: selected ? 0 : -1,
|
|
528
|
-
mode: selected ? "default" : "bleed",
|
|
529
|
-
tone: selected ? "primary" : "default",
|
|
530
|
-
text: panel.label,
|
|
531
|
-
onClick: () => onChange(panel.report),
|
|
532
|
-
fontSize: 1,
|
|
533
|
-
padding: 3
|
|
534
|
-
},
|
|
535
|
-
panel.report
|
|
931
|
+
panel.report
|
|
932
|
+
)
|
|
536
933
|
);
|
|
537
934
|
}) });
|
|
538
935
|
}
|
|
@@ -558,12 +955,12 @@ function ReportPanel({ report, apiBaseUrl, range }) {
|
|
|
558
955
|
report === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
|
|
559
956
|
report === "diagnostics" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DiagnosticsPanel, { data: state.envelope.data }),
|
|
560
957
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
|
|
958
|
+
"Figures cover ",
|
|
561
959
|
state.envelope.range.start,
|
|
562
960
|
" to ",
|
|
563
961
|
state.envelope.range.end,
|
|
564
|
-
"
|
|
565
|
-
state.envelope.range.timezone
|
|
566
|
-
")"
|
|
962
|
+
", in ",
|
|
963
|
+
state.envelope.range.timezone
|
|
567
964
|
] })
|
|
568
965
|
] })
|
|
569
966
|
] });
|
|
@@ -571,8 +968,8 @@ function ReportPanel({ report, apiBaseUrl, range }) {
|
|
|
571
968
|
function VisitorInsightsTool(props) {
|
|
572
969
|
const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
|
|
573
970
|
const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
|
|
574
|
-
const [range, setRange] = (0,
|
|
575
|
-
const [activePanel, setActivePanel] = (0,
|
|
971
|
+
const [range, setRange] = (0, import_react3.useState)("week");
|
|
972
|
+
const [activePanel, setActivePanel] = (0, import_react3.useState)("acquisition");
|
|
576
973
|
const active = PANELS.find((p) => p.report === activePanel) ?? PANELS[0];
|
|
577
974
|
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
975
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "flex-start", justify: "space-between", gap: 4, wrap: "wrap", children: [
|