@promptctl/rich-js 0.24.1 → 0.25.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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +21 -29
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +13 -16
  47. package/dist/core/strip.js +119 -110
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +132 -54
  73. package/dist/renderables/effect.js +188 -115
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
package/dist/core/text.js CHANGED
@@ -6,7 +6,9 @@ import { divideLine, wordWidths } from "./wrap.js";
6
6
  import { Segment } from "./segment.js";
7
7
  import { Style, NULL_STYLE, StyleSyntaxError } from "./style.js";
8
8
  import { stripOscTerminators } from "./osc8.js";
9
- import { drawable, getStyle, withBoundedWidth } from "./protocol.js";
9
+ import { betweenPasses, drawable, getStyle, withBoundedWidth } from "./protocol.js";
10
+ import { pattern } from "./pattern.js";
11
+ import { ListState, State } from "./state.js";
10
12
  // Every C0 control and DEL except \t and \n. Rich's `Text` strips only
11
13
  // `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR); the rest of the set is the
12
14
  // port's extension, so no byte of content can start an escape sequence or move
@@ -22,11 +24,10 @@ import { drawable, getStyle, withBoundedWidth } from "./protocol.js";
22
24
  // pass, since right-to-left text needs them.
23
25
  // [LAW:single-enforcer] Single place where control chars are sanitized: every
24
26
  // string that becomes part of a Text's characters passes here.
25
- const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F\u202A-\u202E\u2066-\u206F]/g;
26
27
  export function stripControlChars(text) {
27
- return text.replace(CONTROL_CHARS_RE, "");
28
+ return text.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F\u202A-\u202E\u2066-\u206F]/g, "");
28
29
  }
29
- const TRAILING_WHITESPACE_RE = /\s+$/;
30
+ const TRAILING_WHITESPACE_RE = pattern(/\s+$/);
30
31
  /**
31
32
  * The plain text of one line of segments, in the coordinate system
32
33
  * `divideLine` and `Segment.divide` share: cell offsets into the styled line
@@ -84,9 +85,9 @@ function sanitizeStyleLink(style) {
84
85
  * lays its text out anew, so a stamp naming where a cell sat in the output it
85
86
  * came from would name the wrong cell once it wraps, slices or overlaps; the
86
87
  * owner that renders the RichText is the one that stamps its cells. A
87
- * `nextChange` stays: when a span's cells next change is true of them
88
- * wherever the text lays them, so a frame appended into a line still says
89
- * when it turns.
88
+ * hold stays — `lastChange` and `nextChange`: when a span's cells began and
89
+ * next change is true of them wherever the text lays them, so a frame
90
+ * appended into a line still says when it turns.
90
91
  */
91
92
  function admitStyle(style) {
92
93
  if (typeof style === "string")
@@ -129,7 +130,7 @@ function resolveDefinition(options, style) {
129
130
  catch (err) {
130
131
  if (!(err instanceof StyleSyntaxError))
131
132
  throw err;
132
- options.onStyleError?.(err, style);
133
+ betweenPasses(() => options.onStyleError?.(err, style));
133
134
  return NULL_STYLE;
134
135
  }
135
136
  }
@@ -170,41 +171,45 @@ export class Span {
170
171
  }
171
172
  }
172
173
  export class RichText {
174
+ // Rich's `Text` is changed in place — appended to, stylized, padded — so its
175
+ // characters, spans and format are state, and a write to one while
176
+ // something is drawn throws. Three holders rather than one per field: a
177
+ // `RichText` is the most-made object in the library.
173
178
  _text;
174
- _spans;
175
- _style;
176
- _justify;
177
- _overflow;
178
- _end;
179
+ _spans = new ListState();
180
+ _format;
179
181
  _tabSize;
180
- _noWrap;
181
182
  constructor(text, options) {
182
- this._text = text ? stripControlChars(text) : "";
183
- this._spans = [];
184
- // [LAW:single-enforcer] `admitStyle` is the boundary that sanitizes
185
- // any link URL crossing into a RichText; downstream trusts the invariant.
186
- this._style = admitStyle(options?.style ?? NULL_STYLE);
187
- this._justify = options?.justify;
188
- this._overflow = options?.overflow;
189
- this._end = options?.end ?? "\n";
183
+ this._text = new State(text ? stripControlChars(text) : "");
184
+ this._format = new State({
185
+ // [LAW:single-enforcer] `admitStyle` is the boundary that sanitizes
186
+ // any link URL crossing into a RichText; downstream trusts the invariant.
187
+ style: admitStyle(options?.style ?? NULL_STYLE),
188
+ justify: options?.justify,
189
+ overflow: options?.overflow,
190
+ end: options?.end ?? "\n",
191
+ noWrap: options?.noWrap ?? false,
192
+ });
190
193
  this._tabSize = parseTabSize(options?.tabSize ?? 8);
191
- this._noWrap = options?.noWrap ?? false;
194
+ }
195
+ _reformat(change) {
196
+ this._format.set({ ...this._format.get(), ...change });
192
197
  }
193
198
  // --- Properties ---
194
199
  get plain() {
195
- return this._text;
200
+ return this._text.get();
196
201
  }
197
202
  set plain(value) {
198
203
  const sanitized = stripControlChars(value);
199
- this._text = sanitized;
204
+ this._text.set(sanitized);
200
205
  // Trim spans that extend beyond new length
201
206
  const len = sanitized.length;
202
- this._spans = this._spans
207
+ this._spans.set(this._spans.get()
203
208
  .map((s) => (s.end > len ? new Span(s.start, Math.min(s.end, len), s.style) : s))
204
- .filter((s) => s.start < len);
209
+ .filter((s) => s.start < len));
205
210
  }
206
211
  get length() {
207
- return this._text.length;
212
+ return this._text.get().length;
208
213
  }
209
214
  /**
210
215
  * The cells of the plain text with each tab widened to its stop counted from
@@ -215,44 +220,44 @@ export class RichText {
215
220
  * no render draws.
216
221
  */
217
222
  get cellLength() {
218
- return cellLen(expandTabs(this._text, this._tabSize).text);
223
+ return cellLen(expandTabs(this._text.get(), this._tabSize).text);
219
224
  }
220
225
  get hasContent() {
221
- return this._text.length > 0;
226
+ return this._text.get().length > 0;
222
227
  }
223
228
  /** The base style every span layers over, resolved at render unless it is a `Style`. */
224
229
  get style() {
225
- return this._style;
230
+ return this._format.get().style;
226
231
  }
227
232
  set style(value) {
228
- this._style = admitStyle(value);
233
+ this._reformat({ style: admitStyle(value) });
229
234
  }
230
235
  get justify() {
231
- return this._justify;
236
+ return this._format.get().justify;
232
237
  }
233
238
  set justify(value) {
234
- this._justify = value;
239
+ this._reformat({ justify: value });
235
240
  }
236
241
  get overflow() {
237
- return this._overflow;
242
+ return this._format.get().overflow;
238
243
  }
239
244
  set overflow(value) {
240
- this._overflow = value;
245
+ this._reformat({ overflow: value });
241
246
  }
242
247
  get end() {
243
- return this._end;
248
+ return this._format.get().end;
244
249
  }
245
250
  set end(value) {
246
- this._end = value;
251
+ this._reformat({ end: value });
247
252
  }
248
253
  get noWrap() {
249
- return this._noWrap;
254
+ return this._format.get().noWrap;
250
255
  }
251
256
  set noWrap(value) {
252
- this._noWrap = value;
257
+ this._reformat({ noWrap: value });
253
258
  }
254
259
  get spans() {
255
- return this._spans;
260
+ return this._spans.get();
256
261
  }
257
262
  /**
258
263
  * This text's own style, spans aside, as the render drawing it resolves it.
@@ -260,7 +265,7 @@ export class RichText {
260
265
  * text forgives a missing name.
261
266
  */
262
267
  resolvedStyle(options) {
263
- return resolveStyle(options, this._style);
268
+ return resolveStyle(options, this._format.get().style);
264
269
  }
265
270
  /**
266
271
  * The style of the cell-column at the named edge — base style merged with
@@ -284,11 +289,11 @@ export class RichText {
284
289
  */
285
290
  edgeStyle(side, options) {
286
291
  const base = this.resolvedStyle(options);
287
- if (this._text.length === 0)
292
+ if (this._text.get().length === 0)
288
293
  return base;
289
- const pos = side === "left" ? 0 : this._text.length - 1;
294
+ const pos = side === "left" ? 0 : this._text.get().length - 1;
290
295
  let result = base;
291
- for (const span of this._spans) {
296
+ for (const span of this._spans.get()) {
292
297
  if (span.start <= pos && pos < span.end) {
293
298
  result = result.add(resolveStyle(options, span.style));
294
299
  }
@@ -301,35 +306,36 @@ export class RichText {
301
306
  if (style !== undefined) {
302
307
  throw new Error("Style argument must not be provided when appending RichText");
303
308
  }
304
- const offset = this._text.length;
305
- this._text += content._text;
309
+ const offset = this._text.get().length;
310
+ // Read before anything is appended, so a text appended to itself takes its spans as they were.
311
+ const moved = content._spans.get().map((span) => span.move(offset));
312
+ this._text.set(this._text.get() + content._text.get());
306
313
  // Its base style becomes a span under its own spans, as the reference's
307
314
  // `append` does, so appended text looks as it did alone.
308
- this._addSpan(offset, this._text.length, content._style);
309
- for (const span of content._spans) {
310
- this._spans.push(span.move(offset));
311
- }
315
+ this._addSpan(offset, this._text.get().length, content._format.get().style);
316
+ for (const span of moved)
317
+ this._spans.push(span);
312
318
  return this;
313
319
  }
314
320
  const sanitized = stripControlChars(content);
315
- const start = this._text.length;
316
- this._text += sanitized;
317
- this._addSpan(start, this._text.length, style ?? "");
321
+ const start = this._text.get().length;
322
+ this._text.set(this._text.get() + sanitized);
323
+ this._addSpan(start, this._text.get().length, style ?? "");
318
324
  return this;
319
325
  }
320
326
  contains(needle) {
321
- const searchText = needle instanceof RichText ? needle._text : needle;
322
- return this._text.includes(searchText);
327
+ const searchText = needle instanceof RichText ? needle._text.get() : needle;
328
+ return this._text.get().includes(searchText);
323
329
  }
324
330
  at(index) {
325
- const resolved = index < 0 ? this._text.length + index : index;
326
- const char = this._text[resolved];
331
+ const resolved = index < 0 ? this._text.get().length + index : index;
332
+ const char = this._text.get()[resolved];
327
333
  if (char === undefined)
328
334
  return new RichText("");
329
335
  return this.slice(resolved, resolved + 1);
330
336
  }
331
337
  slice(start, end) {
332
- const text = this._text;
338
+ const text = this._text.get();
333
339
  const len = text.length;
334
340
  const s = start ?? 0;
335
341
  const e = end ?? len;
@@ -340,7 +346,7 @@ export class RichText {
340
346
  }
341
347
  const slicedText = text.slice(resolvedStart, resolvedEnd);
342
348
  const result = this.blankCopy(slicedText);
343
- for (const span of this._spans) {
349
+ for (const span of this._spans.get()) {
344
350
  const spanStart = Math.max(span.start, resolvedStart) - resolvedStart;
345
351
  const spanEnd = Math.min(span.end, resolvedEnd) - resolvedStart;
346
352
  if (spanStart < spanEnd) {
@@ -362,7 +368,7 @@ export class RichText {
362
368
  this._spans.push(new Span(start, end, admitted));
363
369
  }
364
370
  stylize(style, start, end) {
365
- const len = this._text.length;
371
+ const len = this._text.get().length;
366
372
  const s = start !== undefined ? (start < 0 ? len + start : start) : 0;
367
373
  const e = end !== undefined ? (end < 0 ? len + end : end) : len;
368
374
  if (s >= e || s >= len || e <= 0)
@@ -373,7 +379,7 @@ export class RichText {
373
379
  return this;
374
380
  }
375
381
  highlightRegex(pattern, style) {
376
- const text = this._text;
382
+ const text = this._text.get();
377
383
  let count = 0;
378
384
  // Ensure global flag
379
385
  const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
@@ -415,7 +421,7 @@ export class RichText {
415
421
  const flags = caseSensitive ? "g" : "gi";
416
422
  const re = new RegExp(`\\b${escaped}\\b`, flags);
417
423
  let match;
418
- while ((match = re.exec(this._text)) !== null) {
424
+ while ((match = re.exec(this._text.get())) !== null) {
419
425
  this._addSpan(match.index, match.index + match[0].length, style);
420
426
  count++;
421
427
  }
@@ -424,31 +430,17 @@ export class RichText {
424
430
  }
425
431
  // --- Copy Operations ---
426
432
  copy() {
427
- const result = new RichText(this._text, {
428
- style: this._style,
429
- justify: this._justify,
430
- overflow: this._overflow,
431
- end: this._end,
432
- tabSize: this._tabSize,
433
- noWrap: this._noWrap,
434
- });
435
- result._spans = this._spans.slice();
433
+ const result = this.blankCopy(this._text.get());
434
+ result._spans.set(this._spans.get());
436
435
  return result;
437
436
  }
438
437
  blankCopy(text) {
439
- return new RichText(text ?? "", {
440
- style: this._style,
441
- justify: this._justify,
442
- overflow: this._overflow,
443
- end: this._end,
444
- tabSize: this._tabSize,
445
- noWrap: this._noWrap,
446
- });
438
+ return new RichText(text ?? "", { ...this._format.get(), tabSize: this._tabSize });
447
439
  }
448
440
  // --- Splitting ---
449
441
  split(separator) {
450
442
  const sep = separator ?? "\n";
451
- const text = this._text;
443
+ const text = this._text.get();
452
444
  const parts = [];
453
445
  let start = 0;
454
446
  while (true) {
@@ -476,8 +468,8 @@ export class RichText {
476
468
  }
477
469
  // --- Whitespace Operations ---
478
470
  rstrip() {
479
- const trimmed = this._text.replace(/\s+$/, "");
480
- if (trimmed.length < this._text.length) {
471
+ const trimmed = this._text.get().replace(/\s+$/, "");
472
+ if (trimmed.length < this._text.get().length) {
481
473
  this.plain = trimmed;
482
474
  }
483
475
  return this;
@@ -485,8 +477,8 @@ export class RichText {
485
477
  pad(count, char) {
486
478
  const padding = stripControlChars(char ?? " ").repeat(count);
487
479
  // Shift all spans right by what the left side gained.
488
- this._spans = this._spans.map((s) => s.move(padding.length));
489
- this._text = padding + this._text + padding;
480
+ this._spans.set(this._spans.get().map((s) => s.move(padding.length)));
481
+ this._text.set(padding + this._text.get() + padding);
490
482
  return this;
491
483
  }
492
484
  /**
@@ -495,35 +487,35 @@ export class RichText {
495
487
  * the text measures what it will draw.
496
488
  */
497
489
  expandTabs() {
498
- const expanded = expandTabs(this._text, this._tabSize);
499
- this._spans = this._spans.map((s) => new Span(expanded.offsetOf(s.start), expanded.offsetOf(s.end), s.style));
500
- this._text = expanded.text;
490
+ const expanded = expandTabs(this._text.get(), this._tabSize);
491
+ this._spans.set(this._spans.get().map((s) => new Span(expanded.offsetOf(s.start), expanded.offsetOf(s.end), s.style)));
492
+ this._text.set(expanded.text);
501
493
  return this;
502
494
  }
503
495
  padLeft(count, char) {
504
496
  const padding = stripControlChars(char ?? " ").repeat(count);
505
- this._spans = this._spans.map((s) => s.move(padding.length));
506
- this._text = padding + this._text;
497
+ this._spans.set(this._spans.get().map((s) => s.move(padding.length)));
498
+ this._text.set(padding + this._text.get());
507
499
  return this;
508
500
  }
509
501
  padRight(count, char) {
510
- this._text += stripControlChars(char ?? " ").repeat(count);
502
+ this._text.set(this._text.get() + stripControlChars(char ?? " ").repeat(count));
511
503
  return this;
512
504
  }
513
505
  setLength(length) {
514
- if (this._text.length < length) {
515
- this._text += " ".repeat(length - this._text.length);
506
+ if (this._text.get().length < length) {
507
+ this._text.set(this._text.get() + " ".repeat(length - this._text.get().length));
516
508
  }
517
- else if (this._text.length > length) {
518
- this.plain = this._text.slice(0, length);
509
+ else if (this._text.get().length > length) {
510
+ this.plain = this._text.get().slice(0, length);
519
511
  }
520
512
  return this;
521
513
  }
522
514
  extendStyle(count) {
523
- const oldLen = this._text.length;
524
- this._text += " ".repeat(count);
515
+ const oldLen = this._text.get().length;
516
+ this._text.set(this._text.get() + " ".repeat(count));
525
517
  // Extend existing spans to cover new spaces
526
- this._spans = this._spans.map((s) => s.end === oldLen ? s.extend(count) : s);
518
+ this._spans.set(this._spans.get().map((s) => s.end === oldLen ? s.extend(count) : s));
527
519
  return this;
528
520
  }
529
521
  // --- Truncation ---
@@ -571,14 +563,14 @@ export class RichText {
571
563
  if (mode === "right") {
572
564
  this._cropRightTo(budget);
573
565
  if (marker)
574
- this._text += marker;
566
+ this._text.set(this._text.get() + marker);
575
567
  return this;
576
568
  }
577
569
  if (mode === "left") {
578
570
  this._cropLeftTo(budget);
579
571
  if (marker) {
580
- this._spans = this._spans.map((s) => s.move(marker.length));
581
- this._text = marker + this._text;
572
+ this._spans.set(this._spans.get().map((s) => s.move(marker.length)));
573
+ this._text.set(marker + this._text.get());
582
574
  }
583
575
  return this;
584
576
  }
@@ -586,17 +578,17 @@ export class RichText {
586
578
  const leftBudget = asCellCol(Math.floor(budget / 2));
587
579
  const rightBudget = asCellCol(budget - leftBudget);
588
580
  // Find the char-index ranges to keep from each side.
589
- const leftEndCharIdx = cellFit(this._text, leftBudget).length;
590
- const rightStartCharIdx = this._text.length - cellFitEnd(this._text, rightBudget).length;
591
- const leftText = this._text.slice(0, leftEndCharIdx);
592
- const rightText = this._text.slice(rightStartCharIdx);
581
+ const leftEndCharIdx = cellFit(this._text.get(), leftBudget).length;
582
+ const rightStartCharIdx = this._text.get().length - cellFitEnd(this._text.get(), rightBudget).length;
583
+ const leftText = this._text.get().slice(0, leftEndCharIdx);
584
+ const rightText = this._text.get().slice(rightStartCharIdx);
593
585
  const droppedStart = leftEndCharIdx;
594
586
  const droppedEnd = rightStartCharIdx;
595
587
  const shift = marker.length - (droppedEnd - droppedStart);
596
588
  // Spans falling entirely before the drop stay; entirely after shift by
597
589
  // (marker.length - dropped chars); spans crossing the drop are clipped.
598
590
  const newSpans = [];
599
- for (const s of this._spans) {
591
+ for (const s of this._spans.get()) {
600
592
  if (s.end <= droppedStart) {
601
593
  newSpans.push(s);
602
594
  }
@@ -613,18 +605,18 @@ export class RichText {
613
605
  }
614
606
  }
615
607
  }
616
- this._text = leftText + marker + rightText;
617
- this._spans = newSpans;
608
+ this._text.set(leftText + marker + rightText);
609
+ this._spans.set(newSpans);
618
610
  return this;
619
611
  }
620
612
  _cropRightTo(targetWidth) {
621
- this.plain = cellFit(this._text, targetWidth);
613
+ this.plain = cellFit(this._text.get(), targetWidth);
622
614
  }
623
615
  _cropLeftTo(targetWidth) {
624
- const charIdx = this._text.length - cellFitEnd(this._text, targetWidth).length;
616
+ const charIdx = this._text.get().length - cellFitEnd(this._text.get(), targetWidth).length;
625
617
  // Shift spans left by charIdx; clip spans that started before.
626
618
  const shift = -charIdx;
627
- this._spans = this._spans
619
+ this._spans.set(this._spans.get()
628
620
  .map((s) => {
629
621
  if (s.end <= charIdx)
630
622
  return undefined;
@@ -632,8 +624,8 @@ export class RichText {
632
624
  const newEnd = s.end + shift;
633
625
  return new Span(newStart, newEnd, s.style);
634
626
  })
635
- .filter((s) => s !== undefined);
636
- this._text = this._text.slice(charIdx);
627
+ .filter((s) => s !== undefined));
628
+ this._text.set(this._text.get().slice(charIdx));
637
629
  }
638
630
  // --- Alignment ---
639
631
  align(justify, width) {
@@ -663,8 +655,8 @@ export class RichText {
663
655
  }
664
656
  // --- Suffix Removal ---
665
657
  removeSuffix(suffix) {
666
- if (this._text.endsWith(suffix)) {
667
- this.plain = this._text.slice(0, -suffix.length);
658
+ if (this._text.get().endsWith(suffix)) {
659
+ this.plain = this._text.get().slice(0, -suffix.length);
668
660
  }
669
661
  return this;
670
662
  }
@@ -721,7 +713,7 @@ export class RichText {
721
713
  }
722
714
  // --- Renderable ---
723
715
  *render(options) {
724
- const expanded = expandTabs(this._text, this._tabSize);
716
+ const expanded = expandTabs(this._text.get(), this._tabSize);
725
717
  const base = this.resolvedStyle(options);
726
718
  const { segments: allSegments, covered } = this._buildSegments(expanded, base, options);
727
719
  const spanFree = spanFreeLines(allSegments, covered);
@@ -739,8 +731,8 @@ export class RichText {
739
731
  // overflow arm a no-op, but passed an unbounded one through to `justify`,
740
732
  // which pads — and `" ".repeat(Infinity)` throws.
741
733
  const maxWidth = cellCount(withBoundedWidth(options, this).maxWidth);
742
- const overflow = this._overflow ?? options.overflow ?? "fold";
743
- const noWrap = this._noWrap || (options.noWrap ?? false);
734
+ const overflow = this._format.get().overflow ?? options.overflow ?? "fold";
735
+ const noWrap = this._format.get().noWrap || (options.noWrap ?? false);
744
736
  // Two widths, and they are the reference's `no_wrap` and `"ignore"` told
745
737
  // apart. `edge` is where a line is cut: gone under `"ignore"`, so the line
746
738
  // leaves at its natural width and whatever drew it decides about the
@@ -759,7 +751,7 @@ export class RichText {
759
751
  // A line with no edge has no width to be justified in, so under `"ignore"`
760
752
  // it is not justified — the reference's `Text.wrap` returns that arm before
761
753
  // `justify`. `undefined` is the placement that leaves a line as it is.
762
- const justify = overflow === "ignore" ? undefined : this._justify ?? options.justify;
754
+ const justify = overflow === "ignore" ? undefined : this._format.get().justify ?? options.justify;
763
755
  // One cell either way, so the marker's stand-in leaves the cut where it was.
764
756
  const ellipsis = drawable(options, "\u2026", ".");
765
757
  for (let index = 0; index < logicalLines.length; index += 1) {
@@ -787,13 +779,13 @@ export class RichText {
787
779
  // (truthy) and again for non-empty text (truthy and not the default
788
780
  // "\n"), which gave `end` two meanings depending on whether the text was
789
781
  // empty (rich-text-5ai).
790
- if (this._end) {
791
- yield new Segment(this._end);
782
+ if (this._format.get().end) {
783
+ yield new Segment(this._format.get().end);
792
784
  }
793
785
  }
794
786
  // --- Measurable ---
795
787
  measure(options) {
796
- const lines = expandTabs(this._text, this._tabSize).text.split("\n");
788
+ const lines = expandTabs(this._text.get(), this._tabSize).text.split("\n");
797
789
  const maxLineWidth = lines.reduce((widest, line) => Math.max(widest, cellLen(line)), 0);
798
790
  // [LAW:one-source-of-truth] The minimum is the widest piece the wrapper
799
791
  // never breaks, asked of the wrapper — so a line's indent, which wraps
@@ -848,7 +840,7 @@ export class RichText {
848
840
  // and it is what makes empty text one line to everything that splits a
849
841
  // render into lines — a panel's body, a table cell — where Rich draws one.
850
842
  const inner = new Set();
851
- for (const span of this._spans) {
843
+ for (const span of this._spans.get()) {
852
844
  inner.add(clamp(span.start));
853
845
  inner.add(clamp(span.end));
854
846
  }
@@ -862,7 +854,7 @@ export class RichText {
862
854
  const pieceAt = new Map(boundaries.map((position, piece) => [position, piece]).reverse());
863
855
  const styles = boundaries.slice(0, -1).map(() => base);
864
856
  const covered = styles.map(() => false);
865
- for (const span of this._spans) {
857
+ for (const span of this._spans.get()) {
866
858
  const end = clamp(span.end);
867
859
  const style = resolveStyle(options, span.style);
868
860
  const opensAt = pieceAt.get(clamp(span.start));
@@ -32,12 +32,25 @@
32
32
  * why it is a search and not the ulp walk a sum on one clock takes. A value
33
33
  * moving at the mapped time changes at the host time itself.
34
34
  *
35
+ * A stagger — a Group's children pulsing in sequence, a light walking down
36
+ * a Table's rows — is the same map once per child: `stagger` runs each
37
+ * child's animation on a timeline playing from its index times the delay,
38
+ * and the group changes when the soonest child does, each child's answer
39
+ * carried onto the group's clock by `run`. Before its turn a child is asked
40
+ * for a time before 0, which is its own business: a loop is shifted in
41
+ * phase. Tree, Table, Group and Layout take the children's frames as they
42
+ * take any renderables, through `Animation.map`, so none of them knows a
43
+ * stagger exists. [LAW:composability] A lifecycle is not staggered here: it
44
+ * carries its turn as `entered`, on the host's clock its `leave` and `gone`
45
+ * take, so moving it onto a shifted clock would lose them
46
+ * ([LAW:one-source-of-truth] for when it arrives).
47
+ *
35
48
  * [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. The host
36
49
  * time is an argument everywhere, and a timeline is built from one the host
37
50
  * hands in, so the same host time maps to the same animation time at one
38
51
  * sample a second or sixty.
39
52
  */
40
- import type { Animation } from "./animation.js";
53
+ import { Animation, type Timed } from "./animation.js";
41
54
  /**
42
55
  * A map from host time to animation time. Playing, it is `time` at `origin`
43
56
  * moving `rate` animation seconds a host second; paused, it is `time` at
@@ -76,9 +89,21 @@ export declare const Timeline: {
76
89
  * host time to, changing at the host moment the mapped time reaches the
77
90
  * animation's own answer — backwards, when it drops under where the value
78
91
  * began. Paused, the value never changes and never did. The value is the
79
- * animation's own, on no clock: what a drawn value's cells say about when
80
- * they change is stamped by whoever draws it at a host time
81
- * (`../renderables/frames.ts`, `frameAt`), never carried in from here.
92
+ * animation's own, on no clock: placed, it is stamped at the frame's time
93
+ * (`placedRender`, `./animation.ts`) with this animation's answers, so a
94
+ * value's contents follow the frame's clock and not this map.
95
+ * Reduced, it is the animation reduced on the same timeline: what shows
96
+ * still follows the timeline, and only the motion is gone.
97
+ */
98
+ run<T>(timeline: Timeline, animation: Timed<T>): Animation<T>;
99
+ /**
100
+ * Every one of `animations` at once, each on a timeline playing from its
101
+ * index times `delay` seconds after the earliest, their values in order, on
102
+ * a clock of the stagger's own from 0. It changes when the soonest of them
103
+ * does, on that clock (`Animation.all` of `run`s). A delay of 0 starts them
104
+ * together and a negative one starts the last at 0 and the first last.
105
+ * Reduced, the offsets are the cascade's choreography, which is motion: every
106
+ * child reduced at once, so from 0 everyone has arrived.
82
107
  */
83
- run<T>(timeline: Timeline, animation: Animation<T>): Animation<T>;
108
+ stagger<T>(animations: readonly Timed<T>[], delay: number): Animation<readonly T[]>;
84
109
  };