OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
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
 

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 });
       }

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 });
       }
 

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] ... -- ↑↓ ╌ ═ ┼ ▁▄█ │";

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] ... -- ↑↓ ╌ ═ ┼ ▁▄
█ │";
 

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;

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