Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/favicon.ico
Binary file not shown.
6 changes: 6 additions & 0 deletions app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 14 additions & 5 deletions components/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
import { type ReactNode, useId, useState } from "react";
import { Area, AreaChart, CartesianGrid, ComposedChart, Line, Pie, PieChart, Tooltip, XAxis, YAxis } from "recharts";
import { formatDay, formatLongDay, formatValue, relativeChange, type ValueFormat } from "../lib/format.ts";
import { Tooltip as HoverTip } from "./tooltip.tsx";
import { CardEmpty, ChartSwatch, DashboardCard, Delta, InfoTip, SegmentedControl, categoricalColors, chartColors, cn } from "./ui.tsx";

const previousPeriodInfo = "Change versus the previous period of the same length.";
// Dotted underline (a border, since text-decoration skips the inline-flex Delta) marks the change as hoverable.
const deltaTrigger = "inline-flex cursor-help border-b border-dotted border-muted-foreground/60 pb-px outline-none hover:border-muted-foreground focus-visible:rounded-xs focus-visible:ring-2 focus-visible:ring-ring";
const tooltipClass = "min-w-44 max-w-72 rounded-lg border bg-card px-3 py-2.5 text-xs text-card-foreground shadow-popover";

/** Decorative daily trend for stat cards. */
Expand Down Expand Up @@ -64,7 +68,10 @@ export function StatCard({
}) {
return (
<article
className="relative flex min-h-34 flex-col gap-1.5 rounded-2xl bg-card px-4.5 pt-4 pb-3.5 text-card-foreground shadow-soft ring-1 ring-foreground/10"
className={cn(
"relative flex min-h-34 flex-col gap-1.5 rounded-2xl bg-card px-4.5 pt-4 text-card-foreground shadow-soft ring-1 ring-foreground/10",
trend && trend.length > 1 ? "pb-11" : "pb-3.5",
)}
data-testid={`stat-${label.toLowerCase().replace(/ /g, "-")}`}
>
<header className="flex items-center gap-1">
Expand All @@ -75,8 +82,9 @@ export function StatCard({
<div className="relative z-1 flex flex-1 flex-wrap content-start items-center gap-x-1.5 gap-y-0.5 text-xs text-muted-foreground">
{change !== undefined ? (
<>
<Delta inverse={inverse} kind={changeKind} neutral={neutral} value={change} />
<span>vs previous period</span>
<HoverTip className={deltaTrigger} content={previousPeriodInfo}>
<Delta inverse={inverse} kind={changeKind} neutral={neutral} value={change} />
</HoverTip>
</>
) : null}
{hint ? <span>{hint}</span> : null}
Expand Down Expand Up @@ -335,8 +343,9 @@ export function MetricChartCard({
<strong className="numeric text-2xl font-semibold text-foreground">{formatValue(metric.total, metric.format)}</strong>
{metric.previousTotal !== undefined ? (
<>
<Delta neutral={metric.neutral} value={relativeChange(metric.total ?? null, metric.previousTotal)} />
<span>vs previous period</span>
<HoverTip className={deltaTrigger} content={previousPeriodInfo}>
<Delta neutral={metric.neutral} value={relativeChange(metric.total ?? null, metric.previousTotal)} />
</HoverTip>
</>
) : null}
</div>
Expand Down
46 changes: 46 additions & 0 deletions components/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Hover and focus tooltip. Fixed-positioned and clamped to the viewport, so card overflow
// never clips it and it never causes horizontal scroll.
"use client";

import { type ReactNode, useId, useRef, useState } from "react";

const WIDTH = 240;
const GUTTER = 8;

export function Tooltip({ content, children, className }: { content: string; children: ReactNode; className?: string }) {
const id = useId();
const trigger = useRef<HTMLSpanElement>(null);
const [position, setPosition] = useState<{ top: number; left: number } | null>(null);
const show = () => {
const rect = trigger.current?.getBoundingClientRect();
if (!rect) return;
const left = Math.min(Math.max(GUTTER, rect.left + rect.width / 2 - WIDTH / 2), window.innerWidth - WIDTH - GUTTER);
setPosition({ top: rect.bottom + 6, left });
};
const hide = () => setPosition(null);

return (
<span
aria-describedby={position ? id : undefined}
className={className}
onBlur={hide}
onFocus={show}
onMouseEnter={show}
onMouseLeave={hide}
ref={trigger}
tabIndex={0}
>
{children}
{position ? (
<span
className="pointer-events-none fixed z-50 flex justify-center text-left text-xs font-normal normal-case tracking-normal"
id={id}
role="tooltip"
style={{ top: position.top, left: position.left, width: WIDTH }}
>
<span className="rounded-md bg-foreground px-2.5 py-1.5 leading-snug text-background shadow-popover">{content}</span>
</span>
) : null}
</span>
);
}
15 changes: 7 additions & 8 deletions components/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ComponentProps, CSSProperties, ReactNode } from "react";
import type { CrawlerCategory } from "../lib/crawlers.ts";
import { twMerge } from "tailwind-merge";
import { formatValue, type ValueFormat } from "../lib/format.ts";
import { Tooltip } from "./tooltip.tsx";

export function cn(...classes: (string | false | null | undefined)[]): string {
return twMerge(classes);
Expand Down Expand Up @@ -65,20 +66,18 @@ export function Badge({ variant = "secondary", className, ...props }: ComponentP
);
}

/** Small "i" marker whose explanation shows on hover (native title, no tooltip library). */
/** Small "i" marker whose explanation shows on hover or focus. */
export function InfoTip({ children }: { children: string }) {
return (
<span
className="inline-grid size-4.5 flex-none cursor-help place-items-center text-extra-muted-foreground hover:text-primary"
title={children}
aria-label={children}
role="img"
<Tooltip
className="inline-grid size-4.5 flex-none cursor-help place-items-center rounded-full text-extra-muted-foreground outline-none hover:text-primary focus-visible:text-primary focus-visible:ring-2 focus-visible:ring-ring"
content={children}
>
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
<svg aria-label="More info" className="size-4" fill="none" role="img" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6.25" stroke="currentColor" strokeWidth="1.3" />
<path d="M8 7.25v3.5M8 5.25h.01" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" />
</svg>
</span>
</Tooltip>
);
}

Expand Down
Binary file modified public/overview.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading