@liiift-studio/sanity-visitor-insights 0.58.0 → 0.60.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
@@ -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-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';
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-CMADS44I.mjs';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, 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-CMADS44I.mjs';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
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-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';
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-CMADS44I.js';
4
+ export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, 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-CMADS44I.js';
5
5
 
6
6
  /**
7
7
  * Data hook for the Studio panels.
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_react5 = __toESM(require("react"));
78
+ var import_react4 = __toESM(require("react"));
79
79
  var import_sanity_ui_compat4 = require("@liiift-studio/sanity-ui-compat");
80
80
 
81
81
  // src/studio/useReport.ts
@@ -1166,7 +1166,6 @@ var visuallyHidden = {
1166
1166
  var import_sanity_ui_compat5 = require("@liiift-studio/sanity-ui-compat");
1167
1167
 
1168
1168
  // src/studio/panels.tsx
1169
- var import_react4 = __toESM(require("react"));
1170
1169
  var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
1171
1170
 
1172
1171
  // src/studio/CrossSourceTimeline.tsx
@@ -1907,24 +1906,24 @@ var figureRow = {
1907
1906
  gap: 10,
1908
1907
  flexWrap: "wrap"
1909
1908
  };
1910
- var segmentRow = { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" };
1911
- function segmentButton(selected) {
1912
- return {
1913
- appearance: "none",
1914
- background: selected ? "var(--card-bg-color, rgba(128,128,128,0.12))" : "transparent",
1915
- border: `1px solid ${selected ? "currentColor" : "var(--card-border-color, rgba(128,128,128,0.3))"}`,
1916
- borderRadius: 3,
1917
- color: "inherit",
1918
- font: "inherit",
1919
- fontSize: "0.85em",
1920
- padding: "4px 10px",
1921
- cursor: "pointer",
1922
- display: "flex",
1923
- alignItems: "baseline",
1924
- gap: 7
1925
- };
1926
- }
1927
- var segmentRate = { opacity: 0.65, fontVariantNumeric: "tabular-nums" };
1909
+ var segmentHeadCell = {
1910
+ textAlign: "left",
1911
+ fontSize: "0.78em",
1912
+ letterSpacing: "0.06em",
1913
+ textTransform: "uppercase",
1914
+ fontWeight: 500,
1915
+ opacity: 0.7,
1916
+ padding: "8px 12px",
1917
+ borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
1918
+ whiteSpace: "nowrap"
1919
+ };
1920
+ var segmentRowCell = {
1921
+ textAlign: "left",
1922
+ fontWeight: 400,
1923
+ padding: "8px 12px",
1924
+ borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.18))",
1925
+ verticalAlign: "baseline"
1926
+ };
1928
1927
  var sourceLink = {
1929
1928
  color: "inherit",
1930
1929
  textDecoration: "underline",
@@ -2200,7 +2199,7 @@ function OverviewPanel({ data, previous, onBrush }) {
2200
2199
  ] }),
2201
2200
  (data.campaigns?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2202
2201
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Email campaigns" }),
2203
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Clicks are distinct subscribers. Opens are inflated by Apple Mail Privacy Protection, which fetches images on the recipient\u2019s behalf \u2014 so sort on clicks, not opens." }),
2202
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Clicks are distinct subscribers. Opens are not shown: Apple Mail fetches images on the recipient\u2019s behalf, so an open is often the mail client rather than a person." }),
2204
2203
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2205
2204
  "The last two columns count what your order book records in the ",
2206
2205
  SEND_WINDOW_DAYS,
@@ -2227,7 +2226,11 @@ function OverviewPanel({ data, previous, onBrush }) {
2227
2226
  ] })
2228
2227
  },
2229
2228
  { key: "sent", label: "Sent", numeric: true, sortValue: (c) => c.sent, render: (c) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(c.sent) }) },
2230
- { key: "opens", label: "Opens", numeric: true, sortValue: (c) => c.opens, render: (c) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: formatCount(c.opens) }) },
2229
+ // No Opens column. The caption above the table told the reader, in the tool's
2230
+ // own words, to "sort on clicks, not opens" because Apple Mail Privacy
2231
+ // Protection fetches images on the recipient's behalf. A column whose own
2232
+ // caption instructs you not to read it should not be a column — and the
2233
+ // warning gets shorter as a result.
2231
2234
  { key: "clicks", label: "Clicks", numeric: true, sortValue: (c) => c.clicks, render: (c) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(c.clicks) }) },
2232
2235
  {
2233
2236
  key: "unsub",
@@ -2625,11 +2628,51 @@ function rankLine(series, pick) {
2625
2628
  const ranked = weeklyRank(series.map((d) => ({ date: d.date, value: pick(d) })));
2626
2629
  return ranked ? describeRank(ranked) : null;
2627
2630
  }
2631
+ function SegmentTable({ segments, dimension }) {
2632
+ if (segments.length < 2) return null;
2633
+ const spine = [...segments].sort((a, b) => b.steps.length - a.steps.length)[0]?.steps ?? [];
2634
+ if (spine.length === 0) return null;
2635
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2636
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2637
+ "Every ",
2638
+ dimension,
2639
+ " at once. A rate needs ",
2640
+ MIN_RATE_DENOMINATOR,
2641
+ " people at the step before it; below that only the count is shown."
2642
+ ] }),
2643
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { radius: 2, tone: "transparent", border: true, style: { overflowX: "auto", width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse", minWidth: 360 }, children: [
2644
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("caption", { style: visuallyHidden, children: [
2645
+ "Funnel steps by ",
2646
+ dimension
2647
+ ] }),
2648
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2649
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: segmentHeadCell, children: "Step" }),
2650
+ segments.map((segment) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: { ...segmentHeadCell, textAlign: "right" }, children: segment.label }, segment.key))
2651
+ ] }) }),
2652
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: spine.map((step, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2653
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "row", style: segmentRowCell, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: step.label }) }),
2654
+ segments.map((segment) => {
2655
+ const cell = segment.steps.find((s) => s.key === step.key);
2656
+ const count = cell ? metricSortValue(cell.count) : null;
2657
+ const before = index > 0 ? metricSortValue(segment.steps.find((s) => s.key === spine[index - 1].key)?.count) : null;
2658
+ const rate = cell?.conversionFromPrevious ?? null;
2659
+ const showRate = index > 0 && rate !== null && before !== null && before >= MIN_RATE_DENOMINATOR;
2660
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { style: { ...segmentRowCell, textAlign: "right", fontVariantNumeric: "tabular-nums" }, children: count === null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 1, children: [
2661
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(count) }),
2662
+ showRate && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2663
+ formatPercent(rate, 1),
2664
+ " of ",
2665
+ formatCount(before)
2666
+ ] })
2667
+ ] }) }, segment.key);
2668
+ })
2669
+ ] }, step.key)) })
2670
+ ] }) })
2671
+ ] });
2672
+ }
2628
2673
  function JourneyPanel({ data }) {
2629
- const [segmentKey, setSegmentKey] = import_react4.default.useState(null);
2630
2674
  const segments = data.segments ?? [];
2631
- const segment = segmentKey === null ? null : segments.find((row) => row.key === segmentKey) ?? null;
2632
- const allSteps = segment ? segment.steps : data.steps ?? [];
2675
+ const allSteps = data.steps ?? [];
2633
2676
  const shown = allSteps.filter((step) => step.count.status !== "unavailable");
2634
2677
  const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
2635
2678
  const stages = shown.flatMap(
@@ -2641,44 +2684,8 @@ function JourneyPanel({ data }) {
2641
2684
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked funnel" : "Independent per-step totals" }),
2642
2685
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2643
2686
  ] }) }),
2644
- segments.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2645
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: segmentRow, role: "radiogroup", "aria-label": `Funnel by ${data.segmentDimension ?? "segment"}`, children: [
2646
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2647
- "button",
2648
- {
2649
- type: "button",
2650
- role: "radio",
2651
- "aria-checked": segmentKey === null,
2652
- style: segmentButton(segmentKey === null),
2653
- onClick: () => setSegmentKey(null),
2654
- children: "Everyone"
2655
- }
2656
- ),
2657
- segments.map((row) => {
2658
- const rate = firstStepRate(row.steps);
2659
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2660
- "button",
2661
- {
2662
- type: "button",
2663
- role: "radio",
2664
- "aria-checked": segmentKey === row.key,
2665
- style: segmentButton(segmentKey === row.key),
2666
- onClick: () => setSegmentKey(row.key),
2667
- children: [
2668
- row.label,
2669
- rate !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: segmentRate, children: formatPercent(rate, 2) })
2670
- ]
2671
- },
2672
- row.key
2673
- );
2674
- })
2675
- ] }),
2676
- spread(segments) && /* Said in words, once, where the funnel is. The whole reason to split a funnel
2677
- is that the halves differ; when they differ by a multiple rather than a
2678
- margin, the undivided funnel above describes nobody and the reader should
2679
- be told rather than left to press the buttons and notice. */
2680
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) })
2681
- ] }),
2687
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SegmentTable, { segments, dimension: data.segmentDimension ?? "segment" }),
2688
+ spread(segments) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) }),
2682
2689
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2683
2690
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
2684
2691
  (data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -2926,18 +2933,12 @@ function TypefaceInterestPanel({ data }) {
2926
2933
  "\xD7 catalogue"
2927
2934
  ] });
2928
2935
  }
2929
- },
2930
- {
2931
- key: "testRate",
2932
- label: "Test rate",
2933
- numeric: true,
2934
- sortValue: (row) => finiteOrNull(row.testRate),
2935
- exportValue: (row) => {
2936
- const r = finiteOrNull(row.testRate);
2937
- return r === null ? null : formatPercent(r, 1);
2938
- },
2939
- render: (row) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, "aria-label": finiteOrNull(row.testRate) === null ? `${row.typeface} test rate unavailable` : void 0, children: finiteOrNull(row.testRate) === null ? "\u2014" : formatPercent(row.testRate, 1) })
2940
2936
  }
2937
+ // No Test rate column. It was tested ÷ viewed, rendered muted, from the two
2938
+ // columns immediately to its left — and typefaceInterest's own note says that
2939
+ // where a site maps several tester events onto the step (TDF names three) the
2940
+ // numerator is not a person count, so the ratio ranks page layout as much as
2941
+ // interest. A reader who wants it can see both terms side by side.
2941
2942
  ]
2942
2943
  }
2943
2944
  )
@@ -3092,13 +3093,13 @@ function RangeSelector({
3092
3093
  onChange,
3093
3094
  onCustomChange
3094
3095
  }) {
3095
- const refs = (0, import_react5.useRef)([]);
3096
- const [pickerOpen, setPickerOpen] = (0, import_react5.useState)(value === "custom");
3097
- const [draft, setDraft] = (0, import_react5.useState)(custom);
3098
- import_react5.default.useEffect(() => {
3096
+ const refs = (0, import_react4.useRef)([]);
3097
+ const [pickerOpen, setPickerOpen] = (0, import_react4.useState)(value === "custom");
3098
+ const [draft, setDraft] = (0, import_react4.useState)(custom);
3099
+ import_react4.default.useEffect(() => {
3099
3100
  setDraft(custom);
3100
3101
  }, [custom.start, custom.end]);
3101
- const onKeyDown = (0, import_react5.useCallback)(
3102
+ const onKeyDown = (0, import_react4.useCallback)(
3102
3103
  (event) => {
3103
3104
  const currentIndex = RANGES.findIndex((r) => r.key === value);
3104
3105
  let nextIndex = null;
@@ -3218,8 +3219,8 @@ function SourceStatusRow({ sources }) {
3218
3219
  ] }) });
3219
3220
  }
3220
3221
  function PanelTabs({ value, onChange }) {
3221
- const refs = (0, import_react5.useRef)([]);
3222
- const onKeyDown = (0, import_react5.useCallback)(
3222
+ const refs = (0, import_react4.useRef)([]);
3223
+ const onKeyDown = (0, import_react4.useCallback)(
3223
3224
  (event) => {
3224
3225
  const currentIndex = PANELS.findIndex((p) => p.id === value);
3225
3226
  let nextIndex = null;
@@ -3420,7 +3421,7 @@ function isoDaysAgo(days) {
3420
3421
  return new Date(utcMidnight - days * 864e5).toISOString().slice(0, 10);
3421
3422
  }
3422
3423
  var COMPARED_TABS = ["overview", "acquisition"];
3423
- var PanelBoundary = class extends import_react5.default.Component {
3424
+ var PanelBoundary = class extends import_react4.default.Component {
3424
3425
  constructor(props) {
3425
3426
  super(props);
3426
3427
  this.state = { error: null, shownFor: props.resetKey };
@@ -3459,16 +3460,16 @@ var PanelBoundary = class extends import_react5.default.Component {
3459
3460
  function VisitorInsightsTool(props) {
3460
3461
  const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
3461
3462
  const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
3462
- const [linked] = (0, import_react5.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
3463
+ const [linked] = (0, import_react4.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
3463
3464
  const linkedRange = RANGES.some((r) => r.key === linked.range) ? linked.range : null;
3464
3465
  const linkedTab = PANELS.some((p) => p.id === linked.tab) ? linked.tab : null;
3465
- const [range, setRange] = (0, import_react5.useState)(linkedRange ?? "month");
3466
- const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react5.useState)(linkedRange ?? "month");
3467
- const [custom, setCustom] = (0, import_react5.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
3466
+ const [range, setRange] = (0, import_react4.useState)(linkedRange ?? "month");
3467
+ const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react4.useState)(linkedRange ?? "month");
3468
+ const [custom, setCustom] = (0, import_react4.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
3468
3469
  const atPresent = custom.end >= isoDaysAgo(0);
3469
- const [compare, setCompare] = (0, import_react5.useState)("previous-period");
3470
- const [activePanel, setActivePanel] = (0, import_react5.useState)(linkedTab ?? "overview");
3471
- import_react5.default.useEffect(() => {
3470
+ const [compare, setCompare] = (0, import_react4.useState)("previous-period");
3471
+ const [activePanel, setActivePanel] = (0, import_react4.useState)(linkedTab ?? "overview");
3472
+ import_react4.default.useEffect(() => {
3472
3473
  if (typeof window === "undefined") return;
3473
3474
  const next = mergeIntoHash(window.location.hash, {
3474
3475
  tab: activePanel,