@liiift-studio/sanity-visitor-insights 0.49.0 → 0.50.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
@@ -656,6 +656,44 @@ function columnIsEmpty(column, rows) {
656
656
  return value === 0 || value === "";
657
657
  });
658
658
  }
659
+ function Section({
660
+ title,
661
+ subtitle,
662
+ tone = "primary",
663
+ collapsible = false,
664
+ defaultOpen = true,
665
+ children
666
+ }) {
667
+ const [open, setOpen] = import_react2.default.useState(defaultOpen);
668
+ const bodyId = import_react2.default.useId();
669
+ const shown = !collapsible || open;
670
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
671
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: sectionHeader, children: [
672
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 1, children: [
673
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Heading, { size: tone === "primary" ? 1 : 0, style: tone === "primary" ? primaryTitle : secondaryTitle, children: title }),
674
+ subtitle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: subtitle })
675
+ ] }),
676
+ collapsible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
677
+ "button",
678
+ {
679
+ type: "button",
680
+ style: sectionToggle,
681
+ "aria-expanded": open,
682
+ "aria-controls": bodyId,
683
+ onClick: () => setOpen((was) => !was),
684
+ children: open ? "Hide" : "Show"
685
+ }
686
+ )
687
+ ] }),
688
+ shown && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { id: bodyId, children })
689
+ ] }) });
690
+ }
691
+ function SectionTitle({
692
+ title,
693
+ tone = "primary"
694
+ }) {
695
+ 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 }) });
696
+ }
659
697
  function SortableTable({
660
698
  caption,
661
699
  columns,
@@ -839,6 +877,40 @@ var tableControls = {
839
877
  gap: 10,
840
878
  flexWrap: "wrap"
841
879
  };
880
+ var sectionHeader = {
881
+ display: "flex",
882
+ alignItems: "flex-start",
883
+ justifyContent: "space-between",
884
+ gap: 12,
885
+ borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.22))",
886
+ paddingBottom: 6
887
+ };
888
+ var primaryTitle = { margin: 0, lineHeight: 1.3 };
889
+ var secondaryTitle = {
890
+ margin: 0,
891
+ lineHeight: 1.3,
892
+ textTransform: "uppercase",
893
+ letterSpacing: "0.07em",
894
+ opacity: 0.75
895
+ };
896
+ var sectionToggle = {
897
+ appearance: "none",
898
+ background: "transparent",
899
+ border: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
900
+ borderRadius: 3,
901
+ color: "inherit",
902
+ font: "inherit",
903
+ fontSize: "0.8em",
904
+ padding: "2px 9px",
905
+ cursor: "pointer",
906
+ flex: "0 0 auto"
907
+ };
908
+ var splitGrid = {
909
+ display: "grid",
910
+ gridTemplateColumns: "repeat(auto-fit, minmax(min(26rem, 100%), 1fr))",
911
+ gap: 24,
912
+ alignItems: "start"
913
+ };
842
914
  var filterInput = {
843
915
  font: "inherit",
844
916
  fontSize: "0.85em",
@@ -1694,7 +1766,6 @@ var OLDER_ROUTE = "This site\u2019s analytics route is older than this figure \u
1694
1766
  function maxOf(metrics) {
1695
1767
  return metrics.reduce((max, metric) => metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
1696
1768
  }
1697
- var sectionHeading = { margin: "0 0 2px", lineHeight: 1.3 };
1698
1769
  function finiteOrNull(value) {
1699
1770
  return typeof value === "number" && Number.isFinite(value) ? value : null;
1700
1771
  }
@@ -1796,7 +1867,7 @@ function OverviewPanel({ data, previous, onBrush }) {
1796
1867
  ] }) })
1797
1868
  ] }),
1798
1869
  (data.crossSource?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
1799
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Everything, on one time axis" }),
1870
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Everything, on one time axis" }),
1800
1871
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Each row has its own scale. Hover a day, or use the control below, to see what each source saw of it." }),
1801
1872
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1802
1873
  CrossSourceTimeline,
@@ -1980,7 +2051,7 @@ function OverviewPanel({ data, previous, onBrush }) {
1980
2051
  )
1981
2052
  ] }),
1982
2053
  (data.campaigns?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
1983
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Email campaigns" }),
2054
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Email campaigns" }),
1984
2055
  /* @__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." }),
1985
2056
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
1986
2057
  "The last two columns count what your order book records in the ",
@@ -2094,7 +2165,7 @@ function DataHealthPanel({ data, diagnostics }) {
2094
2165
  const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
2095
2166
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
2096
2167
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2097
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Pageviews, source against source" }),
2168
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Pageviews, source against source" }),
2098
2169
  /* @__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." }),
2099
2170
  /* @__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." }),
2100
2171
  /* @__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." })
@@ -2104,7 +2175,7 @@ function DataHealthPanel({ data, diagnostics }) {
2104
2175
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: data.interpretation })
2105
2176
  ] }) }),
2106
2177
  ((data.capture?.estimates?.length ?? 0) > 0 || data.estimatedSessions?.status === "estimated") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2107
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "How much GA4 is seeing" }),
2178
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "How much GA4 is seeing" }),
2108
2179
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: (data.capture?.estimates ?? []).map((estimate) => /* @__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: 3, children: [
2109
2180
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: estimate.basis === "orders" ? "Measured against orders" : estimate.basis === "email" ? "Measured against email clicks" : "Measured against Vercel" }),
2110
2181
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatPercent(estimate.rate, 0) }),
@@ -2127,36 +2198,52 @@ function DataHealthPanel({ data, diagnostics }) {
2127
2198
  ] }) })
2128
2199
  ] }),
2129
2200
  Object.keys(data.orderStatuses ?? {}).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2130
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Order statuses in this range" }),
2201
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Order statuses in this range", tone: "secondary" }),
2131
2202
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "What the orders actually say, before any filtering. Use these values to set which statuses count as a sale." }),
2132
2203
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: Object.entries(data.orderStatuses ?? {}).sort((a, b) => b[1] - a[1]).map(([status, count]) => /* @__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: 3, children: [
2133
2204
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: status }),
2134
2205
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 3, children: formatCount(count) })
2135
2206
  ] }) }, status)) })
2136
2207
  ] }),
2137
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2138
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Context" }),
2139
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Different units to the figures above, and to each other." }),
2140
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: cardGrid, children: [
2141
- /* @__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: 3, children: [
2142
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Vercel visitors" }),
2143
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.vercelVisitors, OLDER_ROUTE), label: "Vercel visitors" }),
2144
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel\u2019s own counter. Blocked far less often than Google Analytics, but not never \u2014 so read it as a floor on your real traffic." })
2145
- ] }) }),
2146
- /* @__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: 3, children: [
2147
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "GA4 sessions" }),
2148
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.ga4Sessions, label: "GA4 sessions" })
2149
- ] }) }),
2150
- /* @__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: 3, children: [
2151
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Consent granted" }),
2152
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.consentRate, label: "Consent granted, share of visitors GA4 saw", unit: "percent" })
2153
- ] }) })
2154
- ] })
2155
- ] }),
2156
- diagnostics && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2157
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Configuration" }),
2158
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DiagnosticsPanel, { data: diagnostics })
2159
- ] })
2208
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2209
+ Section,
2210
+ {
2211
+ title: "Context",
2212
+ tone: "secondary",
2213
+ subtitle: "Different units to the figures above, and to each other.",
2214
+ collapsible: true,
2215
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: cardGrid, children: [
2216
+ /* @__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: 3, children: [
2217
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Vercel visitors" }),
2218
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.vercelVisitors, OLDER_ROUTE), label: "Vercel visitors" }),
2219
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel\u2019s own counter. Blocked far less often than Google Analytics, but not never \u2014 so read it as a floor on your real traffic." })
2220
+ ] }) }),
2221
+ /* @__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: 3, children: [
2222
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "GA4 sessions" }),
2223
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.ga4Sessions, label: "GA4 sessions" })
2224
+ ] }) }),
2225
+ /* @__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: 3, children: [
2226
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Consent granted" }),
2227
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.consentRate, label: "Consent granted, share of visitors GA4 saw", unit: "percent" })
2228
+ ] }) })
2229
+ ] })
2230
+ }
2231
+ ),
2232
+ diagnostics && /* Open, and primary. This is the longest block on the tab and the obvious candidate
2233
+ for folding away — but the tab it sits on exists to answer "what should I fix
2234
+ before trusting any of this", and these checks ARE that answer. Folding it would
2235
+ have left Data health opening on a summary of the problem with the solution behind
2236
+ a click. It stays collapsible so a reader who has read it can get it out of the
2237
+ way. */
2238
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2239
+ Section,
2240
+ {
2241
+ title: "Configuration",
2242
+ subtitle: "What this site has wired up, and what it is missing.",
2243
+ collapsible: true,
2244
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DiagnosticsPanel, { data: diagnostics })
2245
+ }
2246
+ )
2160
2247
  ] });
2161
2248
  }
2162
2249
  function AcquisitionPanel({ data, previous }) {
@@ -2211,7 +2298,7 @@ function AcquisitionPanel({ data, previous }) {
2211
2298
  ] }) })
2212
2299
  ] }),
2213
2300
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2214
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Traffic sources" }),
2301
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Traffic sources" }),
2215
2302
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Sort, filter, or exclude a row to see what the rest looks like." }),
2216
2303
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2217
2304
  SortableTable,
@@ -2357,69 +2444,74 @@ function JourneyPanel({ data }) {
2357
2444
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2358
2445
  ] }) }),
2359
2446
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2360
- (data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2361
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Other outcomes" }),
2362
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Successful outcomes that are not a licence sale. Counted over the same window as the funnel, but not a step within it." }),
2363
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: (data.outcomes ?? []).map((outcome) => /* @__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: 3, children: [
2364
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: outcome.label }),
2365
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: outcome.count, label: outcome.label, size: 4 }),
2366
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: outcome.note })
2367
- ] }) }, outcome.key)) })
2447
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
2448
+ (data.outcomes?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2449
+ Section,
2450
+ {
2451
+ title: "Other outcomes",
2452
+ subtitle: "Successful outcomes that are not a licence sale, counted over the same window as the funnel but not a step within it.",
2453
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: (data.outcomes ?? []).map((outcome) => /* @__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: 3, children: [
2454
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: outcome.label }),
2455
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: outcome.count, label: outcome.label, size: 4 }),
2456
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: outcome.note })
2457
+ ] }) }, outcome.key)) })
2458
+ }
2459
+ ),
2460
+ (data.topLandingPages?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Section, { title: "Where sessions began", subtitle: "The page a visit started on, busiest first.", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LandingPagesTable, { rows: data.topLandingPages ?? [] }) })
2368
2461
  ] }),
2369
2462
  hiddenSteps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2370
2463
  "Not shown: ",
2371
2464
  hiddenSteps.map((step) => step.label.toLowerCase()).join(", "),
2372
2465
  " \u2014 not instrumented on this site, so there is no figure to place in the funnel."
2373
- ] }),
2374
- (data.topLandingPages?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2375
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Where sessions began" }),
2376
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2377
- SortableTable,
2378
- {
2379
- caption: "Landing pages by sessions",
2380
- initialSort: "sessions",
2381
- rows: data.topLandingPages ?? [],
2382
- rowKey: (page) => page.path,
2383
- filterOn: (page) => page.path,
2384
- filterPlaceholder: "Filter pages",
2385
- exportName: "landing-pages",
2386
- columns: [
2387
- {
2388
- key: "path",
2389
- label: "Page",
2390
- sortValue: (page) => page.path,
2391
- // The comment here used to describe this fix without applying it: it said
2392
- // the foundry's own URLs were the one table that did not link while
2393
- // referrer hosts did, and then rendered plain text. A comment asserting
2394
- // something the code does not do is worse than no comment.
2395
- render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: page.path, target: "_blank", rel: "noopener noreferrer", style: sourceLink, children: page.path }) })
2396
- },
2397
- {
2398
- key: "sessions",
2399
- label: "Sessions",
2400
- numeric: true,
2401
- sortValue: (page) => page.sessions,
2402
- render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(page.sessions) })
2403
- },
2404
- {
2405
- key: "engagement",
2406
- label: "Engaged",
2407
- numeric: true,
2408
- sortValue: (page) => finiteOrNull(page.engagementRate),
2409
- exportValue: (page) => {
2410
- const r = finiteOrNull(page.engagementRate);
2411
- return r === null ? null : formatPercent(r, 0);
2412
- },
2413
- // Volume alone could not distinguish a page that delivers 200 arrivals
2414
- // which leave from one that feeds the shop.
2415
- render: (page) => finiteOrNull(page.engagementRate) === null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatPercent(page.engagementRate, 0) })
2416
- }
2417
- ]
2418
- }
2419
- )
2420
2466
  ] })
2421
2467
  ] });
2422
2468
  }
2469
+ function LandingPagesTable({ rows }) {
2470
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2471
+ SortableTable,
2472
+ {
2473
+ caption: "Landing pages by sessions",
2474
+ initialSort: "sessions",
2475
+ rows,
2476
+ rowKey: (page) => page.path,
2477
+ filterOn: (page) => page.path,
2478
+ filterPlaceholder: "Filter pages",
2479
+ exportName: "landing-pages",
2480
+ columns: [
2481
+ {
2482
+ key: "path",
2483
+ label: "Page",
2484
+ sortValue: (page) => page.path,
2485
+ // The comment here used to describe this fix without applying it: it said
2486
+ // the foundry's own URLs were the one table that did not link while
2487
+ // referrer hosts did, and then rendered plain text. A comment asserting
2488
+ // something the code does not do is worse than no comment.
2489
+ render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: page.path, target: "_blank", rel: "noopener noreferrer", style: sourceLink, children: page.path }) })
2490
+ },
2491
+ {
2492
+ key: "sessions",
2493
+ label: "Sessions",
2494
+ numeric: true,
2495
+ sortValue: (page) => page.sessions,
2496
+ render: (page) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(page.sessions) })
2497
+ },
2498
+ {
2499
+ key: "engagement",
2500
+ label: "Engaged",
2501
+ numeric: true,
2502
+ sortValue: (page) => finiteOrNull(page.engagementRate),
2503
+ exportValue: (page) => {
2504
+ const r = finiteOrNull(page.engagementRate);
2505
+ return r === null ? null : formatPercent(r, 0);
2506
+ },
2507
+ // Volume alone could not distinguish a page that delivers 200 arrivals
2508
+ // which leave from one that feeds the shop.
2509
+ render: (page) => finiteOrNull(page.engagementRate) === null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatPercent(page.engagementRate, 0) })
2510
+ }
2511
+ ]
2512
+ }
2513
+ );
2514
+ }
2423
2515
  var MIN_FAMILY_VIEWS = 30;
2424
2516
  function catalogueRate(rows) {
2425
2517
  let views = 0;
@@ -2476,7 +2568,7 @@ function TypefaceInterestPanel({ data }) {
2476
2568
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
2477
2569
  /* @__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.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: data.interpretationNote }) }),
2478
2570
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2479
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Engagement by typeface" }),
2571
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Engagement by typeface" }),
2480
2572
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2481
2573
  "Sort by any column. \u201CSells vs catalogue\u201D compares each family\u2019s sales-per-view against the catalogue as a whole \u2014 1.0\xD7 is average, and sorting up finds the families that are looked at and do not sell. It is a comparison between your families, not the share of visitors who buy: Google Analytics sees only a fraction of the views, which moves every family together and so cancels out here. With one or two sales a family will still swing a long way, so read it alongside the order counts beside it.",
2482
2574
  " ",
@@ -2611,7 +2703,7 @@ function TypefaceInterestPanel({ data }) {
2611
2703
  )
2612
2704
  ] }),
2613
2705
  (data.licences?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2614
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "How licences sell" }),
2706
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "How licences sell" }),
2615
2707
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "From the orders themselves, so this is exact and unaffected by whatever GA4 is doing. Tier and term are separate rows, because they are the two variables in the pricing question and a tier that sells well at one year may not at perpetual." }),
2616
2708
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2617
2709
  ProportionChart,