@promptctl/rich-js 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +99 -31
  2. package/dist/core/box.d.ts +96 -42
  3. package/dist/core/box.d.ts.map +1 -1
  4. package/dist/core/box.js +348 -490
  5. package/dist/core/box.js.map +1 -1
  6. package/dist/core/cells.d.ts +46 -1
  7. package/dist/core/cells.d.ts.map +1 -1
  8. package/dist/core/cells.js +55 -14
  9. package/dist/core/cells.js.map +1 -1
  10. package/dist/core/color.d.ts.map +1 -1
  11. package/dist/core/color.js +22 -8
  12. package/dist/core/color.js.map +1 -1
  13. package/dist/core/console.d.ts +30 -14
  14. package/dist/core/console.d.ts.map +1 -1
  15. package/dist/core/console.js +176 -104
  16. package/dist/core/console.js.map +1 -1
  17. package/dist/core/highlighter.d.ts +2 -1
  18. package/dist/core/highlighter.d.ts.map +1 -1
  19. package/dist/core/highlighter.js +28 -6
  20. package/dist/core/highlighter.js.map +1 -1
  21. package/dist/{renderables → core}/json.d.ts +8 -3
  22. package/dist/core/json.d.ts.map +1 -0
  23. package/dist/{renderables → core}/json.js +7 -2
  24. package/dist/core/json.js.map +1 -0
  25. package/dist/core/markup.d.ts +6 -9
  26. package/dist/core/markup.d.ts.map +1 -1
  27. package/dist/core/markup.js +153 -47
  28. package/dist/core/markup.js.map +1 -1
  29. package/dist/core/measure.d.ts +11 -0
  30. package/dist/core/measure.d.ts.map +1 -1
  31. package/dist/core/measure.js +15 -3
  32. package/dist/core/measure.js.map +1 -1
  33. package/dist/core/oklch.d.ts +22 -0
  34. package/dist/core/oklch.d.ts.map +1 -1
  35. package/dist/core/oklch.js +62 -18
  36. package/dist/core/oklch.js.map +1 -1
  37. package/dist/core/pretty.d.ts +122 -0
  38. package/dist/core/pretty.d.ts.map +1 -0
  39. package/dist/core/pretty.js +398 -0
  40. package/dist/core/pretty.js.map +1 -0
  41. package/dist/core/protocol.d.ts +80 -0
  42. package/dist/core/protocol.d.ts.map +1 -1
  43. package/dist/core/protocol.js +85 -0
  44. package/dist/core/protocol.js.map +1 -1
  45. package/dist/core/segment.d.ts +26 -0
  46. package/dist/core/segment.d.ts.map +1 -1
  47. package/dist/core/segment.js +45 -0
  48. package/dist/core/segment.js.map +1 -1
  49. package/dist/core/strip.d.ts +9 -5
  50. package/dist/core/strip.d.ts.map +1 -1
  51. package/dist/core/strip.js +39 -41
  52. package/dist/core/strip.js.map +1 -1
  53. package/dist/core/style.d.ts +19 -0
  54. package/dist/core/style.d.ts.map +1 -1
  55. package/dist/core/style.js +46 -38
  56. package/dist/core/style.js.map +1 -1
  57. package/dist/core/subscription.d.ts +13 -0
  58. package/dist/core/subscription.d.ts.map +1 -0
  59. package/dist/core/subscription.js +13 -0
  60. package/dist/core/subscription.js.map +1 -0
  61. package/dist/core/text.d.ts +102 -4
  62. package/dist/core/text.d.ts.map +1 -1
  63. package/dist/core/text.js +332 -162
  64. package/dist/core/text.js.map +1 -1
  65. package/dist/core/wrap.d.ts +42 -0
  66. package/dist/core/wrap.d.ts.map +1 -0
  67. package/dist/core/wrap.js +128 -0
  68. package/dist/core/wrap.js.map +1 -0
  69. package/dist/host/host-stream.d.ts.map +1 -0
  70. package/dist/host/host-stream.js.map +1 -0
  71. package/dist/host/index.d.ts +4 -0
  72. package/dist/host/index.d.ts.map +1 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/index.js.map +1 -0
  75. package/dist/{widgets → host}/terminal-host.d.ts +20 -64
  76. package/dist/host/terminal-host.d.ts.map +1 -0
  77. package/dist/host/terminal-host.js +133 -0
  78. package/dist/host/terminal-host.js.map +1 -0
  79. package/dist/index.d.ts +13 -36
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +35 -22
  82. package/dist/index.js.map +1 -1
  83. package/dist/node/terminal-host.d.ts +85 -0
  84. package/dist/node/terminal-host.d.ts.map +1 -0
  85. package/dist/node/terminal-host.js +161 -0
  86. package/dist/node/terminal-host.js.map +1 -0
  87. package/dist/node/traceback.d.ts +28 -0
  88. package/dist/node/traceback.d.ts.map +1 -0
  89. package/dist/node/traceback.js +113 -0
  90. package/dist/node/traceback.js.map +1 -0
  91. package/dist/renderables/columns.d.ts +24 -2
  92. package/dist/renderables/columns.d.ts.map +1 -1
  93. package/dist/renderables/columns.js +91 -32
  94. package/dist/renderables/columns.js.map +1 -1
  95. package/dist/renderables/layout.d.ts +61 -5
  96. package/dist/renderables/layout.d.ts.map +1 -1
  97. package/dist/renderables/layout.js +191 -19
  98. package/dist/renderables/layout.js.map +1 -1
  99. package/dist/renderables/markdown.d.ts +1 -1
  100. package/dist/renderables/markdown.d.ts.map +1 -1
  101. package/dist/renderables/markdown.js +7 -14
  102. package/dist/renderables/markdown.js.map +1 -1
  103. package/dist/renderables/padding.d.ts +16 -3
  104. package/dist/renderables/padding.d.ts.map +1 -1
  105. package/dist/renderables/padding.js +96 -47
  106. package/dist/renderables/padding.js.map +1 -1
  107. package/dist/renderables/panel.d.ts +48 -7
  108. package/dist/renderables/panel.d.ts.map +1 -1
  109. package/dist/renderables/panel.js +175 -106
  110. package/dist/renderables/panel.js.map +1 -1
  111. package/dist/renderables/progress.d.ts +3 -3
  112. package/dist/renderables/progress.d.ts.map +1 -1
  113. package/dist/renderables/progress.js +11 -9
  114. package/dist/renderables/progress.js.map +1 -1
  115. package/dist/renderables/progressBar.d.ts +3 -3
  116. package/dist/renderables/progressBar.d.ts.map +1 -1
  117. package/dist/renderables/progressBar.js +9 -15
  118. package/dist/renderables/progressBar.js.map +1 -1
  119. package/dist/renderables/prompt.js +1 -1
  120. package/dist/renderables/prompt.js.map +1 -1
  121. package/dist/renderables/rule.d.ts +1 -1
  122. package/dist/renderables/rule.d.ts.map +1 -1
  123. package/dist/renderables/rule.js +5 -10
  124. package/dist/renderables/rule.js.map +1 -1
  125. package/dist/renderables/spinner.d.ts +2 -2
  126. package/dist/renderables/spinner.d.ts.map +1 -1
  127. package/dist/renderables/spinner.js +6 -11
  128. package/dist/renderables/spinner.js.map +1 -1
  129. package/dist/renderables/status.d.ts.map +1 -1
  130. package/dist/renderables/status.js +5 -11
  131. package/dist/renderables/status.js.map +1 -1
  132. package/dist/renderables/table.d.ts +90 -16
  133. package/dist/renderables/table.d.ts.map +1 -1
  134. package/dist/renderables/table.js +481 -223
  135. package/dist/renderables/table.js.map +1 -1
  136. package/dist/renderables/traceback.d.ts +7 -3
  137. package/dist/renderables/traceback.d.ts.map +1 -1
  138. package/dist/renderables/traceback.js +15 -18
  139. package/dist/renderables/traceback.js.map +1 -1
  140. package/dist/renderables/tree.d.ts +29 -4
  141. package/dist/renderables/tree.d.ts.map +1 -1
  142. package/dist/renderables/tree.js +86 -33
  143. package/dist/renderables/tree.js.map +1 -1
  144. package/dist/template-bindings/color-funcs.d.ts +3 -3
  145. package/dist/template-bindings/color-funcs.js +3 -3
  146. package/dist/template-bindings/helpers.d.ts.map +1 -1
  147. package/dist/template-bindings/helpers.js +19 -1
  148. package/dist/template-bindings/helpers.js.map +1 -1
  149. package/dist/template-bindings/index.d.ts +6 -7
  150. package/dist/template-bindings/index.d.ts.map +1 -1
  151. package/dist/template-bindings/index.js +6 -7
  152. package/dist/template-bindings/index.js.map +1 -1
  153. package/dist/template-bindings/palette-funcs.d.ts +17 -5
  154. package/dist/template-bindings/palette-funcs.d.ts.map +1 -1
  155. package/dist/template-bindings/palette-funcs.js +69 -6
  156. package/dist/template-bindings/palette-funcs.js.map +1 -1
  157. package/dist/template-bindings/style-funcs.d.ts +1 -1
  158. package/dist/template-bindings/style-funcs.js +1 -1
  159. package/dist/themes/ramp.d.ts +84 -0
  160. package/dist/themes/ramp.d.ts.map +1 -0
  161. package/dist/themes/ramp.js +123 -0
  162. package/dist/themes/ramp.js.map +1 -0
  163. package/dist/widgets/dropdown.d.ts +1 -1
  164. package/dist/widgets/dropdown.js +1 -1
  165. package/dist/widgets/event-router.d.ts +3 -2
  166. package/dist/widgets/event-router.d.ts.map +1 -1
  167. package/dist/widgets/focus-manager.d.ts +2 -1
  168. package/dist/widgets/focus-manager.d.ts.map +1 -1
  169. package/dist/widgets/index.d.ts +3 -6
  170. package/dist/widgets/index.d.ts.map +1 -1
  171. package/dist/widgets/index.js +4 -3
  172. package/dist/widgets/index.js.map +1 -1
  173. package/dist/widgets/screen.d.ts +1 -1
  174. package/dist/widgets/screen.d.ts.map +1 -1
  175. package/dist/widgets/text-input.d.ts +2 -3
  176. package/dist/widgets/text-input.d.ts.map +1 -1
  177. package/dist/widgets/text-input.js +4 -12
  178. package/dist/widgets/text-input.js.map +1 -1
  179. package/dist/widgets/types.d.ts +1 -1
  180. package/dist/widgets/types.d.ts.map +1 -1
  181. package/dist/widgets/types.js.map +1 -1
  182. package/dist/widgets/widget-base.d.ts +2 -1
  183. package/dist/widgets/widget-base.d.ts.map +1 -1
  184. package/package.json +38 -6
  185. package/dist/renderables/json.d.ts.map +0 -1
  186. package/dist/renderables/json.js.map +0 -1
  187. package/dist/renderables/pretty.d.ts +0 -29
  188. package/dist/renderables/pretty.d.ts.map +0 -1
  189. package/dist/renderables/pretty.js +0 -141
  190. package/dist/renderables/pretty.js.map +0 -1
  191. package/dist/themes/paletteResolver.d.ts +0 -35
  192. package/dist/themes/paletteResolver.d.ts.map +0 -1
  193. package/dist/themes/paletteResolver.js +0 -88
  194. package/dist/themes/paletteResolver.js.map +0 -1
  195. package/dist/widgets/host-stream.d.ts.map +0 -1
  196. package/dist/widgets/host-stream.js.map +0 -1
  197. package/dist/widgets/terminal-host.d.ts.map +0 -1
  198. package/dist/widgets/terminal-host.js +0 -258
  199. package/dist/widgets/terminal-host.js.map +0 -1
  200. /package/dist/{widgets → host}/host-stream.d.ts +0 -0
  201. /package/dist/{widgets → host}/host-stream.js +0 -0
package/README.md CHANGED
@@ -16,6 +16,25 @@ Works on Linux, macOS, and Windows. Requires Node.js >= 20. ESM-only.
16
16
  npm install @promptctl/rich-js
17
17
  ```
18
18
 
19
+ ### Entry points
20
+
21
+ Most of the library comes from the package name itself — `Console`, `Table`, `Panel`, `Tree`, and everything else in the snippets below. Four areas sit behind package subpaths instead — worth knowing before you go looking for one of them in the main entry point and find nothing there:
22
+
23
+ | Import from | What lives there | What you install yourself | Why it's separate |
24
+ |---|---|---|---|
25
+ | `@promptctl/rich-js/widgets` | Button, Checkbox, Toggle, TextInput, Dropdown, Slider, and the screen that mounts them | `mobx` | Carries a third-party runtime dependency of its own — MobX, for widget state |
26
+ | `@promptctl/rich-js/template-bindings` | The styling vocabulary as Go-template functions, so styled text can be authored as a template | `@promptctl/go-template-js` | Carries a third-party runtime dependency of its own — the Go-template engine, for parsing and evaluating the templates |
27
+ | `@promptctl/rich-js/host` | `TerminalHost`, `BrowserTerminalHost`, `hostStream` — the seam between rendering and a terminal | Nothing | A program that just wants to write bytes through a host shouldn't pay for the widget set to do it |
28
+ | `@promptctl/rich-js/node/save`, `/node/prompt`, `/node/traceback`, `/node/terminal-host` | File export, readline input, the crash handler, and the node TTY host | Nothing | Each one reads node built-ins, and keeping them off the main entry point is what keeps that entry point browser-safe |
29
+
30
+ Those two peer dependencies are yours to install — `npm install @promptctl/rich-js` deliberately fetches neither, because a program that prints a table shouldn't acquire a state library or a template engine to do it. Add the ones you need:
31
+
32
+ ```sh
33
+ npm install @promptctl/rich-js mobx @promptctl/go-template-js
34
+ ```
35
+
36
+ Import `@promptctl/rich-js/widgets` without MobX, or `@promptctl/rich-js/template-bindings` without the engine, and the import itself fails, with `ERR_MODULE_NOT_FOUND`. The main entry point is unaffected — every snippet on this page that imports from `@promptctl/rich-js` runs on the plain install.
37
+
19
38
  ## Using the Console
20
39
 
21
40
  Import and construct a `Console` object:
@@ -145,7 +164,7 @@ await progress.run(async () => {
145
164
  For situations where it is hard to calculate progress, use `Status` to display a spinner animation with a message:
146
165
 
147
166
  ```typescript
148
- import { Console } from "@promptctl/rich-js";
167
+ import { Console, Status } from "@promptctl/rich-js";
149
168
 
150
169
  const console = new Console();
151
170
 
@@ -256,6 +275,64 @@ try {
256
275
  }
257
276
  ```
258
277
 
278
+ To format every crash — uncaught exceptions and unhandled promise rejections alike — install the handler at your entry point:
279
+
280
+ ```typescript
281
+ import { installTraceback } from "@promptctl/rich-js/node/traceback";
282
+
283
+ installTraceback();
284
+ ```
285
+
286
+ `installTraceback` lives on the `node/traceback` subpath because it calls `process.on` and `process.exit`; the `Traceback` renderable itself stays in the main barrel, which remains browser-safe.
287
+
288
+ </details>
289
+
290
+ <details>
291
+ <summary>Interactive Widgets</summary>
292
+
293
+ Everything above draws once and returns. Widgets stay on screen and respond — a button that highlights under the cursor, a text field with a cursor you can move, a dropdown you filter by typing. They come from the `widgets` subpath and need MobX installed alongside the package (see [Entry points](#entry-points)).
294
+
295
+ ```typescript
296
+ import { Button, TextInput, DefaultScreen, EventRouter } from "@promptctl/rich-js/widgets";
297
+ import { NodeTerminalHost } from "@promptctl/rich-js/node/terminal-host";
298
+
299
+ const host = new NodeTerminalHost();
300
+ const screen = new DefaultScreen({ host });
301
+ const router = new EventRouter({ screen, host });
302
+
303
+ const name = new TextInput({ placeholder: "your name" });
304
+ const submit = new Button({ label: "Submit", variant: "primary" });
305
+
306
+ const quit = (): void => {
307
+ router.stop();
308
+ screen.stop();
309
+ host.write("\n");
310
+ };
311
+
312
+ submit.onSubmit(() => {
313
+ quit();
314
+ host.write(`hello, ${name.value}\n`);
315
+ process.exit(0);
316
+ });
317
+
318
+ // Raw mode swallows Ctrl+C, so the app has to handle it itself.
319
+ router.onKey(
320
+ (event) => {
321
+ if (event.ctrl && event.key === "c") {
322
+ quit();
323
+ process.exit(0);
324
+ }
325
+ },
326
+ { priority: "high" },
327
+ );
328
+
329
+ screen.mount(name, submit);
330
+ screen.start();
331
+ router.start();
332
+ ```
333
+
334
+ `DefaultScreen` builds a focus manager when you don't pass one, so the `TextInput` has focus before the user touches anything and Tab moves between the two widgets. The screen re-renders through a MobX reaction: change a widget's state and the frame redraws itself, with no explicit repaint call anywhere. See [docs/widgets.md](docs/widgets.md) for the full widget set, key dispatch, layout placements, and how to write your own.
335
+
259
336
  </details>
260
337
 
261
338
  ## Custom Renderables
@@ -302,14 +379,12 @@ saveHtml(console, "output.html");
302
379
 
303
380
  ## Demos
304
381
 
305
- A handful of demos under `examples/` exercise the library against realistic use cases. Two are explained in detail below (`rich-explore` and `claude-sessions`); the rest are listed here with one-line summaries. The full set of available npm scripts is the authoritative list — `cat package.json | jq .scripts` to see them all.
382
+ Every demo below also runs in your browser on the [live demo gallery](https://brandon-fryslie.github.io/rich-js/master/demos/) — the same code mounted against an xterm.js terminal, nothing to install. To drive one against your own terminal instead, run its npm script:
306
383
 
307
384
  ```sh
308
- # Detailed below
309
- npm run demo # rich-explore — TUI file browser + markdown/code reader (interactive)
310
- npm run sessions # claude-sessions — Claude Code session browser (interactive)
311
-
312
- # Other interactive demos
385
+ # Interactive
386
+ npm run demo # rich-explore — TUI file browser + markdown/code reader
387
+ npm run sessions # claude-sessions — Claude Code session browser
313
388
  npm run demo-inputs # rich-config — TextInput / palette search
314
389
  npm run demo:dropdown # dropdown-demo — Dropdown widget showcase
315
390
  npm run dash # rich-dash — Live dashboard
@@ -321,6 +396,8 @@ npm run strip # rich-strip — side-by-side joiner showcase
321
396
  npm run markup-plugins # rich-markup-plugins — plugin-tag examples
322
397
  ```
323
398
 
399
+ `rich-explore`, `claude-sessions`, and `themes-and-color-studio` are covered in detail below. `package.json` holds the authoritative script list — `jq .scripts package.json` to see it.
400
+
324
401
  ### rich-explore — TUI file browser + markdown/code reader
325
402
 
326
403
  A two-pane file browser with a directory tree on the left and a file preview on the right. Navigate with vim-style keys, Tab to switch focus, Enter/arrow keys to expand/collapse directories.
@@ -373,7 +450,7 @@ npm run sessions
373
450
 
374
451
  ### themes-and-color-studio — color, palette, theme, and contrast tour
375
452
 
376
- A one-shot non-interactive demo that walks every public surface of the color subsystem in eight sections: ColorRgba values and parsing, ColorSpec and downgrade tables, color-system detection, the theme registry, PaletteResolver spec forms, every bundled `TerminalTheme` constant, OKLCH transposition (hue circle / chroma sweep / lightness invert / themeKeyForRoot), and the WCAG contrast toolkit.
453
+ A one-shot non-interactive demo that walks every public surface of the color subsystem in eight sections: ColorRgba values and parsing, ColorSpec and downgrade tables, color-system detection, the theme registry, colour references through `resolveColorRef`, every bundled `TerminalTheme` constant, OKLCH transposition (hue circle / chroma sweep / lightness invert / themeKeyForRoot), and the WCAG contrast toolkit.
377
454
 
378
455
  ```sh
379
456
  npm run themes-and-color-studio # terminal output
@@ -387,7 +464,7 @@ EXPORT_HTML=out.html npm run themes-and-color-studio # also write a styled HTML
387
464
  | `ColorRgba` / `parseRgbHex` / `parseRgbaHex` / `blendRgb` | Pixel-level values, two hex parsers, linear blend, alpha compositing |
388
465
  | `ColorSpec` / `ColorDepth` | Every factory; downgrade across `STANDARD_TABLE` / `EIGHT_BIT_TABLE` / `WINDOWS_TABLE`; `ANSI_COLOR_NAMES` lookups; `ColorParseError` |
389
466
  | `detectColorSystem` / `resolveColorSystem` | Env-driven color-system detection with `DetectColorOptions` fixtures; spec-string resolution |
390
- | `Palette` / `PaletteResolver` / `buildPalette` | Bare / modifier / alpha / auto spec forms against gruvbox; `BaseColors` → derived `text-*` / `on-*` / `*-muted` vars |
467
+ | `Palette` / `resolveColorRef` / `parseHexColor` / `buildPalette` | Palette names and `#RRGGBB` literals through one checkpoint against gruvbox, including the hex round-trip that shows it is idempotent and the `ColorRefError` miss; `BaseColors` → derived `text-*` / `on-*` / `*-muted` vars |
391
468
  | Theme registry | `getThemePalette` / `listThemePalettes` / `getThemeBaseColors` walking every bundled theme; raw `THEMES` / `ThemePaletteData` via subpath |
392
469
  | `TerminalTheme` constants | All bundled constants (`DEFAULT`, `SVG_EXPORT`, `MONOKAI`, `NORD`, `GRUVBOX`, `DRACULA`, `TOKYO_NIGHT`, `FLEXOKI`, `CYBERPUNK`, `CATPPUCCIN_*`, `SOLARIZED_*`, `ROSE_PINE*`, `ATOM_ONE_*`, `TEXTUAL_*`) |
393
470
  | `Oklch` / `transposePalette` / `themeKeyForRoot` | Round-trip + `IDENTITY` / `INVERT_LIGHTNESS`; hue circle; chroma sweep; light↔dark invert; `ANCHORED_ROOTS` / `isAnchored` |
@@ -396,35 +473,26 @@ EXPORT_HTML=out.html npm run themes-and-color-studio # also write a styled HTML
396
473
 
397
474
  ---
398
475
 
399
- ### Coverage summary
476
+ ### Demo coverage is checked, not claimed
477
+
478
+ `test/coverage/coverage.test.ts` asserts that every public export is referenced by at least one file under `examples/`. Add a public export without demoing it and CI fails.
479
+
480
+ The check builds its universe from `package.json#exports` when the test loads, so there is no hand-written list of covered symbols to maintain — and none to drift out of date. Coverage is counted per *symbol origin*, meaning the declaring file plus the declared name, so one declaration re-exported under two names is still one thing to demonstrate. Renaming a symbol on import inside a demo still counts; `import * as rich` does not, because a namespace import never names what it pulls in.
400
481
 
401
- **Exercised across demos:**
482
+ An export that genuinely cannot be demonstrated at runtime belongs in `test/coverage/coverage-allowlist.ts` with a written reason. The allowlist is validated in both directions: an entry pointing at no real export fails the suite, and so does an entry for something a demo now covers. You can neither widen the exemption quietly nor leave a stale one lying around.
402
483
 
403
- Core: `Console`, `Style`/`StyleStack`, `RichText`/`Span`, `Segment`, `Box` (multiple variants), `Color`/`blendRgb`/palettes, `Renderable`/`Measurable` protocol, `Measurement`, `cells` (transitively), `ReprHighlighter`, `JSONHighlighter`, `Spinner` data, `TerminalTheme`
484
+ Two things to know before adding a demo. `examples/shared/` is a helper module rather than a demo, and references from it count — the verifier walks every file under `examples/`, not just the entry points. And a demo reaches the browser gallery only if it has an `examples/<name>/wire.ts`; `npm run demos:build` fails loudly when a `wire.ts` has no compiled output rather than dropping the demo from the site.
404
485
 
405
- Renderables: `Layout`, `Panel`, `Tree`, `Table`/`Column`, `Markdown`, `Syntax`, `JSONRenderable`, `Rule`, `Group`, `Spinner`, `Pretty`, `Traceback`
486
+ That check is a floor, not a goal. A script that imports thirty exports and prints them in sequence passes it; a small interactive TUI that lets you drive eight of them in a real composition is the better demo, and no test can tell you which one you wrote.
406
487
 
407
- **Bugs found and fixed via demo integration:**
488
+ **Bugs found and fixed via demo integration** — the argument for exercising the library this way rather than only in unit tests:
408
489
 
409
490
  | Bug | Location | Impact | Fix |
410
491
  |---|---|---|---|
411
- | `Live.refresh()` strips all ANSI styles | `src/renderables/live.ts:106` | Every renderable flowing through `Live` (including `Status`, `Progress`, `Spinner`) appeared unstyled | Apply `style.render(text, colorSystem)` instead of bare `s.text` |
412
- | `Progress.render()` drops column styles | `src/renderables/progress.ts:273` | Progress percentage, timing, and spinner styles were stripped when building table cells | Use `RichText.append(text, style)` to preserve segment styles |
413
- | `Tree` emits double blank lines | `src/renderables/tree.ts:98,122` | Label rendering and the explicit `Segment.line()` both contributed a newline, producing blank lines between tree entries | Make `RichText` stop emitting a trailing newline so `Tree`'s explicit `yield Segment.line()` remains the only line break |
414
- | `Spinner` constructor rejects `undefined` name | `src/renderables/spinner.ts:36` | `SpinnerColumn` (used by `Progress`) passed optional `string \| undefined` to required `string` parameter | Make `name` optional, default to `DEFAULT_SPINNER` |
415
-
416
- **Not yet exercised — candidates for new demos or demo additions:**
417
-
418
- | Module | Notes | Suggested coverage |
419
- |---|---|---|
420
- | `Status` | Spinner + message display | Loading indicator for large sessions in claude-sessions |
421
- | `Prompt` / `IntPrompt` / `FloatPrompt` / `Confirm` | Interactive input via readline | Add a go-to-path prompt in rich-explore; incompatible with raw-mode loops so needs a modal switch |
422
- | `emoji` | Shortcode substitution (`:smiley:` → 😃) | Enable in markup-rendered block text in claude-sessions |
423
- | `NullHighlighter` / `RegexHighlighter` | Specialized highlighters not yet exercised | `RegexHighlighter` for search-term highlighting in claude-sessions |
424
- | `StyleStack` / `Theme` / `DEFAULT_STYLES` | Theme customization | Add a Console style-theme switcher demo |
425
- | `Console` recording | `record`, `exportText`, `exportHtml`, `saveHtml` | Add an export-to-HTML feature to claude-sessions |
426
- | Most `Box` variants | `ASCII`, `SQUARE`, `MINIMAL`, `HEAVY`, `DOUBLE`, `MARKDOWN`, etc. | Add a box-style picker to rich-explore's Panel borders |
427
- | `Measurement.get()` / `measureRenderables` | Explicit width measurement | Used internally; could add a measurement debug overlay |
492
+ | `Live.refresh()` strips all ANSI styles | `Live.refresh` in `src/renderables/live.ts` | Every renderable flowing through `Live` (including `Status`, `Progress`, `Spinner`) appeared unstyled | Apply `style.render(text, colorSystem)` instead of bare `s.text` |
493
+ | `Progress.render()` drops column styles | `Progress.render` in `src/renderables/progress.ts` | Progress percentage, timing, and spinner styles were stripped when building table cells | Use `RichText.append(text, style)` to preserve segment styles |
494
+ | `Tree` emits double blank lines | `Tree.render` in `src/renderables/tree.ts` | Label rendering and the explicit `Segment.line()` both contributed a newline, producing blank lines between tree entries | Make `RichText` stop emitting a trailing newline so `Tree`'s explicit `yield Segment.line()` remains the only line break |
495
+ | `Spinner` constructor rejects `undefined` name | `Spinner` constructor in `src/renderables/spinner.ts` | `SpinnerColumn` (used by `Progress`) passed optional `string \| undefined` to required `string` parameter | Make `name` optional, default to `DEFAULT_SPINNER` |
428
496
 
429
497
  ## Environment Variables
430
498
 
@@ -1,27 +1,46 @@
1
1
  /**
2
2
  * Box-drawing character sets for borders and table grids.
3
+ *
4
+ * A style is declared as an 8x4 character grid — one line per row a table can
5
+ * draw, one column per position within that row:
6
+ *
7
+ * ┌─┬┐ top border
8
+ * │ ││ header content
9
+ * ├─┼┤ header separator
10
+ * │ ││ body content
11
+ * ├─┼┤ row separator
12
+ * ├─┼┤ footer separator
13
+ * │ ││ footer content
14
+ * └─┴┘ bottom border
15
+ *
16
+ * [LAW:one-source-of-truth] The grid is the shape the reference implementation
17
+ * (Python Rich's `box.py`) publishes these glyphs in, so a constant below can
18
+ * be diffed against it character for character. The eighteen named fields this
19
+ * replaced were a second, differently-shaped map of the same territory, and it
20
+ * had drifted: seven of the nineteen constants carried wrong glyphs, and the
21
+ * field named `mid` held the reference's line 3 while the reference's `mid_*`
22
+ * is line 4 — so reading the reference name-for-name swapped a separator for a
23
+ * content row. Named row fields are gone for that reason; a row is reached by
24
+ * what it is (`getRow`, `getContentChars`), never by a name that can be
25
+ * mismatched to a line.
3
26
  */
4
27
  import { Segment } from "./segment.js";
5
28
  import type { Style } from "./style.js";
6
- export interface BoxChars {
7
- topLeft: string;
8
- top: string;
9
- topDivider: string;
10
- topRight: string;
11
- headLeft: string;
12
- headVertical: string;
13
- headRight: string;
14
- midLeft: string;
15
- mid: string;
16
- midVertical: string;
17
- midRight: string;
18
- bottomLeft: string;
19
- bottom: string;
20
- bottomDivider: string;
21
- bottomRight: string;
29
+ /** A rule spanning the table: the top and bottom borders, and every separator. */
30
+ export interface EdgeChars {
22
31
  left: string;
32
+ horizontal: string;
33
+ cross: string;
23
34
  right: string;
35
+ }
36
+ /**
37
+ * The verticals framing a row of cells. A content line of the grid has no
38
+ * horizontal of its own — its fill column is a placeholder the cells occupy.
39
+ */
40
+ export interface ContentChars {
41
+ left: string;
24
42
  vertical: string;
43
+ right: string;
25
44
  }
26
45
  export type RowLevel = "head" | "row" | "foot" | "mid";
27
46
  export interface SubstituteOptions {
@@ -29,44 +48,79 @@ export interface SubstituteOptions {
29
48
  safe?: boolean;
30
49
  }
31
50
  export declare class Box {
32
- readonly topLeft: string;
33
- readonly top: string;
34
- readonly topDivider: string;
35
- readonly topRight: string;
36
- readonly headLeft: string;
37
- readonly headVertical: string;
38
- readonly headRight: string;
39
- readonly midLeft: string;
40
- readonly mid: string;
41
- readonly midVertical: string;
42
- readonly midRight: string;
43
- readonly bottomLeft: string;
44
- readonly bottom: string;
45
- readonly bottomDivider: string;
46
- readonly bottomRight: string;
47
- readonly left: string;
48
- readonly right: string;
49
- readonly vertical: string;
50
- constructor(chars: BoxChars);
51
+ readonly top: EdgeChars;
52
+ readonly bottom: EdgeChars;
53
+ private readonly grid;
54
+ private readonly ascii;
55
+ private readonly headContent;
56
+ private readonly headSeparator;
57
+ private readonly bodyContent;
58
+ private readonly rowSeparator;
59
+ private readonly footSeparator;
60
+ private readonly footContent;
61
+ /**
62
+ * [LAW:parse-dont-validate] The one crossing between a grid string and box
63
+ * glyphs. A `Box` cannot exist without eight rows of four characters, so no
64
+ * consumer below ever re-checks the shape of the data it reads.
65
+ */
66
+ constructor(grid: string);
51
67
  /**
52
68
  * Renders the top border row for given column widths.
53
69
  */
54
- getTop(widths: number[], style?: Style, edge?: boolean): Segment[];
70
+ getTop(widths: readonly number[], style?: Style, edge?: boolean): Segment[];
55
71
  /**
56
- * Renders a separator row.
72
+ * Renders the separator drawn *above* a row at `level` — the head separator
73
+ * under the header, the row separator between body rows, the foot separator
74
+ * above the footer.
57
75
  */
58
- getRow(widths: number[], level: RowLevel, style?: Style, edge?: boolean): Segment[];
76
+ getRow(widths: readonly number[], level: RowLevel, style?: Style, edge?: boolean): Segment[];
77
+ /**
78
+ * The verticals that frame a content row at `level` — the counterpart to
79
+ * `getRow`, which draws the separator between two such rows.
80
+ */
81
+ getContentChars(level: RowLevel): ContentChars;
59
82
  /**
60
83
  * Renders the bottom border row.
61
84
  */
62
- getBottom(widths: number[], style?: Style, edge?: boolean): Segment[];
85
+ getBottom(widths: readonly number[], style?: Style, edge?: boolean): Segment[];
63
86
  /**
64
- * Returns a new Box with characters substituted for ASCII or safe alternatives.
65
- * asciiOnly: all characters become ASCII (+, -, |)
66
- * safe: problematic characters (e.g. rounded corners) replaced with square equivalents
87
+ * The box to draw with when the platform cannot render this one as written.
88
+ *
89
+ * `asciiOnly` gives up a box that spends non-ASCII glyphs for `ASCII`, and
90
+ * leaves the four already-ASCII styles as they are — a caller that chose
91
+ * MARKDOWN or ASCII_DOUBLE_HEAD asked for that frame and it is already
92
+ * drawable, so answering with `ASCII` would trade a frame the terminal
93
+ * supports for a different one it equally supports. `safe` squares off the
94
+ * rounded corners a legacy Windows terminal draws as blanks.
67
95
  */
68
96
  substitute(options?: SubstituteOptions): Box;
69
97
  private safeSubstitute;
98
+ /**
99
+ * The nearest box that spends no special glyphs on a header — what a table
100
+ * with `showHeader: false` draws with. A box whose head row already matches
101
+ * its body is its own answer, so this is the identity for fourteen of the
102
+ * nineteen shipped styles.
103
+ *
104
+ * It stands beside `substitute` rather than joining it because the two ask
105
+ * unrelated questions: `substitute` asks what the *platform* can draw, this
106
+ * asks what the *table* contains. Nothing correlates them, so a caller that
107
+ * wants both wants both, and a shared options bag would only multiply the
108
+ * combinations either one has to reason about. [LAW:no-mode-explosion]
109
+ *
110
+ * [LAW:types-are-the-program] The relation is keyed on the grid, not on
111
+ * object identity as the reference's dict is. A `Box` is wholly determined by
112
+ * its grid — that is what the constructor takes and all eight rows derive
113
+ * from — so two boxes with one grid must answer this alike. Identity keying
114
+ * would say otherwise the moment a box arrived by any route but the shipped
115
+ * constant, and `safeSubstitute` above builds exactly such a box: a fresh
116
+ * instance carrying an unchanged grid.
117
+ */
118
+ plainHeaded(): Box;
119
+ /**
120
+ * [LAW:dataflow-not-control-flow] Every full-width rule the box can draw is
121
+ * this one loop; which rule it is arrives as four characters, not a branch.
122
+ */
123
+ private getEdge;
70
124
  private getRowChars;
71
125
  }
72
126
  export declare const ASCII: Box;
@@ -1 +1 @@
1
- {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/core/box.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/core/box.ts"],"names":[],"mappings":"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"}