@liiift-studio/sanity-visitor-insights 0.58.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
@@ -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",
@@ -2625,11 +2624,51 @@ function rankLine(series, pick) {
2625
2624
  const ranked = weeklyRank(series.map((d) => ({ date: d.date, value: pick(d) })));
2626
2625
  return ranked ? describeRank(ranked) : null;
2627
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
+ }
2628
2669
  function JourneyPanel({ data }) {
2629
- const [segmentKey, setSegmentKey] = import_react4.default.useState(null);
2630
2670
  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 ?? [];
2671
+ const allSteps = data.steps ?? [];
2633
2672
  const shown = allSteps.filter((step) => step.count.status !== "unavailable");
2634
2673
  const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
2635
2674
  const stages = shown.flatMap(
@@ -2641,44 +2680,8 @@ function JourneyPanel({ data }) {
2641
2680
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked funnel" : "Independent per-step totals" }),
2642
2681
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2643
2682
  ] }) }),
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
- ] }),
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) }),
2682
2685
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2683
2686
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
2684
2687
  (data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -3092,13 +3095,13 @@ function RangeSelector({
3092
3095
  onChange,
3093
3096
  onCustomChange
3094
3097
  }) {
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(() => {
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(() => {
3099
3102
  setDraft(custom);
3100
3103
  }, [custom.start, custom.end]);
3101
- const onKeyDown = (0, import_react5.useCallback)(
3104
+ const onKeyDown = (0, import_react4.useCallback)(
3102
3105
  (event) => {
3103
3106
  const currentIndex = RANGES.findIndex((r) => r.key === value);
3104
3107
  let nextIndex = null;
@@ -3218,8 +3221,8 @@ function SourceStatusRow({ sources }) {
3218
3221
  ] }) });
3219
3222
  }
3220
3223
  function PanelTabs({ value, onChange }) {
3221
- const refs = (0, import_react5.useRef)([]);
3222
- const onKeyDown = (0, import_react5.useCallback)(
3224
+ const refs = (0, import_react4.useRef)([]);
3225
+ const onKeyDown = (0, import_react4.useCallback)(
3223
3226
  (event) => {
3224
3227
  const currentIndex = PANELS.findIndex((p) => p.id === value);
3225
3228
  let nextIndex = null;
@@ -3420,7 +3423,7 @@ function isoDaysAgo(days) {
3420
3423
  return new Date(utcMidnight - days * 864e5).toISOString().slice(0, 10);
3421
3424
  }
3422
3425
  var COMPARED_TABS = ["overview", "acquisition"];
3423
- var PanelBoundary = class extends import_react5.default.Component {
3426
+ var PanelBoundary = class extends import_react4.default.Component {
3424
3427
  constructor(props) {
3425
3428
  super(props);
3426
3429
  this.state = { error: null, shownFor: props.resetKey };
@@ -3459,16 +3462,16 @@ var PanelBoundary = class extends import_react5.default.Component {
3459
3462
  function VisitorInsightsTool(props) {
3460
3463
  const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
3461
3464
  const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
3462
- 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));
3463
3466
  const linkedRange = RANGES.some((r) => r.key === linked.range) ? linked.range : null;
3464
3467
  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) });
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) });
3468
3471
  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(() => {
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(() => {
3472
3475
  if (typeof window === "undefined") return;
3473
3476
  const next = mergeIntoHash(window.location.hash, {
3474
3477
  tab: activePanel,