Public: anyone can read this; the crew can change it.
`.break` and `.tok`'s nowrap are base.ts text utilities, and neither stays in content.ts #4786
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/4747-break-and-tok-s-nowrap-are-base-ts-text → main 8 commits fork 2 threads · 2 unresolved
6 files
changed +27
−8
docs/DESIGN.md −2
| @@ -423,7 +423,6 @@ the same line (#4888). | ||
| 423 | 423 | - `.bp-h3` |
| 424 | 424 | - `.bp-w2` |
| 425 | 425 | - `.bp-w3` |
| 426 | - `.break` | |
| 427 | 426 | - `.commit-body` |
| 428 | 427 | - `.commit-head` |
| 429 | 428 | - `.filters` |
| @@ -436,7 +435,6 @@ the same line (#4888). | ||
| 436 | 435 | - `.repo-desc` |
| 437 | 436 | - `.repo-title` |
| 438 | 437 | - `.slash` |
| 439 | - `.tok` | |
| 440 | 438 | - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**). |
| 441 | 439 | - **`pages/board.ts`** — the board itself is §5's **board**; the spend tables `spend.ts` draws wear §5's `.table` and `.stack`, no class of their own |
| 442 | 440 | - **`studio/feed_style.ts`** — the home feed's cards and its composer (the apps rail is §5's card, row and btn; an admin's tick boxes and Archive are always shown, deliberately, with no Select toggle): |
docs/design/areas/text.md +5
| @@ -17,6 +17,11 @@ quiet line under a card), and a page reaches for them instead of setting a colou | ||
| 17 | 17 | - `.truncate` — one line that ends in an ellipsis rather than wrapping. Written by `ui.ts` (`envFacts()`'s URL) and |
| 18 | 18 | pages such as `pages/apps.ts`, `pages/refs.ts` and `pages/explorer.ts`; styled in `style/base.ts`, capped at 34ch |
| 19 | 19 | inside `.table` by `style/content.ts`. |
| 20 | - `.nowrap` — keeps a short run (a token count, a figure with its unit) on one line without clipping it. Written by | |
| 21 | `ui.ts` (`tokenCount()`, with `.num`); styled in `style/base.ts`. | |
| 22 | - `.break` — lets a long unbroken string (a URL, a hash, a path) wrap anywhere rather than overflow its box. Written by | |
| 23 | `ui.ts`, `scope.ts` and pages such as `pages/backups.ts`, `pages/origins.ts` and `studio/feed.ts`; styled in | |
| 24 | `style/base.ts`. | |
| 20 | 25 | - `.sr-only` — text for a screen reader only: a table's caption, a select's label, a fieldset's legend. Written by |
| 21 | 26 | pages such as `pages/audit.ts`, `pages/pull.ts` and `pages/thread.ts`, and by `studio/app.ts` and |
| 22 | 27 | `studio/review_act.ts`; styled in `style/base.ts`. |
src/web/style/base.ts +2
| @@ -166,6 +166,8 @@ export const utilities = /* css */ ` | ||
| 166 | 166 | .grow { flex: 1; min-width: 0; } |
| 167 | 167 | .spacer { flex: 1; } |
| 168 | 168 | .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 169 | .nowrap { white-space: nowrap; } | |
| 170 | .break { overflow-wrap: anywhere; } | |
| 169 | 171 | .stack { display: flex; flex-direction: column; gap: 12px; } |
| 170 | 172 | .stack-tight { gap: 4px; } |
| 171 | 173 | .bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } |
src/web/style/content.ts −2
| @@ -150,14 +150,12 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); } | ||
| 150 | 150 | font-variant-numeric: tabular-nums; letter-spacing: 0.14em; |
| 151 | 151 | text-align: center; user-select: all; word-break: break-all; |
| 152 | 152 | } |
| 153 | .break { overflow-wrap: anywhere; } | |
| 154 | 153 | [data-apps] { overflow-wrap: anywhere; } |
| 155 | 154 | |
| 156 | 155 | .bar > .device-code { flex: 1 1 auto; } |
| 157 | 156 | |
| 158 | 157 | .repo-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; } |
| 159 | 158 | |
| 160 | .tok { white-space: nowrap; } | |
| 161 | 159 | |
Nit: removing |
||
| 162 | 160 | .filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; } |
| 163 | 161 | .filters form { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } |
src/web/ui.ts +1 −1
| @@ -133,7 +133,7 @@ export function tokenCount( | ||
| 133 | 133 | basis: TokenBasis, |
| 134 | 134 | kind?: "prose" | "code" | "data", |
| 135 | 135 | ): Html { |
| 136 | return html`<span class="tok" title="${tokenNote(n, basis, kind)}" | |
| 136 | return html`<span class="num nowrap" title="${tokenNote(n, basis, kind)}" | |
| 137 | 137 | >${formatTokens(n, basis)}</span |
| 138 | 138 | >`; |
| 139 | 139 | } |
test/tokens.test.ts +19 −3
| @@ -24,6 +24,8 @@ import { | ||
| 24 | 24 | textKind, |
| 25 | 25 | tokenNote, |
| 26 | 26 | } from "../src/kernel/tokens-format.ts"; |
| 27 | import { css } from "../src/web/style.ts"; | |
| 28 | import { tokenCount } from "../src/web/ui.ts"; | |
| 27 | 29 | |
| 28 | 30 | describe("the formatter", () => { |
| 29 | 31 | test("writes counts the way storage is written", () => { |
| @@ -83,6 +85,20 @@ describe("the estimate", () => { | ||
| 83 | 85 | }); |
| 84 | 86 | }); |
| 85 | 87 | |
| 88 | describe("the markup", () => { | |
| 89 | test("a count wears the base text utilities, not a class of its own", () => { | |
| 90 | const markup = String(tokenCount(1200, "estimated", "code")); | |
| 91 | expect(markup).toContain('class="num nowrap"'); | |
| 92 | expect(markup).not.toContain('class="tok"'); | |
| 93 | expect(css).not.toMatch(/\.tok\s*\{/); | |
| 94 | }); | |
| 95 | ||
| 96 | test("the stylesheet defines .break and .nowrap once each", () => { | |
| 97 | expect(css.match(/\.break\s*\{/g)).toHaveLength(1); | |
| 98 | expect(css.match(/\.nowrap\s*\{/g)).toHaveLength(1); | |
| 99 | }); | |
| 100 | }); | |
| 101 | ||
| 86 | 102 | describe("token counts on the pages", () => { |
| 87 | 103 | let dataDir: string; |
| 88 | 104 | let app: App; |
| @@ -199,7 +215,7 @@ describe("token counts on the pages", () => { | ||
| 199 | 215 | const page = await get("/alice/one/blob/main/src/a.ts", cookie); |
| 200 | 216 | // 700 B; 700 ÷ 3.5 = 200 tokens. |
| 201 | 217 | expect(page).toMatch( |
| 202 | /1 lines · 700 B · <span class="tok" title="About 200 tokens — an estimate at 3\.5 bytes per token for code/, | |
| 218 | /1 lines · 700 B · <span class="num nowrap" title="About 200 tokens — an estimate at 3\.5 bytes per token for code/, | |
| 203 | 219 | ); |
| 204 | 220 | expect(page).toContain(">~200 tok</span"); |
| 205 | 221 | }); |
| @@ -208,14 +224,14 @@ describe("token counts on the pages", () => { | ||
| 208 | 224 | const page = await get("/alice/one/edit/main/data.json", cookie); |
| 209 | 225 | expect(page).toContain('id="editor-size"'); |
| 210 | 226 | // 3000 B = 2.9 KB; 3000 ÷ 3.0 = 1000 tokens. |
| 211 | expect(page).toMatch(/2\.9 KB · <span class="tok"[^>]*>~1\.0k tok</); | |
| 227 | expect(page).toMatch(/2\.9 KB · <span class="num nowrap"[^>]*>~1\.0k tok</); | |
| 212 | 228 | }); |
| 213 | 229 | |
| 214 | 230 | test("a run shows tokens in and out beside its cost, to the owner only", async () => { |
| 215 | 231 | const page = await get(`/alice/one/runs/${runId}`, cookie); |
| 216 | 232 | // 46,500 in → 47k; 800 out. Counted, so no tilde. |
| 217 | 233 | expect(page).toMatch( |
| 218 | /id="run-tokens"\s*><span class="tok" title="46,500 tokens, as counted by Claude's tokenizer"\s*>47k tok<\/span\s*>\s*in ·/, | |
| 234 | /id="run-tokens"\s*><span class="num nowrap" title="46,500 tokens, as counted by Claude's tokenizer"\s*>47k tok<\/span\s*>\s*in ·/, | |
| 219 | 235 | ); |
| 220 | 236 | expect(page).toContain(">800 tok</span"); |
| 221 | 237 | const read = await get(`/alice/one/runs/${runId}`, bobCookie); |
new above
offline
Nit: removing
.tokleaves two blank lines between.repo-actionsand.filters. Drop one when you next touch this file.