@liiift-studio/sanity-visitor-insights 0.50.0 → 0.52.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/README.md CHANGED
@@ -15,7 +15,7 @@ only as a conversion anchor and a count; no revenue, and no customer data.
15
15
  |---|---|
16
16
  | **Measurement health** | How much of reality does each source actually see? |
17
17
  | **Acquisition** | Where do visitors come from, and how many are design-industry referrals? |
18
- | **Journey** | How far do visitors get, and where do they stop? |
18
+ | **Journey** | How far do visitors get, where do they stop, and is it different on mobile? |
19
19
  | **Typeface interest** | Which families get viewed, tested and bought? |
20
20
  | **Diagnostics** | What to fix before trusting any of the above? |
21
21
 
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as sanity from 'sanity';
2
2
  import React from 'react';
3
- import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CeB8esIA.mjs';
4
- export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CeB8esIA.mjs';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-DBbZ0tg7.mjs';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneySegment, o as JourneyStep, L as LandingPage, p as LicenceTierRow, P as PREEXISTING, q as REPORT_NAMES, r as ReportError, S as SiteAnalyticsConfig, s as SourceName, t as SourceRow, u as SourceStatus, v as TimelineEvent, w as TypefaceInterestRow, U as UnavailableReason, x as coverageForRange, y as estimated, z as isReportName, B as ok, F as partial, G as previousRange, H as resolveRange, I as unavailable, K as validateSiteConfig, N as valueOrNull } from './ranges-DBbZ0tg7.mjs';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
@@ -606,7 +606,6 @@ declare function AcquisitionPanel({ data, previous }: {
606
606
  data: AcquisitionData;
607
607
  previous?: AcquisitionData;
608
608
  }): React.ReactElement;
609
- /** Journey — a funnel, drawn as a tracked sequence or as independent totals, whichever the report used. */
610
609
  declare function JourneyPanel({ data }: {
611
610
  data: JourneyData;
612
611
  }): React.ReactElement;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as sanity from 'sanity';
2
2
  import React from 'react';
3
- import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CeB8esIA.js';
4
- export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CeB8esIA.js';
3
+ import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-DBbZ0tg7.js';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneySegment, o as JourneyStep, L as LandingPage, p as LicenceTierRow, P as PREEXISTING, q as REPORT_NAMES, r as ReportError, S as SiteAnalyticsConfig, s as SourceName, t as SourceRow, u as SourceStatus, v as TimelineEvent, w as TypefaceInterestRow, U as UnavailableReason, x as coverageForRange, y as estimated, z as isReportName, B as ok, F as partial, G as previousRange, H as resolveRange, I as unavailable, K as validateSiteConfig, N as valueOrNull } from './ranges-DBbZ0tg7.js';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
@@ -606,7 +606,6 @@ declare function AcquisitionPanel({ data, previous }: {
606
606
  data: AcquisitionData;
607
607
  previous?: AcquisitionData;
608
608
  }): React.ReactElement;
609
- /** Journey — a funnel, drawn as a tracked sequence or as independent totals, whichever the report used. */
610
609
  declare function JourneyPanel({ data }: {
611
610
  data: JourneyData;
612
611
  }): React.ReactElement;
package/dist/index.js CHANGED
@@ -75,7 +75,7 @@ var import_sanity2 = require("sanity");
75
75
  var import_icons = require("@liiift-studio/sanity-ui-compat/icons");
76
76
 
77
77
  // src/studio/VisitorInsightsTool.tsx
78
- var import_react4 = __toESM(require("react"));
78
+ var import_react5 = __toESM(require("react"));
79
79
  var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
80
80
 
81
81
  // src/studio/useReport.ts
@@ -266,6 +266,30 @@ var SEND_WINDOW_DAYS = 3;
266
266
  // src/studio/Figure.tsx
267
267
  var import_react2 = __toESM(require("react"));
268
268
  var import_sanity_ui_compat = require("@liiift-studio/sanity-ui-compat");
269
+
270
+ // src/studio/palette.ts
271
+ var SERIES = {
272
+ /** The complete, server-side count. The reference every other line is read against. */
273
+ vercel: "#4C8FD0",
274
+ /** GA4's pageviews — the like-for-like counterpart to Vercel's. */
275
+ ga4Pageviews: "#DD6B3F",
276
+ /** GA4's sessions. Warm, like its sibling above: same instrument, different unit. */
277
+ ga4Sessions: "#BC861F",
278
+ /** Orders, from the foundry's own records. Exact. */
279
+ orders: "#2E9E6B",
280
+ /** Revenue, same source, different unit. */
281
+ revenue: "#BF5D9B"
282
+ };
283
+ var REGION_ALPHA = 0.18;
284
+ function seriesFill(key, alpha = REGION_ALPHA) {
285
+ const hex = SERIES[key];
286
+ const r = parseInt(hex.slice(1, 3), 16);
287
+ const g = parseInt(hex.slice(3, 5), 16);
288
+ const b = parseInt(hex.slice(5, 7), 16);
289
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
290
+ }
291
+
292
+ // src/studio/Figure.tsx
269
293
  var import_jsx_runtime = require("react/jsx-runtime");
270
294
  var REASON_TEXT = {
271
295
  not_instrumented: "Not tracked on this site",
@@ -524,7 +548,10 @@ function FunnelChart({ stages, measurement }) {
524
548
  const active = activeKey === stage.key;
525
549
  const delta = previous ? previous.value - stage.value : 0;
526
550
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: funnelItem, children: [
527
- previous && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: funnelGap, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: gapLabel(delta, measurement) }) }),
551
+ previous && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: funnelGap, "aria-hidden": "true", children: [
552
+ measurement === "sequence" && delta > 0 && entry > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: lostTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...lostFill, width: `${Math.max(1, Math.min(1, delta / entry) * 100)}%` } }) }),
553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: gapLabel(delta, measurement) })
554
+ ] }),
528
555
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
529
556
  "div",
530
557
  {
@@ -564,9 +591,30 @@ var funnelList = { listStyle: "none", margin: 0, padding: 0 };
564
591
  var funnelItem = { display: "grid", gap: 4 };
565
592
  var funnelGap = {
566
593
  display: "flex",
594
+ alignItems: "center",
567
595
  justifyContent: "flex-end",
596
+ gap: 8,
568
597
  padding: "4px 2px"
569
598
  };
599
+ var lostTrack = {
600
+ width: "38%",
601
+ maxWidth: 200,
602
+ height: 6,
603
+ borderRadius: 2,
604
+ overflow: "hidden",
605
+ background: "currentColor",
606
+ opacity: 0.1,
607
+ // Right to left: the bar grows back toward the funnel it came out of, which reads as leaving
608
+ // rather than as another quantity accumulating alongside.
609
+ display: "flex",
610
+ justifyContent: "flex-end"
611
+ };
612
+ var lostFill = {
613
+ height: "100%",
614
+ borderRadius: 2,
615
+ background: SERIES.ga4Pageviews,
616
+ opacity: 0.75
617
+ };
570
618
  function funnelStage(active) {
571
619
  return {
572
620
  display: "grid",
@@ -581,10 +629,11 @@ function funnelStage(active) {
581
629
  var barFill = {
582
630
  height: "100%",
583
631
  borderRadius: 2,
584
- // currentColor at an alpha that clears 3:1 against the card in both themes, so the mark works
585
- // from one declaration rather than needing a palette.
586
- background: "currentColor",
587
- opacity: 0.55
632
+ // The reference blue, as everywhere else the tool draws "people who were here". Previously
633
+ // currentColor at 0.55, which made the funnel a stack of grey bars whose only variable was
634
+ // length — legible, and giving the eye nothing to hold on to across five rungs.
635
+ background: SERIES.vercel,
636
+ opacity: 0.85
588
637
  };
589
638
  var barTrack = {
590
639
  height: 8,
@@ -1018,6 +1067,7 @@ var visuallyHidden = {
1018
1067
  var import_sanity_ui_compat5 = require("@liiift-studio/sanity-ui-compat");
1019
1068
 
1020
1069
  // src/studio/panels.tsx
1070
+ var import_react4 = __toESM(require("react"));
1021
1071
  var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
1022
1072
 
1023
1073
  // src/studio/CrossSourceTimeline.tsx
@@ -1026,30 +1076,6 @@ var import_sanity_ui_compat2 = require("@liiift-studio/sanity-ui-compat");
1026
1076
  var import_d3_scale = require("d3-scale");
1027
1077
  var import_d3_shape = require("d3-shape");
1028
1078
  var import_d3_array = require("d3-array");
1029
-
1030
- // src/studio/palette.ts
1031
- var SERIES = {
1032
- /** The complete, server-side count. The reference every other line is read against. */
1033
- vercel: "#4C8FD0",
1034
- /** GA4's pageviews — the like-for-like counterpart to Vercel's. */
1035
- ga4Pageviews: "#DD6B3F",
1036
- /** GA4's sessions. Warm, like its sibling above: same instrument, different unit. */
1037
- ga4Sessions: "#BC861F",
1038
- /** Orders, from the foundry's own records. Exact. */
1039
- orders: "#2E9E6B",
1040
- /** Revenue, same source, different unit. */
1041
- revenue: "#BF5D9B"
1042
- };
1043
- var REGION_ALPHA = 0.18;
1044
- function seriesFill(key, alpha = REGION_ALPHA) {
1045
- const hex = SERIES[key];
1046
- const r = parseInt(hex.slice(1, 3), 16);
1047
- const g = parseInt(hex.slice(3, 5), 16);
1048
- const b = parseInt(hex.slice(5, 7), 16);
1049
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1050
- }
1051
-
1052
- // src/studio/CrossSourceTimeline.tsx
1053
1079
  var import_jsx_runtime2 = require("react/jsx-runtime");
1054
1080
  function colorFor(source, unit, explicit) {
1055
1081
  if (explicit) return explicit;
@@ -1778,6 +1804,24 @@ var figureRow = {
1778
1804
  gap: 10,
1779
1805
  flexWrap: "wrap"
1780
1806
  };
1807
+ var segmentRow = { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" };
1808
+ function segmentButton(selected) {
1809
+ return {
1810
+ appearance: "none",
1811
+ background: selected ? "var(--card-bg-color, rgba(128,128,128,0.12))" : "transparent",
1812
+ border: `1px solid ${selected ? "currentColor" : "var(--card-border-color, rgba(128,128,128,0.3))"}`,
1813
+ borderRadius: 3,
1814
+ color: "inherit",
1815
+ font: "inherit",
1816
+ fontSize: "0.85em",
1817
+ padding: "4px 10px",
1818
+ cursor: "pointer",
1819
+ display: "flex",
1820
+ alignItems: "baseline",
1821
+ gap: 7
1822
+ };
1823
+ }
1824
+ var segmentRate = { opacity: 0.65, fontVariantNumeric: "tabular-nums" };
1781
1825
  var sourceLink = {
1782
1826
  color: "inherit",
1783
1827
  textDecoration: "underline",
@@ -2430,8 +2474,33 @@ function AcquisitionPanel({ data, previous }) {
2430
2474
  data.rowsWithheld && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NoticeList, { notices: ["GA4 withheld some low-traffic rows for privacy, so this list is shorter than reality."] })
2431
2475
  ] });
2432
2476
  }
2477
+ function firstStepRate(steps) {
2478
+ const second = steps[1];
2479
+ if (!second) return null;
2480
+ const rate = second.conversionFromPrevious;
2481
+ return typeof rate === "number" && Number.isFinite(rate) ? rate : null;
2482
+ }
2483
+ function spread(segments) {
2484
+ const MIN_USERS = 200;
2485
+ const RATIO = 3;
2486
+ const rated = segments.map((segment) => ({
2487
+ label: segment.label,
2488
+ rate: firstStepRate(segment.steps),
2489
+ users: metricSortValue(segment.steps[0]?.count) ?? 0
2490
+ })).filter((row) => row.rate !== null && row.rate > 0 && row.users >= MIN_USERS);
2491
+ if (rated.length < 2) return null;
2492
+ const sorted = [...rated].sort((a, b) => b.rate - a.rate);
2493
+ const best = sorted[0];
2494
+ const worst = sorted[sorted.length - 1];
2495
+ const ratio = best.rate / worst.rate;
2496
+ if (ratio < RATIO) return null;
2497
+ return `${worst.label} reaches the second step at ${formatPercent(worst.rate, 2)} against ${best.label}'s ${formatPercent(best.rate, 2)} \u2014 about ${Math.round(ratio)} times worse, on ${formatCount(worst.users)} visitors. The combined funnel above averages the two and describes neither.`;
2498
+ }
2433
2499
  function JourneyPanel({ data }) {
2434
- const allSteps = data.steps ?? [];
2500
+ const [segmentKey, setSegmentKey] = import_react4.default.useState(null);
2501
+ const segments = data.segments ?? [];
2502
+ const segment = segmentKey === null ? null : segments.find((row) => row.key === segmentKey) ?? null;
2503
+ const allSteps = segment ? segment.steps : data.steps ?? [];
2435
2504
  const shown = allSteps.filter((step) => step.count.status !== "unavailable");
2436
2505
  const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
2437
2506
  const stages = shown.flatMap(
@@ -2443,6 +2512,44 @@ function JourneyPanel({ data }) {
2443
2512
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked funnel" : "Independent per-step totals" }),
2444
2513
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2445
2514
  ] }) }),
2515
+ segments.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2516
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: segmentRow, role: "radiogroup", "aria-label": `Funnel by ${data.segmentDimension ?? "segment"}`, children: [
2517
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2518
+ "button",
2519
+ {
2520
+ type: "button",
2521
+ role: "radio",
2522
+ "aria-checked": segmentKey === null,
2523
+ style: segmentButton(segmentKey === null),
2524
+ onClick: () => setSegmentKey(null),
2525
+ children: "Everyone"
2526
+ }
2527
+ ),
2528
+ segments.map((row) => {
2529
+ const rate = firstStepRate(row.steps);
2530
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2531
+ "button",
2532
+ {
2533
+ type: "button",
2534
+ role: "radio",
2535
+ "aria-checked": segmentKey === row.key,
2536
+ style: segmentButton(segmentKey === row.key),
2537
+ onClick: () => setSegmentKey(row.key),
2538
+ children: [
2539
+ row.label,
2540
+ rate !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: segmentRate, children: formatPercent(rate, 2) })
2541
+ ]
2542
+ },
2543
+ row.key
2544
+ );
2545
+ })
2546
+ ] }),
2547
+ spread(segments) && /* Said in words, once, where the funnel is. The whole reason to split a funnel
2548
+ is that the halves differ; when they differ by a multiple rather than a
2549
+ margin, the undivided funnel above describes nobody and the reader should
2550
+ be told rather than left to press the buttons and notice. */
2551
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) })
2552
+ ] }),
2446
2553
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2447
2554
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
2448
2555
  (data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -2853,13 +2960,13 @@ function RangeSelector({
2853
2960
  onChange,
2854
2961
  onCustomChange
2855
2962
  }) {
2856
- const refs = (0, import_react4.useRef)([]);
2857
- const [pickerOpen, setPickerOpen] = (0, import_react4.useState)(value === "custom");
2858
- const [draft, setDraft] = (0, import_react4.useState)(custom);
2859
- import_react4.default.useEffect(() => {
2963
+ const refs = (0, import_react5.useRef)([]);
2964
+ const [pickerOpen, setPickerOpen] = (0, import_react5.useState)(value === "custom");
2965
+ const [draft, setDraft] = (0, import_react5.useState)(custom);
2966
+ import_react5.default.useEffect(() => {
2860
2967
  setDraft(custom);
2861
2968
  }, [custom.start, custom.end]);
2862
- const onKeyDown = (0, import_react4.useCallback)(
2969
+ const onKeyDown = (0, import_react5.useCallback)(
2863
2970
  (event) => {
2864
2971
  const currentIndex = RANGES.findIndex((r) => r.key === value);
2865
2972
  let nextIndex = null;
@@ -2979,8 +3086,8 @@ function SourceStatusRow({ sources }) {
2979
3086
  ] }) });
2980
3087
  }
2981
3088
  function PanelTabs({ value, onChange }) {
2982
- const refs = (0, import_react4.useRef)([]);
2983
- const onKeyDown = (0, import_react4.useCallback)(
3089
+ const refs = (0, import_react5.useRef)([]);
3090
+ const onKeyDown = (0, import_react5.useCallback)(
2984
3091
  (event) => {
2985
3092
  const currentIndex = PANELS.findIndex((p) => p.id === value);
2986
3093
  let nextIndex = null;
@@ -3184,7 +3291,7 @@ function isoDaysAgo(days) {
3184
3291
  return new Date(utcMidnight - days * 864e5).toISOString().slice(0, 10);
3185
3292
  }
3186
3293
  var COMPARED_TABS = ["overview", "acquisition"];
3187
- var PanelBoundary = class extends import_react4.default.Component {
3294
+ var PanelBoundary = class extends import_react5.default.Component {
3188
3295
  constructor(props) {
3189
3296
  super(props);
3190
3297
  this.state = { error: null, shownFor: props.resetKey };
@@ -3223,16 +3330,16 @@ var PanelBoundary = class extends import_react4.default.Component {
3223
3330
  function VisitorInsightsTool(props) {
3224
3331
  const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
3225
3332
  const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
3226
- const [linked] = (0, import_react4.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
3333
+ const [linked] = (0, import_react5.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
3227
3334
  const linkedRange = RANGES.some((r) => r.key === linked.range) ? linked.range : null;
3228
3335
  const linkedTab = PANELS.some((p) => p.id === linked.tab) ? linked.tab : null;
3229
- const [range, setRange] = (0, import_react4.useState)(linkedRange ?? "month");
3230
- const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react4.useState)(linkedRange ?? "month");
3231
- const [custom, setCustom] = (0, import_react4.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
3336
+ const [range, setRange] = (0, import_react5.useState)(linkedRange ?? "month");
3337
+ const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react5.useState)(linkedRange ?? "month");
3338
+ const [custom, setCustom] = (0, import_react5.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
3232
3339
  const atPresent = custom.end >= isoDaysAgo(0);
3233
- const [compare, setCompare] = (0, import_react4.useState)("previous-period");
3234
- const [activePanel, setActivePanel] = (0, import_react4.useState)(linkedTab ?? "overview");
3235
- import_react4.default.useEffect(() => {
3340
+ const [compare, setCompare] = (0, import_react5.useState)("previous-period");
3341
+ const [activePanel, setActivePanel] = (0, import_react5.useState)(linkedTab ?? "overview");
3342
+ import_react5.default.useEffect(() => {
3236
3343
  if (typeof window === "undefined") return;
3237
3344
  const next = mergeIntoHash(window.location.hash, {
3238
3345
  tab: activePanel,