@liiift-studio/sanity-visitor-insights 0.57.0 → 0.59.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
@@ -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
@@ -306,7 +306,8 @@ var MARKS = {
306
306
  "chart.grid": { key: "neutral", alpha: 0.18, role: "furniture", note: "Grid rules" },
307
307
  "bar.fill": { key: "vercel", alpha: 1, role: "data", note: "Every proportion, comparison and funnel bar" },
308
308
  "bar.track": { key: "neutral", alpha: 0.12, role: "furniture", note: "The rail a bar sits in" },
309
- "bar.lost": { key: "revenue", alpha: 1, role: "data", note: "People who did not continue past a funnel rung" }
309
+ "bar.lost": { key: "revenue", alpha: 1, role: "data", note: "People who did not continue past a funnel rung" },
310
+ "bar.seen": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The share of a complete count that the lossy source saw" }
310
311
  };
311
312
  function mark(name) {
312
313
  const m = MARKS[name];
@@ -315,6 +316,33 @@ function mark(name) {
315
316
  return seriesFill(m.key, m.alpha);
316
317
  }
317
318
 
319
+ // src/types.ts
320
+ function estimated(value, low, high, basis) {
321
+ return { status: "estimated", value, low, high, basis };
322
+ }
323
+ function ok(value) {
324
+ return { status: "ok", value };
325
+ }
326
+ function unavailable(reason, detail) {
327
+ return { status: "unavailable", reason, detail };
328
+ }
329
+ function partial(value, coveredFrom, note) {
330
+ return { status: "partial", value, coveredFrom, note };
331
+ }
332
+ function valueOrNull(metric) {
333
+ return metric.status === "unavailable" ? null : metric.value;
334
+ }
335
+ var REPORT_NAMES = [
336
+ "measurement-health",
337
+ "acquisition",
338
+ "journey",
339
+ "typeface-interest",
340
+ "diagnostics"
341
+ ];
342
+ function isReportName(value) {
343
+ return typeof value === "string" && REPORT_NAMES.includes(value);
344
+ }
345
+
318
346
  // src/studio/Figure.tsx
319
347
  var import_jsx_runtime = require("react/jsx-runtime");
320
348
  var REASON_TEXT = {
@@ -773,6 +801,47 @@ function SectionTitle({
773
801
  }) {
774
802
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: sectionHeader, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Heading, { size: tone === "primary" ? 1 : 0, style: tone === "primary" ? primaryTitle : secondaryTitle, children: title }) });
775
803
  }
804
+ function isContainment(whole, part) {
805
+ const w = valueOrNull(whole);
806
+ const p = valueOrNull(part);
807
+ return w !== null && p !== null && w > 0 && p <= w;
808
+ }
809
+ function ContainmentBar({
810
+ wholeLabel,
811
+ whole,
812
+ partLabel,
813
+ part,
814
+ missingLabel
815
+ }) {
816
+ const wholeValue = valueOrNull(whole);
817
+ const partValue = valueOrNull(part);
818
+ if (!isContainment(whole, part)) return null;
819
+ if (wholeValue === null || partValue === null) return null;
820
+ const share = partValue / wholeValue;
821
+ const missing = wholeValue - partValue;
822
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
823
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: barHeader, children: [
824
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: wholeLabel }),
825
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: formatCount(wholeValue) })
826
+ ] }),
827
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: { ...barTrack, background: mark("bar.fill"), height: 14 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { height: "100%", width: `${share * 100}%`, background: mark("bar.seen"), borderRadius: 2 } }) }),
828
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: barHeader, children: [
829
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
830
+ partLabel,
831
+ ": ",
832
+ formatCount(partValue),
833
+ " (",
834
+ formatPercent(share, 0),
835
+ ")"
836
+ ] }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
838
+ formatCount(missing),
839
+ " ",
840
+ missingLabel
841
+ ] })
842
+ ] })
843
+ ] });
844
+ }
776
845
  function SortableTable({
777
846
  caption,
778
847
  columns,
@@ -1097,7 +1166,6 @@ var visuallyHidden = {
1097
1166
  var import_sanity_ui_compat5 = require("@liiift-studio/sanity-ui-compat");
1098
1167
 
1099
1168
  // src/studio/panels.tsx
1100
- var import_react4 = __toESM(require("react"));
1101
1169
  var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
1102
1170
 
1103
1171
  // src/studio/CrossSourceTimeline.tsx
@@ -1812,33 +1880,6 @@ function describeRank(rank) {
1812
1880
  return `${ordinal(rank.rank)} best of the last ${rank.of} weeks`;
1813
1881
  }
1814
1882
 
1815
- // src/types.ts
1816
- function estimated(value, low, high, basis) {
1817
- return { status: "estimated", value, low, high, basis };
1818
- }
1819
- function ok(value) {
1820
- return { status: "ok", value };
1821
- }
1822
- function unavailable(reason, detail) {
1823
- return { status: "unavailable", reason, detail };
1824
- }
1825
- function partial(value, coveredFrom, note) {
1826
- return { status: "partial", value, coveredFrom, note };
1827
- }
1828
- function valueOrNull(metric) {
1829
- return metric.status === "unavailable" ? null : metric.value;
1830
- }
1831
- var REPORT_NAMES = [
1832
- "measurement-health",
1833
- "acquisition",
1834
- "journey",
1835
- "typeface-interest",
1836
- "diagnostics"
1837
- ];
1838
- function isReportName(value) {
1839
- return typeof value === "string" && REPORT_NAMES.includes(value);
1840
- }
1841
-
1842
1883
  // src/studio/panels.tsx
1843
1884
  var import_jsx_runtime3 = require("react/jsx-runtime");
1844
1885
  function metricSortValue(metric) {
@@ -1865,24 +1906,24 @@ var figureRow = {
1865
1906
  gap: 10,
1866
1907
  flexWrap: "wrap"
1867
1908
  };
1868
- var segmentRow = { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" };
1869
- function segmentButton(selected) {
1870
- return {
1871
- appearance: "none",
1872
- background: selected ? "var(--card-bg-color, rgba(128,128,128,0.12))" : "transparent",
1873
- border: `1px solid ${selected ? "currentColor" : "var(--card-border-color, rgba(128,128,128,0.3))"}`,
1874
- borderRadius: 3,
1875
- color: "inherit",
1876
- font: "inherit",
1877
- fontSize: "0.85em",
1878
- padding: "4px 10px",
1879
- cursor: "pointer",
1880
- display: "flex",
1881
- alignItems: "baseline",
1882
- gap: 7
1883
- };
1884
- }
1885
- 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
+ };
1886
1927
  var sourceLink = {
1887
1928
  color: "inherit",
1888
1929
  textDecoration: "underline",
@@ -2269,12 +2310,24 @@ function Verdict({ data, previous }) {
2269
2310
  }
2270
2311
  function DataHealthPanel({ data, diagnostics }) {
2271
2312
  const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
2313
+ const contained = isContainment(data.vercelPageviews, data.ga4Pageviews);
2272
2314
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
2273
2315
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2274
2316
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Pageviews, source against source" }),
2275
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.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." }),
2276
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax, outOf: "Blocked less than GA4, but not immune to it." }),
2277
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax, outOf: "A subset of the bar above, not a rival measurement." })
2317
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Both counting pageviews. Google Analytics is blockable, so seeing fewer is normal." }),
2318
+ contained ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2319
+ ContainmentBar,
2320
+ {
2321
+ wholeLabel: "Pageviews Vercel counted",
2322
+ whole: data.vercelPageviews,
2323
+ partLabel: "Seen by Google Analytics",
2324
+ part: data.ga4Pageviews,
2325
+ missingLabel: "it missed"
2326
+ }
2327
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2328
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax }),
2329
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax })
2330
+ ] })
2278
2331
  ] }),
2279
2332
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
2280
2333
  data.shortfallRatio !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "semibold", children: data.shortfallRatio >= 0 ? `GA4 saw ${formatPercent(data.shortfallRatio, 1)} fewer pageviews than Vercel` : `GA4 saw ${formatPercent(-data.shortfallRatio, 1)} more pageviews than Vercel` }),
@@ -2571,11 +2624,51 @@ function rankLine(series, pick) {
2571
2624
  const ranked = weeklyRank(series.map((d) => ({ date: d.date, value: pick(d) })));
2572
2625
  return ranked ? describeRank(ranked) : null;
2573
2626
  }
2627
+ function SegmentTable({ segments, dimension }) {
2628
+ if (segments.length < 2) return null;
2629
+ const spine = [...segments].sort((a, b) => b.steps.length - a.steps.length)[0]?.steps ?? [];
2630
+ if (spine.length === 0) return null;
2631
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2632
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2633
+ "Every ",
2634
+ dimension,
2635
+ " at once. A rate needs ",
2636
+ MIN_RATE_DENOMINATOR,
2637
+ " people at the step before it; below that only the count is shown."
2638
+ ] }),
2639
+ /* @__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: [
2640
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("caption", { style: visuallyHidden, children: [
2641
+ "Funnel steps by ",
2642
+ dimension
2643
+ ] }),
2644
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2645
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: segmentHeadCell, children: "Step" }),
2646
+ segments.map((segment) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: { ...segmentHeadCell, textAlign: "right" }, children: segment.label }, segment.key))
2647
+ ] }) }),
2648
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: spine.map((step, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2649
+ /* @__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 }) }),
2650
+ segments.map((segment) => {
2651
+ const cell = segment.steps.find((s) => s.key === step.key);
2652
+ const count = cell ? metricSortValue(cell.count) : null;
2653
+ const before = index > 0 ? metricSortValue(segment.steps.find((s) => s.key === spine[index - 1].key)?.count) : null;
2654
+ const rate = cell?.conversionFromPrevious ?? null;
2655
+ const showRate = index > 0 && rate !== null && before !== null && before >= MIN_RATE_DENOMINATOR;
2656
+ 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: [
2657
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(count) }),
2658
+ showRate && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2659
+ formatPercent(rate, 1),
2660
+ " of ",
2661
+ formatCount(before)
2662
+ ] })
2663
+ ] }) }, segment.key);
2664
+ })
2665
+ ] }, step.key)) })
2666
+ ] }) })
2667
+ ] });
2668
+ }
2574
2669
  function JourneyPanel({ data }) {
2575
- const [segmentKey, setSegmentKey] = import_react4.default.useState(null);
2576
2670
  const segments = data.segments ?? [];
2577
- const segment = segmentKey === null ? null : segments.find((row) => row.key === segmentKey) ?? null;
2578
- const allSteps = segment ? segment.steps : data.steps ?? [];
2671
+ const allSteps = data.steps ?? [];
2579
2672
  const shown = allSteps.filter((step) => step.count.status !== "unavailable");
2580
2673
  const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
2581
2674
  const stages = shown.flatMap(
@@ -2587,44 +2680,8 @@ function JourneyPanel({ data }) {
2587
2680
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked funnel" : "Independent per-step totals" }),
2588
2681
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2589
2682
  ] }) }),
2590
- segments.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2591
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: segmentRow, role: "radiogroup", "aria-label": `Funnel by ${data.segmentDimension ?? "segment"}`, children: [
2592
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2593
- "button",
2594
- {
2595
- type: "button",
2596
- role: "radio",
2597
- "aria-checked": segmentKey === null,
2598
- style: segmentButton(segmentKey === null),
2599
- onClick: () => setSegmentKey(null),
2600
- children: "Everyone"
2601
- }
2602
- ),
2603
- segments.map((row) => {
2604
- const rate = firstStepRate(row.steps);
2605
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2606
- "button",
2607
- {
2608
- type: "button",
2609
- role: "radio",
2610
- "aria-checked": segmentKey === row.key,
2611
- style: segmentButton(segmentKey === row.key),
2612
- onClick: () => setSegmentKey(row.key),
2613
- children: [
2614
- row.label,
2615
- rate !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: segmentRate, children: formatPercent(rate, 2) })
2616
- ]
2617
- },
2618
- row.key
2619
- );
2620
- })
2621
- ] }),
2622
- spread(segments) && /* Said in words, once, where the funnel is. The whole reason to split a funnel
2623
- is that the halves differ; when they differ by a multiple rather than a
2624
- margin, the undivided funnel above describes nobody and the reader should
2625
- be told rather than left to press the buttons and notice. */
2626
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) })
2627
- ] }),
2683
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SegmentTable, { segments, dimension: data.segmentDimension ?? "segment" }),
2684
+ spread(segments) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) }),
2628
2685
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2629
2686
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
2630
2687
  (data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -3038,13 +3095,13 @@ function RangeSelector({
3038
3095
  onChange,
3039
3096
  onCustomChange
3040
3097
  }) {
3041
- const refs = (0, import_react5.useRef)([]);
3042
- const [pickerOpen, setPickerOpen] = (0, import_react5.useState)(value === "custom");
3043
- const [draft, setDraft] = (0, import_react5.useState)(custom);
3044
- import_react5.default.useEffect(() => {
3098
+ const refs = (0, import_react4.useRef)([]);
3099
+ const [pickerOpen, setPickerOpen] = (0, import_react4.useState)(value === "custom");
3100
+ const [draft, setDraft] = (0, import_react4.useState)(custom);
3101
+ import_react4.default.useEffect(() => {
3045
3102
  setDraft(custom);
3046
3103
  }, [custom.start, custom.end]);
3047
- const onKeyDown = (0, import_react5.useCallback)(
3104
+ const onKeyDown = (0, import_react4.useCallback)(
3048
3105
  (event) => {
3049
3106
  const currentIndex = RANGES.findIndex((r) => r.key === value);
3050
3107
  let nextIndex = null;
@@ -3164,8 +3221,8 @@ function SourceStatusRow({ sources }) {
3164
3221
  ] }) });
3165
3222
  }
3166
3223
  function PanelTabs({ value, onChange }) {
3167
- const refs = (0, import_react5.useRef)([]);
3168
- const onKeyDown = (0, import_react5.useCallback)(
3224
+ const refs = (0, import_react4.useRef)([]);
3225
+ const onKeyDown = (0, import_react4.useCallback)(
3169
3226
  (event) => {
3170
3227
  const currentIndex = PANELS.findIndex((p) => p.id === value);
3171
3228
  let nextIndex = null;
@@ -3366,7 +3423,7 @@ function isoDaysAgo(days) {
3366
3423
  return new Date(utcMidnight - days * 864e5).toISOString().slice(0, 10);
3367
3424
  }
3368
3425
  var COMPARED_TABS = ["overview", "acquisition"];
3369
- var PanelBoundary = class extends import_react5.default.Component {
3426
+ var PanelBoundary = class extends import_react4.default.Component {
3370
3427
  constructor(props) {
3371
3428
  super(props);
3372
3429
  this.state = { error: null, shownFor: props.resetKey };
@@ -3405,16 +3462,16 @@ var PanelBoundary = class extends import_react5.default.Component {
3405
3462
  function VisitorInsightsTool(props) {
3406
3463
  const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
3407
3464
  const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
3408
- const [linked] = (0, import_react5.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
3465
+ const [linked] = (0, import_react4.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
3409
3466
  const linkedRange = RANGES.some((r) => r.key === linked.range) ? linked.range : null;
3410
3467
  const linkedTab = PANELS.some((p) => p.id === linked.tab) ? linked.tab : null;
3411
- const [range, setRange] = (0, import_react5.useState)(linkedRange ?? "month");
3412
- const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react5.useState)(linkedRange ?? "month");
3413
- const [custom, setCustom] = (0, import_react5.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
3468
+ const [range, setRange] = (0, import_react4.useState)(linkedRange ?? "month");
3469
+ const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react4.useState)(linkedRange ?? "month");
3470
+ const [custom, setCustom] = (0, import_react4.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
3414
3471
  const atPresent = custom.end >= isoDaysAgo(0);
3415
- const [compare, setCompare] = (0, import_react5.useState)("previous-period");
3416
- const [activePanel, setActivePanel] = (0, import_react5.useState)(linkedTab ?? "overview");
3417
- import_react5.default.useEffect(() => {
3472
+ const [compare, setCompare] = (0, import_react4.useState)("previous-period");
3473
+ const [activePanel, setActivePanel] = (0, import_react4.useState)(linkedTab ?? "overview");
3474
+ import_react4.default.useEffect(() => {
3418
3475
  if (typeof window === "undefined") return;
3419
3476
  const next = mergeIntoHash(window.location.hash, {
3420
3477
  tab: activePanel,