diff --git a/apps/web/src/components/analytics/analytics-breakdown-panel.tsx b/apps/web/src/components/analytics/analytics-breakdown-panel.tsx index 11d89741a..ae6c5617e 100644 --- a/apps/web/src/components/analytics/analytics-breakdown-panel.tsx +++ b/apps/web/src/components/analytics/analytics-breakdown-panel.tsx @@ -209,14 +209,19 @@ export function AnalyticsBreakdownPanel({ ) return ( -
+ // Its own container, not `/page`: the panel's width depends on both the page + // column and whether the breakdown grid is 1- or 2-up, so the panel itself + // is the only honest measure. The expand dialog portals out of this card — + // `/panel` classes are card-only (`!ranked`); the dialog keeps viewport + // queries, which are truthful there via its `max-w-[92vw]`. +
{tabs}
@@ -432,13 +437,18 @@ function BreakdownTable({ currentKey={sortKey} dir={sortDir} onSort={onSort} + // In the card, Views is the ranking column for pages — Sessions is + // the number a very narrow panel can afford to drop. + hidden={!ranked && hasViews ? "hidden @min-[380px]/panel:flex" : undefined} /> {showShare ? ( label="Share" width="w-16" align="right" - hidden={ranked ? "max-sm:hidden" : undefined} + // Card mode sheds Share by panel width — the row's background bar + // already carries it. The dialog sheds by viewport, as before. + hidden={ranked ? "max-sm:hidden" : "hidden @min-[380px]/panel:flex"} /> ) : null} @@ -462,7 +472,7 @@ function BreakdownTable({ // selected row's background-color instead of overwriting it. style={shareBar(row.share)} className={cn( - "group flex w-full items-center gap-4 border-b border-border/40 px-4 py-2 text-left transition-colors last:border-0", + "group flex w-full items-center gap-3 border-b border-border/40 px-3 py-2 text-left transition-colors last:border-0 @min-[420px]/panel:gap-4 @min-[420px]/panel:px-4", // Hover and focus lift the whole row from behind the fill, so a // long bar and a short one light up by the same amount. // `--foreground`, not `--muted`: muted is barely above this @@ -513,6 +523,7 @@ function BreakdownTable({ "text-right font-mono text-[11px] tabular-nums", ranked ? "w-16 sm:w-24" : hasViews ? "w-20" : "w-24", hasViews && !ranked && "text-muted-foreground", + !ranked && hasViews && "hidden @min-[380px]/panel:inline-block", )} > {formatNumber(row.count)} @@ -521,7 +532,7 @@ function BreakdownTable({ {formatPercent(row.share)} diff --git a/apps/web/src/components/analytics/analytics-metric-strip.tsx b/apps/web/src/components/analytics/analytics-metric-strip.tsx index 5d57c6548..810b14622 100644 --- a/apps/web/src/components/analytics/analytics-metric-strip.tsx +++ b/apps/web/src/components/analytics/analytics-metric-strip.tsx @@ -18,12 +18,17 @@ import { * this grid needs the rule between its two rows at every width. The classes are * otherwise `StatRail`'s exactly. * - * The 4-up break is held to `lg` rather than `md`: eight tiles carrying a value, - * a delta and a sparkline truncate their labels at tablet width where four do - * not. + * The 4-up break is a *container* query, not a viewport one: with both sidebars + * open the content column can be ~512px narrower than the viewport, and a + * viewport `lg:` flipped to four columns inside a space that fits two. 880px is + * the honest budget — each tile carries `px-5`, a no-wrap value ("12m 45s" at + * 26px mono is ~110px), a `w-24` spark slot and the gaps, ~220px apiece. */ const GRID = - "grid grid-cols-2 divide-x divide-y divide-border overflow-hidden rounded-md border bg-card lg:grid-cols-4" + "grid grid-cols-2 divide-x divide-y divide-border overflow-hidden rounded-md border bg-card @min-[880px]/page:grid-cols-4" + +/** Tighter tile chrome once the 2-col grid leaves each tile under ~260px. */ +const TILE_NARROW = "@max-[560px]/page:px-4 @max-[560px]/page:py-3" interface AnalyticsMetricStripProps { source: AnalyticsMetricSource @@ -104,6 +109,8 @@ function MetricTile({ disabled={!available} onSelect={() => onSelect(metric.key)} delay={delay} + className={TILE_NARROW} + valueClassName="@max-[560px]/page:text-[22px]" /> ) } @@ -146,7 +153,7 @@ export function AnalyticsMetricStripLoading() { return (
{ANALYTICS_METRICS.map((metric) => ( -
+
diff --git a/apps/web/src/components/analytics/analytics-traffic-chart.tsx b/apps/web/src/components/analytics/analytics-traffic-chart.tsx index 7567426ea..dd2534c1a 100644 --- a/apps/web/src/components/analytics/analytics-traffic-chart.tsx +++ b/apps/web/src/components/analytics/analytics-traffic-chart.tsx @@ -18,6 +18,7 @@ import { verticalGradient, type PlotTooltipSeries, } from "@maple/ui/components/plot" +import { useMediaQuery } from "@maple/ui/hooks/use-media-query" import { useTheme } from "@maple/ui/hooks/use-theme" import { linkedCursorChartProps } from "@/hooks/use-linked-cursor" @@ -153,6 +154,11 @@ export function AnalyticsTrafficChart({ metric, companion, source, syncId }: Ana [metric, companion, colors, totals], ) + // On a phone the 52px axis gutter is ~15% of the plot; the compact tick labels + // ("1.2k", "45s") fit in 36. Viewport rather than container is honest here — + // on phones the sidebars are sheets, so the two agree. + const narrow = useMediaQuery("max-sm") + const definition = useMemo(() => { const at = (point: TrafficPoint) => point.label // A bucket one table has and the other doesn't is a gap, not a zero — @@ -217,12 +223,12 @@ export function AnalyticsTrafficChart({ metric, companion, source, syncId }: Ana }, }, }, - margin: { left: 52, right: 8, top: 4, bottom: 0 }, + margin: { left: narrow ? 36 : 52, right: 8, top: 4, bottom: 0 }, focus: "group-x", focusRing: false, tooltip: cursorTooltip(focusStore.anchor), }) - }, [data, painted, gradientPrefix, chromeColors, metric, focusStore]) + }, [data, painted, gradientPrefix, chromeColors, metric, focusStore, narrow]) // Only when there are two series to tell apart — a lone series is already // named by the card title, and a legend restating it is one accessory too many. diff --git a/apps/web/src/components/infra/primitives/stat-rail.tsx b/apps/web/src/components/infra/primitives/stat-rail.tsx index 6ea2a48ad..34d45bfd2 100644 --- a/apps/web/src/components/infra/primitives/stat-rail.tsx +++ b/apps/web/src/components/infra/primitives/stat-rail.tsx @@ -51,6 +51,10 @@ interface StatRailItemProps { selected?: boolean /** A tile with nothing to show: still rendered, but not selectable. */ disabled?: boolean + /** Extra classes on the tile shell, e.g. container-query padding overrides. */ + className?: string + /** Extra classes on the value, e.g. a container-query size step-down. */ + valueClassName?: string } /** @@ -78,6 +82,8 @@ export function StatRailItem({ onSelect, selected, disabled, + className, + valueClassName, }: StatRailItemProps) { const body = ( <> @@ -105,6 +111,7 @@ export function StatRailItem({ className={cn( "shrink-0 whitespace-nowrap font-mono text-[26px] font-semibold tabular-nums leading-none tracking-[-0.01em]", VALUE_TONE[tone], + valueClassName, )} > {value} @@ -125,7 +132,7 @@ export function StatRailItem({ ) - const shell = cn("relative px-5 py-4 animate-in fade-in slide-in-from-bottom-1 duration-500") + const shell = cn("relative px-5 py-4 animate-in fade-in slide-in-from-bottom-1 duration-500", className) const style = delay ? { animationDelay: `${delay}ms`, animationFillMode: "backwards" } : undefined if (!onSelect) { diff --git a/apps/web/src/routes/analytics/index.tsx b/apps/web/src/routes/analytics/index.tsx index 504aa6b80..3ee71c81b 100644 --- a/apps/web/src/routes/analytics/index.tsx +++ b/apps/web/src/routes/analytics/index.tsx @@ -367,7 +367,7 @@ function AnalyticsContent({ {Result.builder(breakdownsResult) .onInitial(() => ( -
+
@@ -522,7 +522,7 @@ function AnalyticsContent({ ] return ( -
+
{cards.map((card) => (