Typography round 4 (owner): the snap to the ladder — prose 16.5→16, xxs rung merged into xs

The system: the ladder is the truth, everything snaps to it.

- Prose snaps to the ladder's 16px rung (--lh-prose-size 16.5→16; the
  half-pixel was a reskin-era hand-tune with no visible payoff and a
  system cost). lh-prose, lh-input, and the session titles all read the
  one variable — one reading size, now a ladder citizen. @theme inline
  inlines literals without emitting runtime vars, so the variable can't
  var()-link to --text-lg: documented as the one deliberate duplication
  (change one, change both).
- The 10px rung merges into 11px: one pixel apart was a distinction with
  no visible difference, and 10px sits at the mono legibility floor —
  quiet labels (Sessions, Background agents, dock hints, model-picker
  footers) get their quiet from color + tracking, not a notch of size.
  All 13 text-xxs uses → text-xs; --text-xxs token removed.
- Derived constant follows: TEXT_CAP_SCALE 16.5/12.5 → 16/12.5 (1.32 →
  1.28; test pin 232.32px → 225.28px) — the plan-graph text cap now
  tracks the snapped prose size.

Live: title 16px, Sessions/Background-agents labels 11px, composer 16px
with the 20px lh-input line box. Gates: typecheck, lint, 469/469.

Lane note: components/graph-context-marker.tsx (graph-stream file) had a
mechanical text-xxs → text-xs swap — required: the token removal
otherwise leaves a dead class in their file.
This commit is contained in:
amosblomqvist
2026-09-17 13:43:26 +03:00
parent d01759ec32
commit 2a8554af27
9 changed files with 40 additions and 25 deletions
+1 -1
View File
@@ -274,7 +274,7 @@ export function ActivitySection({ feed: feedProp }: { feed?: ActivityFeed }) {
// alone (the cost span is population, not decoration).
return (
<section data-testid="activity-section" className="border-t border-border px-3 pb-3 pt-2">
<div className="mb-1 flex items-center gap-1.5 text-xxs uppercase tracking-widest text-muted-foreground/70">
<div className="mb-1 flex items-center gap-1.5 text-xs uppercase tracking-widest text-muted-foreground/70">
<ActivityIcon className="size-3" />
Background agents
{blobs.length > 0 && (
+2 -2
View File
@@ -72,7 +72,7 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
)}
>
{/* Owner round 8: a touch smaller than the text (size-4→size-3.5,
≈14px vs the 16.5px row) while centered. */}
≈14px vs the 16px row) while centered. */}
{(key || sel) && <GlyphMark kind={key ? "check" : "x"} className="size-3.5" />}
</span>
)}
@@ -101,7 +101,7 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
{answer.note && (
<div data-testid="your-thinking" className="mt-3 border-l-2 border-primary/40 pl-3">
<span className="mb-0.5 block text-xxs font-medium uppercase tracking-wide text-muted-foreground">
<span className="mb-0.5 block text-xs font-medium uppercase tracking-wide text-muted-foreground">
Your thinking
</span>
<span className="lh-prose block whitespace-pre-wrap text-foreground/90">{answer.note}</span>
+1 -1
View File
@@ -91,7 +91,7 @@ export function GraphContextList({ data }: { data: GraphContextData }): ReactNod
<li key={p.id} className="flex items-baseline gap-2">
<span className="shrink-0 tabular-nums text-muted-foreground">{i + 1}.</span>
{p.level && (
<span className="shrink-0 rounded bg-foreground/10 px-1 py-0.5 text-xxs text-muted-foreground">
<span className="shrink-0 rounded bg-foreground/10 px-1 py-0.5 text-xs text-muted-foreground">
{p.level}
</span>
)}
+4 -4
View File
@@ -190,7 +190,7 @@ export function ModelControl({
className="flex items-center gap-1.5 rounded-full border border-border/60 bg-card/70 px-2.5 py-1 transition-colors hover:border-border hover:bg-muted"
>
<span className="text-card-foreground">{effectiveId ? prettyModel(effectiveId) : "—"}</span>
<span aria-hidden className="text-xxs text-muted-foreground">▾</span>
<span aria-hidden className="text-xs text-muted-foreground">▾</span>
</button>
<button
type="button"
@@ -208,7 +208,7 @@ export function ModelControl({
<span className="text-card-foreground">
{effectiveLevel === undefined ? "—" : (LEVEL_LABEL[effectiveLevel] ?? effectiveLevel)}
</span>
<span aria-hidden className="text-xxs text-muted-foreground">▾</span>
<span aria-hidden className="text-xs text-muted-foreground">▾</span>
</button>
{open === "model" && (
@@ -267,7 +267,7 @@ export function ModelControl({
</li>
))}
</ul>
<div className="mt-1 border-t px-2 pt-1.5 text-xxs text-muted-foreground">
<div className="mt-1 border-t px-2 pt-1.5 text-xs text-muted-foreground">
Applies to the next turn · background agents stay config-pinned
</div>
</div>
@@ -299,7 +299,7 @@ export function ModelControl({
</button>
))}
{error && <div className="mt-1 text-destructive">{error}</div>}
<div className="mt-1 border-t px-2 pt-1.5 text-xxs text-muted-foreground">
<div className="mt-1 border-t px-2 pt-1.5 text-xs text-muted-foreground">
Levels this model actually supports
</div>
</div>
+4 -3
View File
@@ -192,8 +192,9 @@ export const GLOW_BLUR = 5;
export const GLOW_REACH = GLOW_MARGIN + 3 * GLOW_BLUR;
/** Node titles render at text-xs (12.5px) in svg units. The in-chat block
* scales up to fill the column but NEVER past the chat prose size
* (--lh-prose-size, 16.5px) — the width cap that makes that hold. */
export const TEXT_CAP_SCALE = 16.5 / 12.5;
* (--lh-prose-size, 16px — the ladder's reading rung) — the width cap
* that makes that hold. */
export const TEXT_CAP_SCALE = 16 / 12.5;
// ─── The owner's arrowhead (plan-graph round 5) ──────────────────────────────
@@ -808,7 +809,7 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa
/>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-between p-2">
<span className="text-xxs text-muted-foreground/60">Drag to pan · scroll to zoom</span>
<span className="text-xs text-muted-foreground/60">Drag to pan · scroll to zoom</span>
<div className="flex gap-1">
<button
type="button"
+4 -4
View File
@@ -307,7 +307,7 @@ function QuestionDockForm({
)}
>
{isSel && (
<span className={cn(qd.mode === "multi-select" ? "text-xxs" : "size-1.5 rounded-full bg-current")}>
<span className={cn(qd.mode === "multi-select" ? "text-xs" : "size-1.5 rounded-full bg-current")}>
{qd.mode === "multi-select" ? "✓" : ""}
</span>
)}
@@ -373,7 +373,7 @@ function QuestionDockForm({
)}
>
{sel.otherSelected && (
<span className={cn(qd.mode === "multi-select" ? "text-xxs" : "size-1.5 rounded-full bg-current")}>
<span className={cn(qd.mode === "multi-select" ? "text-xs" : "size-1.5 rounded-full bg-current")}>
{qd.mode === "multi-select" ? "✓" : ""}
</span>
)}
@@ -412,7 +412,7 @@ function QuestionDockForm({
data-testid={qd.mode === "text" ? "answer-field" : "thinking-field"}
className="rounded-xl border border-border/70 bg-background/60 transition-colors focus-within:border-primary/50"
>
<div className="px-2.5 pt-2 text-xxs font-medium uppercase tracking-wide text-muted-foreground">
<div className="px-2.5 pt-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
{qd.mode === "text" ? "Your answer" : "Your thinking"}
</div>
{qd.mode === "text" ? (
@@ -455,7 +455,7 @@ function QuestionDockForm({
)}
<div className="flex items-center justify-between gap-2 p-1">
{qd.mode !== "text" && (
<span aria-hidden className="pl-1.5 text-xxs text-muted-foreground">
<span aria-hidden className="pl-1.5 text-xs text-muted-foreground">
⌘/Ctrl + ↵ submits
</span>
)}
+1 -1
View File
@@ -218,7 +218,7 @@ function SessionsSidebar() {
</button>
<div className="mx-3 mb-1 flex items-center gap-2">
<span className="text-xxs uppercase tracking-widest text-muted-foreground/70">Sessions</span>
<span className="text-xs uppercase tracking-widest text-muted-foreground/70">Sessions</span>
<span className="relative flex-1">
<Input
data-testid="session-search"