@liiift-studio/sanity-visitor-insights 0.9.0 → 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 CHANGED
@@ -200,6 +200,21 @@ var legendRow = {
200
200
  alignItems: "center",
201
201
  flexWrap: "wrap"
202
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
+ };
203
218
  var noticeRow = {
204
219
  display: "flex",
205
220
  gap: 12,
@@ -208,11 +223,30 @@ var noticeRow = {
208
223
  };
209
224
  var noticeBadge = { flex: "0 0 auto" };
210
225
  function NoticeList({ notices }) {
226
+ const [expanded, setExpanded] = import_react2.default.useState(false);
211
227
  if (notices.length === 0) return null;
212
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "grid", gap: 8 }, children: notices.map((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: [
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: [
213
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" }) }),
214
232
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: notice })
215
- ] }) }) }, 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
+ ] });
216
250
  }
217
251
  var CHART = { w: 760, h: 240, left: 52, right: 12, top: 16, bottom: 30 };
218
252
  function niceCeiling(value) {
@@ -393,6 +427,104 @@ function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
393
427
  ] })
394
428
  ] });
395
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
+ };
396
528
 
397
529
  // src/studio/VisitorInsightsTool.tsx
398
530
  var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
@@ -400,23 +532,34 @@ var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
400
532
  // src/studio/panels.tsx
401
533
  var import_sanity_ui_compat2 = require("@liiift-studio/sanity-ui-compat");
402
534
  var import_jsx_runtime2 = require("react/jsx-runtime");
535
+ function metricSortValue(metric) {
536
+ return metric.status === "unavailable" ? null : metric.value;
537
+ }
403
538
  function maxOf(metrics) {
404
539
  return metrics.reduce((max, metric) => metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
405
540
  }
406
- var tableWrap = { overflowX: "auto", width: "100%" };
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
+ }
407
551
  var cardGrid = {
408
552
  display: "grid",
409
553
  gridTemplateColumns: "repeat(auto-fit, minmax(min(14rem, 100%), 1fr))",
410
554
  gap: 16
411
555
  };
412
- var table = { width: "100%", borderCollapse: "collapse", minWidth: 420 };
413
556
  var cell = { padding: "8px 12px", textAlign: "left", borderBottom: "1px solid var(--card-border-color)" };
414
557
  var numericCell = { ...cell, textAlign: "right" };
415
558
  function MeasurementHealthPanel({ data }) {
416
559
  const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
417
560
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
418
561
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
419
- /* @__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" }),
420
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." }),
421
564
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax, tone: "primary" }),
422
565
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax, tone: "default" })
@@ -426,12 +569,12 @@ function MeasurementHealthPanel({ data }) {
426
569
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: data.interpretation })
427
570
  ] }) }),
428
571
  (data.daily?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
429
- /* @__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" }),
430
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." }),
431
574
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrendChart, { points: data.daily ?? [] })
432
575
  ] }),
433
576
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
434
- /* @__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" }),
435
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." }),
436
579
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: cardGrid, children: [
437
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: [
@@ -466,72 +609,157 @@ function AcquisitionPanel({ data }) {
466
609
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 4, children: formatPercent(data.unattributedShare, 1) })
467
610
  ] }) })
468
611
  ] }),
469
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { radius: 2, tone: "transparent", border: true, style: tableWrap, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: table, children: [
470
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("caption", { style: { textAlign: "left", paddingBottom: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Traffic sources by sessions" }) }),
471
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
472
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Source" }) }),
473
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Channel" }) }),
474
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Sessions" }) })
475
- ] }) }),
476
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: (data.rows ?? []).map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
477
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "row", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [
478
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: row.source }),
479
- row.designIndustry && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Badge, { tone: "primary", fontSize: 0, children: "design industry" }),
480
- row.unattributed && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Badge, { tone: "caution", fontSize: 0, children: "unattributed" })
481
- ] }) }),
482
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: row.channel }) }),
483
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(row.sessions) }) })
484
- ] }, `${row.source}-${row.channel}`)) })
485
- ] }) }),
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
+ ] }),
486
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."] })
487
660
  ] });
488
661
  }
489
662
  function JourneyPanel({ data }) {
490
- const max = maxOf((data.steps ?? []).map((step) => step.count));
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));
491
667
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
492
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 }) }),
493
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children: (data.steps ?? []).map((step) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, 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: [
494
670
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ComparisonBar, { label: step.label, metric: step.count, max, tone: "primary" }),
495
- 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: [
496
672
  formatPercent(step.conversionFromPrevious, 1),
497
- " of the previous measurable step"
673
+ " of ",
674
+ shown[index - 1]?.label.toLowerCase()
498
675
  ] })
499
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
+ ] }),
500
682
  (data.topLandingPages?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
501
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, children: "Where sessions began" }),
502
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { radius: 2, tone: "transparent", border: true, style: tableWrap, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: table, children: [
503
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
504
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Page" }) }),
505
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Sessions" }) })
506
- ] }) }),
507
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: (data.topLandingPages ?? []).map((page) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
508
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "row", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: page.path }) }),
509
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(page.sessions) }) })
510
- ] }, page.path)) })
511
- ] }) })
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
+ )
512
708
  ] })
513
709
  ] });
514
710
  }
515
711
  function TypefaceInterestPanel({ data }) {
516
712
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
517
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 }) }),
518
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { radius: 2, tone: "transparent", border: true, style: tableWrap, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: table, children: [
519
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("caption", { style: { textAlign: "left", paddingBottom: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Engagement by typeface" }) }),
520
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
521
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Typeface" }) }),
522
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Viewed" }) }),
523
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Tested" }) }),
524
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Bought" }) }),
525
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "col", style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Test rate" }) })
526
- ] }) }),
527
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: (data.rows ?? []).map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
528
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { scope: "row", style: cell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: row.typeface }) }),
529
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.viewed, label: `${row.typeface} viewed`, size: 1 }) }),
530
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.tested, label: `${row.typeface} tested`, size: 1 }) }),
531
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: row.bought, label: `${row.typeface} bought`, size: 1 }) }),
532
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: numericCell, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: row.testRate === null ? "\u2014" : formatPercent(row.testRate, 1) }) })
533
- ] }, row.typeface)) })
534
- ] }) })
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
+ ] })
535
763
  ] });
536
764
  }
537
765
  var CHECK_TONE = {
@@ -547,15 +775,16 @@ var CHECK_WORD = {
547
775
  skipped: "Skipped"
548
776
  };
549
777
  function DiagnosticsPanel({ data }) {
550
- const failing = data.checks.filter((c) => c.status === "fail").length;
551
- const warning = data.checks.filter((c) => c.status === "warn").length;
552
- const summary = data.verdict === "pass" ? "Everything checked out. The figures in the other panels can be taken at face value." : `${failing} failing, ${warning} worth a look. Panels depending on these will be wrong or incomplete until they are resolved.`;
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.`;
553
782
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
554
- /* @__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 }) }),
555
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Stack, { space: 3, children: data.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: [
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: [
556
785
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Flex, { align: "center", justify: "space-between", gap: 3, children: [
557
786
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: item.label }),
558
- /* @__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 })
559
788
  ] }),
560
789
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: item.detail }),
561
790
  item.remedy && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: item.remedy })
@@ -565,16 +794,39 @@ function DiagnosticsPanel({ data }) {
565
794
 
566
795
  // src/studio/VisitorInsightsTool.tsx
567
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
+ }
568
820
  var RANGES = [
569
- { key: "week", label: "Week" },
570
- { key: "quarter", label: "Quarter" },
571
- { key: "year", label: "Year" }
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" }
572
824
  ];
573
825
  var PANELS = [
574
- { report: "measurement-health", label: "Measurement health", blurb: "How much of reality each source actually sees" },
575
826
  { report: "acquisition", label: "Acquisition", blurb: "Where visitors come from" },
576
- { report: "journey", label: "Journey", blurb: "How far visitors get, and where they stop" },
827
+ { report: "journey", label: "Journey", blurb: "How far visitors get" },
577
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" },
578
830
  { report: "diagnostics", label: "Diagnostics", blurb: "What to fix before trusting the numbers above" }
579
831
  ];
580
832
  function RangeSelector({ value, onChange }) {
@@ -610,6 +862,7 @@ function RangeSelector({ value, onChange }) {
610
862
  mode: selected ? "default" : "bleed",
611
863
  tone: selected ? "primary" : "default",
612
864
  text: range.label,
865
+ title: `Ending today, covering ${range.span}`,
613
866
  onClick: () => onChange(range.key),
614
867
  fontSize: 1,
615
868
  padding: 3
@@ -653,27 +906,30 @@ function PanelTabs({ value, onChange }) {
653
906
  },
654
907
  [value, onChange]
655
908
  );
656
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Flex, { role: "tablist", "aria-label": "Report", gap: 1, wrap: "wrap", onKeyDown, children: PANELS.map((panel, index) => {
909
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "tablist", "aria-label": "Report", style: tabStrip, onKeyDown, children: PANELS.map((panel, index) => {
657
910
  const selected = panel.report === value;
658
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
659
- import_sanity_ui_compat3.Button,
660
- {
661
- ref: (el) => {
662
- refs.current[index] = el;
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
663
930
  },
664
- id: `tab-${panel.report}`,
665
- role: "tab",
666
- "aria-selected": selected,
667
- "aria-controls": `panel-${panel.report}`,
668
- tabIndex: selected ? 0 : -1,
669
- mode: selected ? "default" : "bleed",
670
- tone: selected ? "primary" : "default",
671
- text: panel.label,
672
- onClick: () => onChange(panel.report),
673
- fontSize: 1,
674
- padding: 3
675
- },
676
- panel.report
931
+ panel.report
932
+ )
677
933
  );
678
934
  }) });
679
935
  }
@@ -699,12 +955,12 @@ function ReportPanel({ report, apiBaseUrl, range }) {
699
955
  report === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
700
956
  report === "diagnostics" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DiagnosticsPanel, { data: state.envelope.data }),
701
957
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
958
+ "Figures cover ",
702
959
  state.envelope.range.start,
703
960
  " to ",
704
961
  state.envelope.range.end,
705
- " (",
706
- state.envelope.range.timezone,
707
- ")"
962
+ ", in ",
963
+ state.envelope.range.timezone
708
964
  ] })
709
965
  ] })
710
966
  ] });
@@ -713,7 +969,7 @@ function VisitorInsightsTool(props) {
713
969
  const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
714
970
  const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
715
971
  const [range, setRange] = (0, import_react3.useState)("week");
716
- const [activePanel, setActivePanel] = (0, import_react3.useState)("measurement-health");
972
+ const [activePanel, setActivePanel] = (0, import_react3.useState)("acquisition");
717
973
  const active = PANELS.find((p) => p.report === activePanel) ?? PANELS[0];
718
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: [
719
975
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "flex-start", justify: "space-between", gap: 4, wrap: "wrap", children: [