@liiift-studio/sanity-visitor-insights 0.32.0 → 0.34.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.d.mts CHANGED
@@ -341,30 +341,6 @@ interface NoticeListProps {
341
341
  * Rendered in the panel rather than a README: a caveat nobody sees does not prevent a wrong read.
342
342
  */
343
343
  declare function NoticeList({ notices }: NoticeListProps): React.ReactElement | null;
344
- /** Props for TrendChart. */
345
- interface TrendChartProps {
346
- /** Daily points, oldest first. */
347
- points: Array<{
348
- date: string;
349
- ga4: number | null;
350
- vercel: number | null;
351
- }>;
352
- /** Accessible description of what the two lines are. */
353
- label?: string;
354
- }
355
- /**
356
- * Daily GA4 against Vercel, with axes, dated ticks and a hover readout.
357
- *
358
- * The shaded band between the lines is the point of the chart rather than decoration: it IS the
359
- * measurement gap, and seeing where it opens is what distinguishes an incident from a standing
360
- * difference. Darden's opened overnight on 2026-08-24 and stayed open for over a week, which no
361
- * scalar on this panel could have shown.
362
- *
363
- * Inline SVG with no charting library — one dependency-free element in a Studio that already loads
364
- * a great deal, and it renders on the server so the tests can assert on it. Strokes use
365
- * currentColor so the whole thing follows the Studio's theme without a palette of its own.
366
- */
367
- declare function TrendChart({ points, label }: TrendChartProps): React.ReactElement | null;
368
344
  /** One column of a sortable table. */
369
345
  interface SortColumn<Row> {
370
346
  /** Stable key, also used as the sort key. */
@@ -638,4 +614,4 @@ interface VisitorInsightsPluginOptions {
638
614
  */
639
615
  declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
640
616
 
641
- export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
617
+ export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
package/dist/index.d.ts CHANGED
@@ -341,30 +341,6 @@ interface NoticeListProps {
341
341
  * Rendered in the panel rather than a README: a caveat nobody sees does not prevent a wrong read.
342
342
  */
343
343
  declare function NoticeList({ notices }: NoticeListProps): React.ReactElement | null;
344
- /** Props for TrendChart. */
345
- interface TrendChartProps {
346
- /** Daily points, oldest first. */
347
- points: Array<{
348
- date: string;
349
- ga4: number | null;
350
- vercel: number | null;
351
- }>;
352
- /** Accessible description of what the two lines are. */
353
- label?: string;
354
- }
355
- /**
356
- * Daily GA4 against Vercel, with axes, dated ticks and a hover readout.
357
- *
358
- * The shaded band between the lines is the point of the chart rather than decoration: it IS the
359
- * measurement gap, and seeing where it opens is what distinguishes an incident from a standing
360
- * difference. Darden's opened overnight on 2026-08-24 and stayed open for over a week, which no
361
- * scalar on this panel could have shown.
362
- *
363
- * Inline SVG with no charting library — one dependency-free element in a Studio that already loads
364
- * a great deal, and it renders on the server so the tests can assert on it. Strokes use
365
- * currentColor so the whole thing follows the Studio's theme without a palette of its own.
366
- */
367
- declare function TrendChart({ points, label }: TrendChartProps): React.ReactElement | null;
368
344
  /** One column of a sortable table. */
369
345
  interface SortColumn<Row> {
370
346
  /** Stable key, also used as the sort key. */
@@ -638,4 +614,4 @@ interface VisitorInsightsPluginOptions {
638
614
  */
639
615
  declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
640
616
 
641
- export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
617
+ export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
package/dist/index.js CHANGED
@@ -47,7 +47,6 @@ __export(index_exports, {
47
47
  ProportionChart: () => ProportionChart,
48
48
  REPORT_NAMES: () => REPORT_NAMES,
49
49
  SortableTable: () => SortableTable,
50
- TrendChart: () => TrendChart,
51
50
  TypefaceInterestPanel: () => TypefaceInterestPanel,
52
51
  VisitorInsightsTool: () => VisitorInsightsTool,
53
52
  coverageForRange: () => coverageForRange,
@@ -578,18 +577,6 @@ var barTrack = {
578
577
  background: "currentColor",
579
578
  opacity: 0.12
580
579
  };
581
- var chartFrame = { position: "relative", width: "100%" };
582
- var readout = {
583
- position: "absolute",
584
- top: 0,
585
- right: 0,
586
- padding: "6px 10px",
587
- borderRadius: 3,
588
- background: "var(--card-bg-color, rgba(0,0,0,0.55))",
589
- border: "1px solid var(--card-border-color, rgba(128,128,128,0.35))",
590
- pointerEvents: "none",
591
- maxWidth: "70%"
592
- };
593
580
  var barHeader = {
594
581
  display: "flex",
595
582
  alignItems: "center",
@@ -597,12 +584,6 @@ var barHeader = {
597
584
  gap: 12,
598
585
  flexWrap: "wrap"
599
586
  };
600
- var legendRow = {
601
- display: "flex",
602
- gap: 12,
603
- alignItems: "center",
604
- flexWrap: "wrap"
605
- };
606
587
  var disclosure = {
607
588
  appearance: "none",
608
589
  background: "transparent",
@@ -651,185 +632,6 @@ function NoticeList({ notices }) {
651
632
  )
652
633
  ] });
653
634
  }
654
- var CHART = { w: 760, h: 240, left: 52, right: 12, top: 16, bottom: 30 };
655
- function niceCeiling(value) {
656
- if (value <= 0) return 1;
657
- const magnitude = 10 ** Math.floor(Math.log10(value));
658
- const normalised = value / magnitude;
659
- const step = normalised <= 1 ? 1 : normalised <= 2 ? 2 : normalised <= 5 ? 5 : 10;
660
- return step * magnitude;
661
- }
662
- function shortDate(iso) {
663
- const parsed = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
664
- if (Number.isNaN(parsed.getTime())) return iso;
665
- return `${parsed.getUTCDate()} ${parsed.toLocaleString("en-GB", { month: "short", timeZone: "UTC" })}`;
666
- }
667
- function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
668
- const [hovered, setHovered] = import_react2.default.useState(null);
669
- if (points.length < 3) return null;
670
- const values = points.flatMap((p) => [p.ga4, p.vercel]).filter((v) => typeof v === "number");
671
- if (values.length === 0) return null;
672
- const max = niceCeiling(Math.max(...values, 1));
673
- const plotW = CHART.w - CHART.left - CHART.right;
674
- const plotH = CHART.h - CHART.top - CHART.bottom;
675
- const x = (i) => CHART.left + i / Math.max(1, points.length - 1) * plotW;
676
- const y = (v) => CHART.top + plotH - v / max * plotH;
677
- const line = (pick) => {
678
- let d = "";
679
- let penDown = false;
680
- points.forEach((p, i) => {
681
- const v = pick(p);
682
- if (typeof v !== "number") {
683
- penDown = false;
684
- return;
685
- }
686
- d += `${penDown ? "L" : "M"}${x(i).toFixed(1)} ${y(v).toFixed(1)} `;
687
- penDown = true;
688
- });
689
- return d.trim();
690
- };
691
- const bands = [];
692
- let run = [];
693
- const flushRun = () => {
694
- if (run.length > 1) {
695
- const top = run.map((i) => `${x(i).toFixed(1)} ${y(points[i].vercel).toFixed(1)}`);
696
- const bottom = [...run].reverse().map((i) => `${x(i).toFixed(1)} ${y(points[i].ga4).toFixed(1)}`);
697
- bands.push(`M${top.join(" L")} L${bottom.join(" L")} Z`);
698
- }
699
- run = [];
700
- };
701
- points.forEach((p, i) => {
702
- if (typeof p.ga4 === "number" && typeof p.vercel === "number") run.push(i);
703
- else flushRun();
704
- });
705
- flushRun();
706
- const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => ({ value: max * f, y: y(max * f) }));
707
- const labelEvery = Math.max(1, Math.ceil(points.length / 6));
708
- const dateTicks = points.map((p, i) => ({ i, date: p.date })).filter(({ i }) => i % labelEvery === 0 || i === points.length - 1);
709
- const active = hovered !== null ? points[hovered] : null;
710
- const activeGap = active && typeof active.ga4 === "number" && typeof active.vercel === "number" && active.vercel > 0 ? (active.vercel - active.ga4) / active.vercel : null;
711
- const onKeyDown = (event) => {
712
- const last = points.length - 1;
713
- const current = hovered ?? 0;
714
- const move = (next) => {
715
- event.preventDefault();
716
- setHovered(Math.max(0, Math.min(last, next)));
717
- };
718
- if (event.key === "ArrowRight") return move(hovered === null ? 0 : current + 1);
719
- if (event.key === "ArrowLeft") return move(hovered === null ? last : current - 1);
720
- if (event.key === "Home") return move(0);
721
- if (event.key === "End") return move(last);
722
- if (event.key === "Escape") {
723
- setHovered(null);
724
- return;
725
- }
726
- };
727
- const onMove = (event) => {
728
- const box = event.currentTarget.getBoundingClientRect();
729
- if (box.width === 0) return;
730
- const svgX = (event.clientX - box.left) / box.width * CHART.w;
731
- const ratio = (svgX - CHART.left) / plotW;
732
- const index = Math.round(ratio * (points.length - 1));
733
- setHovered(index >= 0 && index < points.length ? index : null);
734
- };
735
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
736
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: chartFrame, children: [
737
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
738
- "svg",
739
- {
740
- viewBox: `0 0 ${CHART.w} ${CHART.h}`,
741
- width: "100%",
742
- style: { display: "block", height: "auto" },
743
- role: "img",
744
- "aria-label": `${label}, ${points[0]?.date} to ${points[points.length - 1]?.date}. Peak ${formatCount(max)} pageviews in a day.`,
745
- onMouseMove: onMove,
746
- onMouseLeave: () => setHovered(null),
747
- onKeyDown,
748
- onBlur: () => setHovered(null),
749
- tabIndex: 0,
750
- "aria-describedby": active ? "trend-readout" : void 0,
751
- children: [
752
- ticks.map((tick) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
753
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
754
- "line",
755
- {
756
- x1: CHART.left,
757
- x2: CHART.w - CHART.right,
758
- y1: tick.y,
759
- y2: tick.y,
760
- stroke: "currentColor",
761
- strokeOpacity: tick.value === 0 ? 0.35 : 0.12,
762
- strokeWidth: "1"
763
- }
764
- ),
765
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
766
- "text",
767
- {
768
- x: CHART.left - 8,
769
- y: tick.y + 4,
770
- textAnchor: "end",
771
- fontSize: "11",
772
- fill: "currentColor",
773
- fillOpacity: "0.55",
774
- children: formatCount(tick.value)
775
- }
776
- )
777
- ] }, tick.value)),
778
- dateTicks.map(({ i, date }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
779
- "text",
780
- {
781
- x: x(i),
782
- y: CHART.h - 10,
783
- textAnchor: "middle",
784
- fontSize: "11",
785
- fill: "currentColor",
786
- fillOpacity: "0.55",
787
- children: shortDate(date)
788
- },
789
- date
790
- )),
791
- bands.map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d, fill: "currentColor", fillOpacity: "0.09", stroke: "none" }, d.slice(0, 24))),
792
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.vercel), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2" }),
793
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.ga4), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2", strokeDasharray: "5 3" }),
794
- hovered !== null && active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
795
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
796
- "line",
797
- {
798
- x1: x(hovered),
799
- x2: x(hovered),
800
- y1: CHART.top,
801
- y2: CHART.top + plotH,
802
- stroke: "currentColor",
803
- strokeOpacity: "0.4",
804
- strokeWidth: "1"
805
- }
806
- ),
807
- typeof active.vercel === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.vercel), r: "4", fill: "currentColor" }),
808
- typeof active.ga4 === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.ga4), r: "4", fill: "currentColor" })
809
- ] })
810
- ]
811
- }
812
- ),
813
- active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: readout, id: "trend-readout", role: "status", "aria-live": "polite", children: [
814
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: shortDate(active.date) }),
815
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: [
816
- "Vercel ",
817
- typeof active.vercel === "number" ? formatCount(active.vercel) : "\u2014",
818
- " \xB7 ",
819
- "GA4 ",
820
- typeof active.ga4 === "number" ? formatCount(active.ga4) : "\u2014",
821
- activeGap !== null ? ` \xB7 gap ${formatPercent(activeGap, 0)}` : ""
822
- ] })
823
- ] })
824
- ] }),
825
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: legendRow, children: [
826
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2501\u2501 Vercel" }),
827
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2504\u2504 GA4" }),
828
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "shaded band is the gap" }),
829
- !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" })
830
- ] })
831
- ] });
832
- }
833
635
  function SortableTable({
834
636
  caption,
835
637
  columns,
@@ -1598,7 +1400,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
1598
1400
  m.label
1599
1401
  ] }, m.label))
1600
1402
  ] }),
1601
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: legendRow2, children: [
1403
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: legendRow, children: [
1602
1404
  series.some((row) => row.shortfall) && // Hover is not available on touch and not reachable by keyboard, so the reveal
1603
1405
  // has an explicit control too. Without it the detail would exist only for people
1604
1406
  // using a mouse.
@@ -1655,7 +1457,7 @@ var revealButton = {
1655
1457
  cursor: "pointer",
1656
1458
  whiteSpace: "nowrap"
1657
1459
  };
1658
- var legendRow2 = {
1460
+ var legendRow = {
1659
1461
  display: "flex",
1660
1462
  gap: 14,
1661
1463
  flexWrap: "wrap",
@@ -2806,15 +2608,21 @@ function PanelTabs({ value, onChange }) {
2806
2608
  }) });
2807
2609
  }
2808
2610
  function CoverageRibbon({ ratio }) {
2809
- if (ratio === null || ratio < 0.2) return null;
2611
+ if (ratio === null) {
2612
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: "Every figure on this tab comes from Google Analytics. How much of your traffic it is seeing has not been checked for this window \u2014 open Overview or Data health to measure it." });
2613
+ }
2614
+ if (ratio < 0.2) return null;
2810
2615
  const seen = Math.round((1 - ratio) * 100);
2616
+ const multiplier = seen > 0 ? Math.round(100 / seen) : null;
2811
2617
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: ratio > 0.6 ? "critical" : "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
2812
- "Every figure on this tab comes from Google Analytics, which is seeing about ",
2618
+ "Every figure on this tab comes from Google Analytics, which is seeing roughly ",
2813
2619
  seen,
2814
- "% of this site\u2019s traffic in this window \u2014 so the real numbers are roughly",
2620
+ "% of this site\u2019s traffic in this window \u2014 so the real numbers are",
2815
2621
  " ",
2816
- seen > 0 ? `${(100 / seen).toFixed(1)}\xD7` : "many times",
2817
- " these. Data health explains why."
2622
+ multiplier === null ? "many times" : `several times`,
2623
+ " these",
2624
+ multiplier !== null && multiplier > 1 ? ` (very roughly ${multiplier}\xD7)` : "",
2625
+ ". Data health explains why, and how uncertain that is."
2818
2626
  ] }) });
2819
2627
  }
2820
2628
  function ReadyReport({
@@ -3287,7 +3095,6 @@ var index_default = visitorInsights;
3287
3095
  ProportionChart,
3288
3096
  REPORT_NAMES,
3289
3097
  SortableTable,
3290
- TrendChart,
3291
3098
  TypefaceInterestPanel,
3292
3099
  VisitorInsightsTool,
3293
3100
  coverageForRange,