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:
+20
-7
@@ -40,7 +40,12 @@
|
|||||||
/* Emphasis coloring in teaching prose — ready for the settings UI. */
|
/* Emphasis coloring in teaching prose — ready for the settings UI. */
|
||||||
--lh-ink-strong: #c96b4a;
|
--lh-ink-strong: #c96b4a;
|
||||||
--lh-ink-italic: #6fa9d6;
|
--lh-ink-italic: #6fa9d6;
|
||||||
--lh-prose-size: 16.5px;
|
/* THE READING SIZE = the ladder's 16px rung (--text-lg). Can't be
|
||||||
|
var()-linked: @theme inline inlines literals into utilities without
|
||||||
|
emitting runtime vars — so this is the one documented duplication:
|
||||||
|
change one, change both. (owner, typography round 4: snapped from
|
||||||
|
the reskin's hand-tuned 16.5 to the ladder's 16.) */
|
||||||
|
--lh-prose-size: 16px;
|
||||||
--lh-prose-leading: 1.5;
|
--lh-prose-leading: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +72,12 @@
|
|||||||
--ring: #8d8d8d;
|
--ring: #8d8d8d;
|
||||||
--lh-ink-strong: #c96b4a;
|
--lh-ink-strong: #c96b4a;
|
||||||
--lh-ink-italic: #6fa9d6;
|
--lh-ink-italic: #6fa9d6;
|
||||||
--lh-prose-size: 16.5px;
|
/* THE READING SIZE = the ladder's 16px rung (--text-lg). Can't be
|
||||||
|
var()-linked: @theme inline inlines literals into utilities without
|
||||||
|
emitting runtime vars — so this is the one documented duplication:
|
||||||
|
change one, change both. (owner, typography round 4: snapped from
|
||||||
|
the reskin's hand-tuned 16.5 to the ladder's 16.) */
|
||||||
|
--lh-prose-size: 16px;
|
||||||
--lh-prose-leading: 1.5;
|
--lh-prose-leading: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +103,12 @@
|
|||||||
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
|
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
|
||||||
--font-sans: var(--font-sans3), ui-sans-serif, system-ui, sans-serif;
|
--font-sans: var(--font-sans3), ui-sans-serif, system-ui, sans-serif;
|
||||||
/* The modular type ladder — each size carries its own line-height.
|
/* The modular type ladder — each size carries its own line-height.
|
||||||
Component sizes must pull from this ladder. */
|
Component sizes must pull from this ladder. (owner, typography
|
||||||
--text-xxs: 10px;
|
round 4: the 10px rung is merged into 11 — one pixel apart was a
|
||||||
--text-xxs--line-height: 1.4;
|
distinction with no visible difference, and 10px sits at the mono
|
||||||
|
legibility floor; quiet labels get their quiet from color +
|
||||||
|
tracking, not a notch of size. Reading prose = the 16px rung via
|
||||||
|
--lh-prose-size.) */
|
||||||
--text-xs: 11px;
|
--text-xs: 11px;
|
||||||
--text-xs--line-height: 1.4;
|
--text-xs--line-height: 1.4;
|
||||||
--text-sm: 12.5px;
|
--text-sm: 12.5px;
|
||||||
@@ -166,9 +179,9 @@ body {
|
|||||||
|
|
||||||
/* lh-input (owner, typography round 2): lh-prose's 1.5 READING leading
|
/* lh-input (owner, typography round 2): lh-prose's 1.5 READING leading
|
||||||
makes typing carets tower over the glyphs — the caret fills the line
|
makes typing carets tower over the glyphs — the caret fills the line
|
||||||
box (24.75px at 16.5px text, ~2.1× the glyph height). Inputs are not
|
box (24px at 16px text, ~2× the glyph height). Inputs are not
|
||||||
paragraphs: a tighter 1.25 leading brings the caret to the font-metric
|
paragraphs: a tighter 1.25 leading brings the caret to the font-metric
|
||||||
floor (~21px, as close to the glyphs as Chromium will draw it) while
|
floor (~20px, as close to the glyphs as Chromium will draw it) while
|
||||||
wrapped lines in growing textareas still read fine. Rendered prose keeps
|
wrapped lines in growing textareas still read fine. Rendered prose keeps
|
||||||
1.5 — the two share family and size, they differ only in leading. */
|
1.5 — the two share family and size, they differ only in leading. */
|
||||||
.lh-input {
|
.lh-input {
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ export function ActivitySection({ feed: feedProp }: { feed?: ActivityFeed }) {
|
|||||||
// alone (the cost span is population, not decoration).
|
// alone (the cost span is population, not decoration).
|
||||||
return (
|
return (
|
||||||
<section data-testid="activity-section" className="border-t border-border px-3 pb-3 pt-2">
|
<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" />
|
<ActivityIcon className="size-3" />
|
||||||
Background agents
|
Background agents
|
||||||
{blobs.length > 0 && (
|
{blobs.length > 0 && (
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Owner round 8: a touch smaller than the text (size-4→size-3.5,
|
{/* 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" />}
|
{(key || sel) && <GlyphMark kind={key ? "check" : "x"} className="size-3.5" />}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -101,7 +101,7 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|||||||
|
|
||||||
{answer.note && (
|
{answer.note && (
|
||||||
<div data-testid="your-thinking" className="mt-3 border-l-2 border-primary/40 pl-3">
|
<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
|
Your thinking
|
||||||
</span>
|
</span>
|
||||||
<span className="lh-prose block whitespace-pre-wrap text-foreground/90">{answer.note}</span>
|
<span className="lh-prose block whitespace-pre-wrap text-foreground/90">{answer.note}</span>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function GraphContextList({ data }: { data: GraphContextData }): ReactNod
|
|||||||
<li key={p.id} className="flex items-baseline gap-2">
|
<li key={p.id} className="flex items-baseline gap-2">
|
||||||
<span className="shrink-0 tabular-nums text-muted-foreground">{i + 1}.</span>
|
<span className="shrink-0 tabular-nums text-muted-foreground">{i + 1}.</span>
|
||||||
{p.level && (
|
{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}
|
{p.level}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"
|
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 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>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -208,7 +208,7 @@ export function ModelControl({
|
|||||||
<span className="text-card-foreground">
|
<span className="text-card-foreground">
|
||||||
{effectiveLevel === undefined ? "—" : (LEVEL_LABEL[effectiveLevel] ?? effectiveLevel)}
|
{effectiveLevel === undefined ? "—" : (LEVEL_LABEL[effectiveLevel] ?? effectiveLevel)}
|
||||||
</span>
|
</span>
|
||||||
<span aria-hidden className="text-xxs text-muted-foreground">▾</span>
|
<span aria-hidden className="text-xs text-muted-foreground">▾</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open === "model" && (
|
{open === "model" && (
|
||||||
@@ -267,7 +267,7 @@ export function ModelControl({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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
|
Applies to the next turn · background agents stay config-pinned
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,7 +299,7 @@ export function ModelControl({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{error && <div className="mt-1 text-destructive">{error}</div>}
|
{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
|
Levels this model actually supports
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,8 +192,9 @@ export const GLOW_BLUR = 5;
|
|||||||
export const GLOW_REACH = GLOW_MARGIN + 3 * GLOW_BLUR;
|
export const GLOW_REACH = GLOW_MARGIN + 3 * GLOW_BLUR;
|
||||||
/** Node titles render at text-xs (12.5px) in svg units. The in-chat block
|
/** 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
|
* 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. */
|
* (--lh-prose-size, 16px — the ladder's reading rung) — the width cap
|
||||||
export const TEXT_CAP_SCALE = 16.5 / 12.5;
|
* that makes that hold. */
|
||||||
|
export const TEXT_CAP_SCALE = 16 / 12.5;
|
||||||
|
|
||||||
// ─── The owner's arrowhead (plan-graph round 5) ──────────────────────────────
|
// ─── The owner's arrowhead (plan-graph round 5) ──────────────────────────────
|
||||||
|
|
||||||
@@ -808,7 +809,7 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-between p-2">
|
<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">
|
<div className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ function QuestionDockForm({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSel && (
|
{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" ? "✓" : ""}
|
{qd.mode === "multi-select" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -373,7 +373,7 @@ function QuestionDockForm({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{sel.otherSelected && (
|
{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" ? "✓" : ""}
|
{qd.mode === "multi-select" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -412,7 +412,7 @@ function QuestionDockForm({
|
|||||||
data-testid={qd.mode === "text" ? "answer-field" : "thinking-field"}
|
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"
|
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"}
|
{qd.mode === "text" ? "Your answer" : "Your thinking"}
|
||||||
</div>
|
</div>
|
||||||
{qd.mode === "text" ? (
|
{qd.mode === "text" ? (
|
||||||
@@ -455,7 +455,7 @@ function QuestionDockForm({
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between gap-2 p-1">
|
<div className="flex items-center justify-between gap-2 p-1">
|
||||||
{qd.mode !== "text" && (
|
{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
|
⌘/Ctrl + ↵ submits
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ function SessionsSidebar() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="mx-3 mb-1 flex items-center gap-2">
|
<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">
|
<span className="relative flex-1">
|
||||||
<Input
|
<Input
|
||||||
data-testid="session-search"
|
data-testid="session-search"
|
||||||
|
|||||||
@@ -130,9 +130,10 @@ describe("plan block render", () => {
|
|||||||
assert.ok(!/data-testid="plan-block-full"[^>]*border/.test(html), "no wrapping chrome");
|
assert.ok(!/data-testid="plan-block-full"[^>]*border/.test(html), "no wrapping chrome");
|
||||||
// Fills the column UP TO the chat-font cap: the svg is w-full +
|
// Fills the column UP TO the chat-font cap: the svg is w-full +
|
||||||
// mx-auto with maxWidth = extent × TEXT_CAP_SCALE, so node text never
|
// mx-auto with maxWidth = extent × TEXT_CAP_SCALE, so node text never
|
||||||
// renders past the 16.5px prose size (owner, plan-graph round 1).
|
// renders past the 16px prose size (owner, plan-graph round 1; the
|
||||||
|
// snap 16.5→16 landed in typography round 4).
|
||||||
assert.match(html, /<svg[^>]*class="block h-auto w-full mx-auto"/);
|
assert.match(html, /<svg[^>]*class="block h-auto w-full mx-auto"/);
|
||||||
assert.match(html, /max-width:232\.32px/); // (160+16) × 1.32
|
assert.match(html, /max-width:225\.28px/); // (160+16) × 1.28
|
||||||
// Two stacked nodes: 2*NODE_H + GAP_Y = 136 extent + GRAPH_PAD on
|
// Two stacked nodes: 2*NODE_H + GAP_Y = 136 extent + GRAPH_PAD on
|
||||||
// every side (strokes/glow never clip at the edges).
|
// every side (strokes/glow never clip at the edges).
|
||||||
assert.match(html, /viewBox="-8 -8 176 152"/);
|
assert.match(html, /viewBox="-8 -8 176 152"/);
|
||||||
|
|||||||
Reference in New Issue
Block a user