HASH 9534702e4e8d
DATE 2026-07-18
SUBJECT web: hard-wrap for foreign text in timeline descriptions
FILES 3 CHANGED
HASH 9534702e4e8d
DATE 2026-07-18
SUBJECT web: hard-wrap for foreign text in
timeline descriptions
FILES 3 CHANGED
┌─ ooknet-design/src/components/Timeline/Timeline.ts ────────────────────────┐
│ diff --git a/ooknet-design/src/components/Timeline/Timeline.ts b/ooknet-design/src │
│ /components/Timeline/Timeline.ts │
│ index eaf3a1e..f2f2a22 100644 │
│ --- a/ooknet-design/src/components/Timeline/Timeline.ts │
│ +++ b/ooknet-design/src/components/Timeline/Timeline.ts │
│ @@ -1,7 +1,7 @@ │
│ // Vertical timeline: date column, spine junction, entry title, wrapped │
│ // description beside the rail. Returns rows so the component can link │
│ // titles. │
│ -import { G, checkGrid, len, pad, wrap } from "../../lib/ascii"; │
│ +import { G, checkGrid, len, pad, wrapHard } from "../../lib/ascii"; │
│ │
│ export interface TimelineEntry { │
│ date: string; │
│ @@ -39,7 +39,7 @@ export function layoutTimeline(entries: TimelineEntry[], width: │
│ number): Timelin │
│ out.push({ prefix, text: title, href: e.href }); │
│ │
│ if (e.desc) { │
│ - for (const ln of wrap(e.desc, textW).split("\n")) { │
│ + for (const ln of wrapHard(e.desc, textW).split("\n")) { │
│ checkGrid(rail + ln, width, "timeline desc"); │
│ out.push({ prefix: rail, text: ln, soft: true }); │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...mponents/Timeline/Timeline.ts ───┐
│ diff --git a/ooknet-design/src/components/Ti │
│ meline/Timeline.ts b/ooknet-design/src/compo │
│ nents/Timeline/Timeline.ts │
│ index eaf3a1e..f2f2a22 100644 │
│ --- a/ooknet-design/src/components/Timeline/ │
│ Timeline.ts │
│ +++ b/ooknet-design/src/components/Timeline/ │
│ Timeline.ts │
│ @@ -1,7 +1,7 @@ │
│ // Vertical timeline: date column, spine ju │
│ nction, entry title, wrapped │
│ // description beside the rail. Returns row │
│ s so the component can link │
│ // titles. │
│ -import { G, checkGrid, len, pad, wrap } fro │
│ m "../../lib/ascii"; │
│ +import { G, checkGrid, len, pad, wrapHard } │
│ from "../../lib/ascii"; │
│ │
│ export interface TimelineEntry { │
│ date: string; │
│ @@ -39,7 +39,7 @@ export function layoutTime │
│ line(entries: TimelineEntry[], width: number │
│ ): Timelin │
│ out.push({ prefix, text: title, href: e │
│ .href }); │
│ │
│ if (e.desc) { │
│ - for (const ln of wrap(e.desc, textW). │
│ split("\n")) { │
│ + for (const ln of wrapHard(e.desc, tex │
│ tW).split("\n")) { │
│ checkGrid(rail + ln, width, "timeli │
│ ne desc"); │
│ out.push({ prefix: rail, text: ln, │
│ soft: true }); │
│ } │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/ascii.test.ts ──────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/ascii.test.ts b/ooknet-design/src/lib/ascii.tes │
│ t.ts │
│ index 3d54aab..5a2f51e 100644 │
│ --- a/ooknet-design/src/lib/ascii.test.ts │
│ +++ b/ooknet-design/src/lib/ascii.test.ts │
│ @@ -1,5 +1,5 @@ │
│ import { describe, expect, it } from "vitest"; │
│ -import { G, box, center, checkGrid, kv, len, pad, row2, row3, rule, ruleD, wrap } │
│ from "./ascii"; │
│ +import { G, box, center, checkGrid, kv, len, pad, row2, row3, rule, ruleD, wrap, │
│ wrapHard } from "./ascii"; │
│ │
│ describe("len", () => { │
│ it("counts codepoints, not UTF-16 units", () => { │
│ @@ -85,6 +85,17 @@ describe("wrap", () => { │
│ }); │
│ }); │
│ │
│ +describe("wrapHard", () => { │
│ + it("splits words wider than the width", () => { │
│ + const out = wrapHard("feat(hosts:{ooksmedia,ooksdesk,ooknode}) done", 20); │
│ + for (const line of out.split("\n")) expect(len(line)).toBeLessThanOrEqual(20) │
│ ; │
│ + expect(out.replace(/\n/g, "")).toBe("feat(hosts:{ooksmedia,ooksdesk,ooknode}) │
│ done"); │
│ + }); │
│ + it("behaves like wrap for normal prose", () => { │
│ + expect(wrapHard("aa bb cc", 5)).toBe(wrap("aa bb cc", 5)); │
│ + }); │
│ +}); │
│ + │
│ describe("checkGrid", () => { │
│ it("accepts frame glyphs, arrows, and ASCII", () => { │
│ const ok = "│ ok [x] ... -- ↑↓ ╌ ═ ┼ ▁▄█ │"; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...-design/src/lib/ascii.test.ts ───┐
│ diff --git a/ooknet-design/src/lib/ascii.tes │
│ t.ts b/ooknet-design/src/lib/ascii.test.ts │
│ index 3d54aab..5a2f51e 100644 │
│ --- a/ooknet-design/src/lib/ascii.test.ts │
│ +++ b/ooknet-design/src/lib/ascii.test.ts │
│ @@ -1,5 +1,5 @@ │
│ import { describe, expect, it } from "vites │
│ t"; │
│ -import { G, box, center, checkGrid, kv, len │
│ , pad, row2, row3, rule, ruleD, wrap } from │
│ "./ascii"; │
│ +import { G, box, center, checkGrid, kv, len │
│ , pad, row2, row3, rule, ruleD, wrap, wrapHa │
│ rd } from "./ascii"; │
│ │
│ describe("len", () => { │
│ it("counts codepoints, not UTF-16 units", │
│ () => { │
│ @@ -85,6 +85,17 @@ describe("wrap", () => { │
│ }); │
│ }); │
│ │
│ +describe("wrapHard", () => { │
│ + it("splits words wider than the width", ( │
│ ) => { │
│ + const out = wrapHard("feat(hosts:{ooksm │
│ edia,ooksdesk,ooknode}) done", 20); │
│ + for (const line of out.split("\n")) exp │
│ ect(len(line)).toBeLessThanOrEqual(20); │
│ + expect(out.replace(/\n/g, "")).toBe("fe │
│ at(hosts:{ooksmedia,ooksdesk,ooknode})done") │
│ ; │
│ + }); │
│ + it("behaves like wrap for normal prose", │
│ () => { │
│ + expect(wrapHard("aa bb cc", 5)).toBe(wr │
│ ap("aa bb cc", 5)); │
│ + }); │
│ +}); │
│ + │
│ describe("checkGrid", () => { │
│ it("accepts frame glyphs, arrows, and ASC │
│ II", () => { │
│ const ok = "│ ok [x] ... -- ↑↓ ╌ ═ ┼ ▁▄ │
│ █ │"; │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/ascii.ts ───────────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/ascii.ts b/ooknet-design/src/lib/ascii.ts │
│ index 31f24ef..03c1fab 100644 │
│ --- a/ooknet-design/src/lib/ascii.ts │
│ +++ b/ooknet-design/src/lib/ascii.ts │
│ @@ -91,6 +91,21 @@ export const kv = (key: string, val: string, keyW: number, tota │
│ lW: number) => │
│ │
│ export const center = (s: string, w: number) => pad(s, w, "center"); │
│ │
│ +/** wrap(), but words wider than the width hard-split instead of │
│ + * overflowing - for foreign text like commit subjects. */ │
│ +export function wrapHard(s: string, w: number): string { │
│ + return wrap(s, w) │
│ + .split("\n") │
│ + .flatMap((line) => { │
│ + const cps = [...line]; │
│ + if (cps.length <= w) return [line]; │
│ + const out: string[] = []; │
│ + for (let i = 0; i < cps.length; i += w) out.push(cps.slice(i, i + w).join(" │
│ ")); │
│ + return out; │
│ + }) │
│ + .join("\n"); │
│ +} │
│ + │
│ export function row2(l: string, r: string, w: number): string { │
│ const gap = Math.max(1, w - len(l) - len(r)); │
│ return l + " ".repeat(gap) + r; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/ascii.ts ─────┐
│ diff --git a/ooknet-design/src/lib/ascii.ts │
│ b/ooknet-design/src/lib/ascii.ts │
│ index 31f24ef..03c1fab 100644 │
│ --- a/ooknet-design/src/lib/ascii.ts │
│ +++ b/ooknet-design/src/lib/ascii.ts │
│ @@ -91,6 +91,21 @@ export const kv = (key: s │
│ tring, val: string, keyW: number, totalW: nu │
│ mber) => │
│ │
│ export const center = (s: string, w: number │
│ ) => pad(s, w, "center"); │
│ │
│ +/** wrap(), but words wider than the width │
│ hard-split instead of │
│ + * overflowing - for foreign text like com │
│ mit subjects. */ │
│ +export function wrapHard(s: string, w: numb │
│ er): string { │
│ + return wrap(s, w) │
│ + .split("\n") │
│ + .flatMap((line) => { │
│ + const cps = [...line]; │
│ + if (cps.length <= w) return [line]; │
│ + const out: string[] = []; │
│ + for (let i = 0; i < cps.length; i += │
│ w) out.push(cps.slice(i, i + w).join("")); │
│ + return out; │
│ + }) │
│ + .join("\n"); │
│ +} │
│ + │
│ export function row2(l: string, r: string, │
│ w: number): string { │
│ const gap = Math.max(1, w - len(l) - len( │
│ r)); │
│ return l + " ".repeat(gap) + r; │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET