milestone 23 s3: convert features/lookup/LookupPage.tsx to stylex

This commit is contained in:
2026-08-12 22:44:23 +02:00
parent 1cd8f71a9b
commit ae9f7fb9f3
2 changed files with 165 additions and 46 deletions
+4 -2
View File
@@ -88,6 +88,8 @@ test("fetches nothing until submit, then renders the blocked verdict", async ()
test("defaults the group select to the default group (id 1)", async () => { test("defaults the group select to the default group (id 1)", async () => {
renderPage(); renderPage();
const select = (await screen.findByLabelText("Group")) as HTMLSelectElement; // A RAC Select names its trigger with the current value and then the label, so
expect(select.value).toBe("1"); // the selected group's name is the only thing the trigger shows.
const trigger = await screen.findByRole("button", { name: /Group$/ });
expect(trigger.textContent).toContain("default");
}); });
+161 -44
View File
@@ -1,10 +1,15 @@
import { useState, type FormEvent, type ReactNode } from "react"; import { useState, type FormEvent, type ReactNode } from "react";
import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
import * as stylex from "@stylexjs/stylex";
import { ApiError } from "@/lib/api"; import { ApiError } from "@/lib/api";
import { groupsQuery, lookupQuery } from "@/lib/queries"; import { groupsQuery, lookupQuery } from "@/lib/queries";
import type { Group, LookupResult } from "@/lib/types"; import type { Group, LookupResult } from "@/lib/types";
import { defaultGroupId } from "@/lib/defaultGroup"; import { defaultGroupId } from "@/lib/defaultGroup";
import { inputClass, largePrimaryButtonClass } from "@/ui/classes"; import Select from "@/ui/Select";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
const DARK = "@media (prefers-color-scheme: dark)";
interface Submitted { interface Submitted {
domain: string; domain: string;
@@ -13,10 +18,123 @@ interface Submitted {
interface Verdict { interface Verdict {
label: string; label: string;
className: string; tone: "local" | "blocked" | "forwarded" | "allowed";
description: string; description: string;
} }
const styles = stylex.create({
heading: {
fontSize: "1.5rem",
lineHeight: "2rem",
fontWeight: 600,
},
intro: {
marginTop: "0.5rem",
color: colors.textMuted,
},
form: {
marginTop: "1.5rem",
display: "flex",
maxWidth: "42rem",
flexWrap: "wrap",
alignItems: "flex-end",
gap: "0.75rem",
},
domainField: {
minWidth: "14rem",
flexGrow: 1,
},
fieldLabel: {
display: "block",
fontSize: "0.875rem",
lineHeight: "1.25rem",
fontWeight: 500,
},
note: {
marginTop: "1.5rem",
color: colors.textMuted,
},
error: {
marginTop: "1.5rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.danger,
},
card: {
marginTop: "1.5rem",
borderRadius: "0.25rem",
borderWidth: 1,
borderStyle: "solid",
borderColor: colors.border,
},
banner: {
borderStartStartRadius: "0.25rem",
borderStartEndRadius: "0.25rem",
paddingInline: "1rem",
paddingBlock: "0.75rem",
},
/** Four verdicts need four tints; only "blocked" maps onto a token role. */
local: {
backgroundColor: { default: "oklch(93.2% 0.032 255.585)", [DARK]: "oklch(28.2% 0.091 267.935)" },
color: { default: "oklch(42.4% 0.199 265.638)", [DARK]: "oklch(80.9% 0.105 251.813)" },
},
blocked: {
backgroundColor: { default: "oklch(93.6% 0.032 17.717)", [DARK]: "oklch(25.8% 0.092 26.042)" },
color: { default: "oklch(44.4% 0.177 26.899)", [DARK]: "oklch(80.8% 0.114 19.571)" },
},
forwarded: {
backgroundColor: { default: "oklch(96.2% 0.059 95.617)", [DARK]: "oklch(27.9% 0.077 45.635)" },
color: { default: "oklch(47.3% 0.137 46.201)", [DARK]: "oklch(87.9% 0.169 91.605)" },
},
allowed: {
backgroundColor: { default: "oklch(96.2% 0.044 156.743)", [DARK]: "oklch(26.6% 0.065 152.934)" },
color: { default: "oklch(44.8% 0.119 151.328)", [DARK]: "oklch(87.1% 0.15 154.449)" },
},
verdictLabel: {
fontSize: "1.125rem",
lineHeight: "1.75rem",
fontWeight: 600,
},
verdictDescription: {
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
details: {
paddingInline: "1rem",
paddingBlock: "0.5rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
/** `divide-y`: a hairline between rows, so the first row carries none. */
detailRow: {
display: "flex",
gap: "1rem",
paddingBlock: "0.5rem",
borderTopWidth: { default: 1, ":first-child": 0 },
borderTopStyle: "solid",
borderTopColor: colors.border,
},
detailTerm: {
width: "10rem",
flexShrink: 0,
color: colors.textMuted,
},
detailValue: {
minWidth: 0,
overflowWrap: "break-word",
},
sourceLink: {
color: colors.primaryOnSurface,
textDecorationLine: "underline",
},
});
function toneStyle(tone: Verdict["tone"]) {
if (tone === "local") return styles.local;
if (tone === "blocked") return styles.blocked;
return tone === "forwarded" ? styles.forwarded : styles.allowed;
}
/** /**
* Header priority follows the pipeline order the lookup handler documents * Header priority follows the pipeline order the lookup handler documents
* (PLAN §6): local records answer first, then the block decision, then * (PLAN §6): local records answer first, then the block decision, then
@@ -26,27 +144,27 @@ export function verdictOf(result: LookupResult): Verdict {
if (result.local_records) { if (result.local_records) {
return { return {
label: "Local answer", label: "Local answer",
className: "bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300", tone: "local",
description: "A local record answers this name directly.", description: "A local record answers this name directly.",
}; };
} }
if (result.blocked) { if (result.blocked) {
return { return {
label: "Blocked", label: "Blocked",
className: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300", tone: "blocked",
description: "Queries for this name get a blocked response.", description: "Queries for this name get a blocked response.",
}; };
} }
if (result.forward_zone !== null) { if (result.forward_zone !== null) {
return { return {
label: "Forwarded", label: "Forwarded",
className: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300", tone: "forwarded",
description: `Queries go to the resolver for zone ${result.forward_zone}.`, description: `Queries go to the resolver for zone ${result.forward_zone}.`,
}; };
} }
return { return {
label: "Allowed", label: "Allowed",
className: "bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-300", tone: "allowed",
description: "Queries resolve through the upstream pool.", description: "Queries resolve through the upstream pool.",
}; };
} }
@@ -68,9 +186,9 @@ function errorMessage(error: unknown): string {
function DetailRow({ label, children }: { label: string; children: ReactNode }) { function DetailRow({ label, children }: { label: string; children: ReactNode }) {
return ( return (
<div className="flex gap-4 py-2"> <div {...stylex.props(styles.detailRow)}>
<dt className="w-40 shrink-0 text-zinc-500">{label}</dt> <dt {...stylex.props(styles.detailTerm)}>{label}</dt>
<dd className="min-w-0 break-words">{children}</dd> <dd {...stylex.props(styles.detailValue)}>{children}</dd>
</div> </div>
); );
} }
@@ -80,26 +198,30 @@ function VerdictCard({ result, groups }: { result: LookupResult; groups: Group[]
const groupName = groups.find((group) => group.id === result.group_id)?.name ?? `#${result.group_id}`; const groupName = groups.find((group) => group.id === result.group_id)?.name ?? `#${result.group_id}`;
return ( return (
<div className="mt-6 rounded border border-zinc-200 dark:border-zinc-800"> <div {...stylex.props(styles.card)}>
<div className={`rounded-t px-4 py-3 ${verdict.className}`}> <div {...stylex.props(styles.banner, toneStyle(verdict.tone))}>
<h2 className="text-lg font-semibold">{verdict.label}</h2> <h2 {...stylex.props(styles.verdictLabel)}>{verdict.label}</h2>
<p className="text-sm">{verdict.description}</p> <p {...stylex.props(styles.verdictDescription)}>{verdict.description}</p>
</div> </div>
<dl className="divide-y divide-zinc-100 px-4 py-2 text-sm dark:divide-zinc-900"> <dl {...stylex.props(styles.details)}>
<DetailRow label="Domain"> <DetailRow label="Domain">
<span className="font-mono">{result.domain}</span> <span {...stylex.props(shared.mono)}>{result.domain}</span>
</DetailRow> </DetailRow>
<DetailRow label="Group">{groupName}</DetailRow> <DetailRow label="Group">{groupName}</DetailRow>
<DetailRow label="Local record">{result.local_records ? "Yes" : "No"}</DetailRow> <DetailRow label="Local record">{result.local_records ? "Yes" : "No"}</DetailRow>
<DetailRow label="Forward zone"> <DetailRow label="Forward zone">
{result.forward_zone !== null ? <span className="font-mono">{result.forward_zone}</span> : "—"} {result.forward_zone !== null ? (
<span {...stylex.props(shared.mono)}>{result.forward_zone}</span>
) : (
"—"
)}
</DetailRow> </DetailRow>
<DetailRow label="Blocked">{result.blocked ? "Yes" : "No"}</DetailRow> <DetailRow label="Blocked">{result.blocked ? "Yes" : "No"}</DetailRow>
<DetailRow label="Reason"> <DetailRow label="Reason">
<span className="font-mono">{result.reason}</span> <span {...stylex.props(shared.mono)}>{result.reason}</span>
</DetailRow> </DetailRow>
<DetailRow label="Matched pattern"> <DetailRow label="Matched pattern">
{result.matched !== "" ? <span className="font-mono">{result.matched}</span> : "—"} {result.matched !== "" ? <span {...stylex.props(shared.mono)}>{result.matched}</span> : "—"}
</DetailRow> </DetailRow>
<DetailRow label="Blocklist source"> <DetailRow label="Blocklist source">
{result.source_url !== null ? ( {result.source_url !== null ? (
@@ -107,7 +229,7 @@ function VerdictCard({ result, groups }: { result: LookupResult; groups: Group[]
href={result.source_url} href={result.source_url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-blue-600 underline dark:text-blue-400" {...stylex.props(styles.sourceLink, shared.focusRing)}
> >
{result.source_url} {result.source_url}
</a> </a>
@@ -117,7 +239,7 @@ function VerdictCard({ result, groups }: { result: LookupResult; groups: Group[]
</DetailRow> </DetailRow>
<DetailRow label="Safe search rewrite"> <DetailRow label="Safe search rewrite">
{result.safe_search_rewrite !== null ? ( {result.safe_search_rewrite !== null ? (
<span className="font-mono">{result.safe_search_rewrite}</span> <span {...stylex.props(shared.mono)}>{result.safe_search_rewrite}</span>
) : ( ) : (
"—" "—"
)} )}
@@ -153,13 +275,13 @@ export default function LookupPage() {
return ( return (
<section> <section>
<h1 className="text-2xl font-semibold">Lookup</h1> <h1 {...stylex.props(styles.heading)}>Lookup</h1>
<p className="mt-2 text-zinc-500"> <p {...stylex.props(styles.intro)}>
What the pipeline would do with a domain: local records, forward zones, block decision, safe search. What the pipeline would do with a domain: local records, forward zones, block decision, safe search.
</p> </p>
<form onSubmit={onSubmit} className="mt-6 flex max-w-2xl flex-wrap items-end gap-3"> <form onSubmit={onSubmit} {...stylex.props(styles.form)}>
<div className="min-w-56 grow"> <div {...stylex.props(styles.domainField)}>
<label htmlFor="lookup-domain" className="block text-sm font-medium"> <label htmlFor="lookup-domain" {...stylex.props(styles.fieldLabel)}>
Domain Domain
</label> </label>
<input <input
@@ -168,33 +290,28 @@ export default function LookupPage() {
value={domain} value={domain}
onChange={(event) => setDomain(event.target.value)} onChange={(event) => setDomain(event.target.value)}
placeholder="ads.example.com" placeholder="ads.example.com"
className={inputClass} {...stylex.props(shared.input, shared.focusRing)}
/> />
</div> </div>
<div> <div>
<label htmlFor="lookup-group" className="block text-sm font-medium"> <Select
Group label="Group"
</label> value={String(groupId)}
<select onChange={(value) => setGroupId(Number(value))}
id="lookup-group" options={groups.map((group) => ({ value: String(group.id), label: group.name }))}
value={groupId} />
onChange={(event) => setGroupId(Number(event.target.value))}
className={inputClass}
>
{groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</div> </div>
<button type="submit" className={largePrimaryButtonClass} disabled={lookup.isFetching}> <button
type="submit"
disabled={lookup.isFetching}
{...stylex.props(shared.largePrimaryButton, shared.focusRing)}
>
Look up Look up
</button> </button>
</form> </form>
{lookup.isFetching && <p className="mt-6 text-zinc-500">Looking up</p>} {lookup.isFetching && <p {...stylex.props(styles.note)}>Looking up</p>}
{!lookup.isFetching && lookup.isError && ( {!lookup.isFetching && lookup.isError && (
<p role="alert" className="mt-6 text-sm text-red-600 dark:text-red-400"> <p role="alert" {...stylex.props(styles.error)}>
{errorMessage(lookup.error)} {errorMessage(lookup.error)}
</p> </p>
)} )}