@promptctl/rich-js 0.24.0 → 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 (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  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 +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -41,7 +41,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
41
41
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
42
42
  done = true;
43
43
  };
44
- import { observableRef, action } from "mobx";
44
+ import { action } from "mobx";
45
+ import { observableRef } from "./observable.js";
45
46
  import { Style } from "../core/style.js";
46
47
  import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
47
48
  import { WidgetBase } from "./widget-base.js";
@@ -44,7 +44,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
44
44
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
45
45
  done = true;
46
46
  };
47
- import { observable, action, observableRef } from "mobx";
47
+ import { action } from "mobx";
48
+ import { observableRef } from "./observable.js";
48
49
  import { Segment } from "../core/segment.js";
49
50
  import { Style } from "../core/style.js";
50
51
  import { cellLen } from "../core/cells.js";
@@ -79,8 +80,8 @@ let Toggle = (() => {
79
80
  return class Toggle extends _classSuper {
80
81
  static {
81
82
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
82
- _label_decorators = [observable];
83
- _on_decorators = [observable];
83
+ _label_decorators = [observableRef];
84
+ _on_decorators = [observableRef];
84
85
  _variant_decorators = [observableRef];
85
86
  _handleKey_decorators = [action];
86
87
  _handleMouse_decorators = [action];
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
9
9
  import type { Segment } from "../core/segment.js";
10
+ import type { Anchor } from "../core/anchor.js";
10
11
  import type { Unsubscribe } from "../core/subscription.js";
11
12
  import type { KeyPress, ScreenMouseEvent } from "../host/input.js";
12
13
  export declare class KeyEvent implements KeyPress {
@@ -15,8 +16,9 @@ export declare class KeyEvent implements KeyPress {
15
16
  readonly shift: boolean;
16
17
  readonly ctrl: boolean;
17
18
  readonly meta: boolean;
19
+ readonly time: number;
18
20
  private _stopped;
19
- constructor(init: KeyPress);
21
+ constructor(init: KeyPress, time: number);
20
22
  get stopped(): boolean;
21
23
  stop(): void;
22
24
  }
@@ -28,9 +30,12 @@ export type WidgetMouseEvent = Extract<ScreenMouseEvent, {
28
30
  type: "mouse_down" | "mouse_up" | "mouse_move";
29
31
  }> & {
30
32
  readonly over: boolean;
33
+ readonly cell: Anchor | undefined;
34
+ readonly time: number;
31
35
  };
32
36
  export interface WidgetFocusEvent {
33
- type: "focus" | "blur";
37
+ readonly type: "focus" | "blur";
38
+ readonly time: number;
34
39
  }
35
40
  export interface InteractiveWidget extends Renderable, Measurable {
36
41
  readonly id: string;
@@ -42,8 +47,8 @@ export interface InteractiveWidget extends Renderable, Measurable {
42
47
  handleKey(event: KeyEvent): void;
43
48
  handleMouse(event: WidgetMouseEvent): void;
44
49
  handleFocus(event: WidgetFocusEvent): void;
45
- focus(): void;
46
- blur(): void;
50
+ focus(time: number): void;
51
+ blur(time: number): void;
47
52
  setDisabled(value: boolean): void;
48
53
  setHovered(value: boolean): void;
49
54
  onChange(handler: (widget: InteractiveWidget) => void): Unsubscribe;
@@ -66,5 +71,6 @@ export interface FocusManager {
66
71
  export interface FrameSource {
67
72
  readonly focusManager: FocusManager;
68
73
  readonly frame: readonly (readonly Segment[])[];
74
+ readonly frameTime: number;
69
75
  refresh(): void;
70
76
  }
@@ -6,8 +6,12 @@
6
6
  * terminal escape sequences, or their host environment.
7
7
  */
8
8
  // --- Event types ---
9
+ // Every event a widget hears carries `time`: when it was heard, on the clock
10
+ // the frame's cells are read on (`RenderOptions.frame`), so a widget
11
+ // keeping the moment of its last interaction — a caret's blink restarting —
12
+ // compares it with the frame it draws and never reads a clock.
9
13
  // [LAW:types-are-the-program] KeyEvent is a `KeyPress` the app heard, plus
10
- // the say over who else hears it. It carries a single mutable signal
14
+ // when it was heard and the say over who else hears it. It carries a single mutable signal
11
15
  // (`stopped`) that participants in the dispatch chain set by calling
12
16
  // `stop()`. Treating it as an interface would force every handler to
13
17
  // shuttle a return value upward and the router to interpret it — the
@@ -19,13 +23,15 @@ export class KeyEvent {
19
23
  shift;
20
24
  ctrl;
21
25
  meta;
26
+ time;
22
27
  _stopped = false;
23
- constructor(init) {
28
+ constructor(init, time) {
24
29
  this.key = init.key;
25
30
  this.character = init.character;
26
31
  this.shift = init.shift;
27
32
  this.ctrl = init.ctrl;
28
33
  this.meta = init.meta;
34
+ this.time = time;
29
35
  }
30
36
  get stopped() { return this._stopped; }
31
37
  // Claim this key. Halts further chain dispatch — no high/normal handler
@@ -7,8 +7,10 @@
7
7
  * `Table` cell — and nothing lays it out a second time. Everything added here
8
8
  * reads the frame the view drew:
9
9
  *
10
- * - Focus moves in document order: the order widgets start to render in
11
- * (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
10
+ * - Focus moves in document order: the order the view reports its widgets in
11
+ * (`RenderOptions.onPresent`), among those with a cell on the frame on screen.
12
+ * A widget its container hands no column is reported held, not drawn: it is
13
+ * in the view, so it keeps focus, and it has no cell, so Tab passes it.
12
14
  * Not reading order — in a `Layout` split into columns, reading order
13
15
  * zigzags between the columns row by row.
14
16
  * - Overlays are painted over the view's frame, in document order, directly
@@ -24,10 +26,14 @@
24
26
  * hear the input.
25
27
  */
26
28
  import { Segment } from "../core/segment.js";
29
+ import type { FrameHost, Presence } from "../core/protocol.js";
27
30
  import type { Unsubscribe } from "../core/subscription.js";
31
+ import type { Clock, FrameRate } from "../core/clock.js";
32
+ import type { Timeline } from "../core/timeline.js";
28
33
  import { type AppOptions, type AppPhase, type Wheel } from "../host/app.js";
29
34
  import type { FocusManager, FrameSource, KeyEvent, KeyHandlerOptions } from "./types.js";
30
35
  import type { AppMouseEvent } from "../host/app.js";
36
+ import { WidgetBase } from "./widget-base.js";
31
37
  export interface WidgetAppOptions extends AppOptions {
32
38
  /**
33
39
  * Whether a left-button press focuses the widget under it, before it hears the
@@ -35,17 +41,39 @@ export interface WidgetAppOptions extends AppOptions {
35
41
  */
36
42
  readonly focusOnClick?: boolean;
37
43
  }
38
- export declare class WidgetApp implements FrameSource {
44
+ /** A widget the view reported for a frame, and whether it was drawn or held. */
45
+ export interface WidgetPresence {
46
+ readonly widget: WidgetBase;
47
+ readonly presence: Presence;
48
+ }
49
+ export declare class WidgetApp implements FrameSource, FrameHost {
39
50
  readonly focusManager: FocusManager;
40
51
  private readonly focus;
41
52
  private readonly app;
42
53
  private readonly router;
43
54
  private readonly reaction;
44
- private drawn;
55
+ private _present;
45
56
  constructor(options: WidgetAppOptions);
46
57
  get phase(): AppPhase;
58
+ /** The clock the app reads the time from: where the host time a `Timeline` operation takes is read. */
59
+ get clock(): Clock;
60
+ /** The map from the clock to the time the view is drawn for; setting it paints the frame for the new time, as `App.timeline` does. */
61
+ get timeline(): Timeline;
62
+ set timeline(timeline: Timeline);
63
+ /** How often the app paints while what it shows moves; setting it goes on at the new rate from the next frame, as `App.rate` does. */
64
+ get rate(): FrameRate;
65
+ set rate(rate: FrameRate);
66
+ /**
67
+ * The widgets the view reported for the frame on screen, in document order,
68
+ * each drawn or held (`RenderOptions.onPresent`). A held widget is one its
69
+ * container handed no column: in the view, with no cell on the frame.
70
+ */
71
+ get present(): readonly WidgetPresence[];
72
+ private get widgets();
47
73
  /** The rows on screen now, as painted: what a pointer event lands on. */
48
74
  get frame(): readonly (readonly Segment[])[];
75
+ /** The time a frame drawn now is drawn for, as `App.frameTime` reads it. */
76
+ get frameTime(): number;
49
77
  /**
50
78
  * Take the terminal, paint the first frame, and route input to its widgets
51
79
  * until the app stops — `App.run`, with the input.
@@ -60,8 +88,6 @@ export declare class WidgetApp implements FrameSource {
60
88
  */
61
89
  refresh(): void;
62
90
  suspend(): Promise<void>;
63
- /** Paint at the app's rate until the returned function is called, as `App.animate` does. */
64
- animate(): Unsubscribe;
65
91
  /**
66
92
  * Hear every key the app does not quit on. A `high` handler hears it before the focused widget, a
67
93
  * `normal` one after it — and after Tab has moved focus — and a handler
@@ -7,8 +7,10 @@
7
7
  * `Table` cell — and nothing lays it out a second time. Everything added here
8
8
  * reads the frame the view drew:
9
9
  *
10
- * - Focus moves in document order: the order widgets start to render in
11
- * (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
10
+ * - Focus moves in document order: the order the view reports its widgets in
11
+ * (`RenderOptions.onPresent`), among those with a cell on the frame on screen.
12
+ * A widget its container hands no column is reported held, not drawn: it is
13
+ * in the view, so it keeps focus, and it has no cell, so Tab passes it.
12
14
  * Not reading order — in a `Layout` split into columns, reading order
13
15
  * zigzags between the columns row by row.
14
16
  * - Overlays are painted over the view's frame, in document order, directly
@@ -25,6 +27,8 @@
25
27
  */
26
28
  import { Reaction } from "mobx";
27
29
  import { Segment } from "../core/segment.js";
30
+ import { render } from "../core/protocol.js";
31
+ import { hostView } from "../core/view.js";
28
32
  import { App } from "../host/app.js";
29
33
  import { DefaultFocusManager } from "./focus-manager.js";
30
34
  import { EventRouter } from "./event-router.js";
@@ -38,18 +42,30 @@ export class WidgetApp {
38
42
  router;
39
43
  // Paints the next frame when anything the last one read changes.
40
44
  reaction;
41
- // The widgets the view drew for the frame on screen, in document order.
42
- drawn = [];
45
+ _present = [];
43
46
  constructor(options) {
44
- const { view, focusOnClick = true } = options;
47
+ const { focusOnClick = true } = options;
48
+ const view = hostView(options.view);
45
49
  this.reaction = new Reaction("WidgetApp", () => this.app.refresh());
50
+ // The view is drawn inside `compose`, where what it reads is tracked —
51
+ // a function view asked there too — and when it changes is its own to
52
+ // say, as it would be to the App.
53
+ // Its frames are its own, drawn for its `frameTime`: what draws in them
54
+ // is told this app is the host, not the App it paints through, so
55
+ // `drawnBy: widgetApp` names the host its frames carry.
46
56
  this.app = new App({
47
57
  ...options,
48
- view: (t) => ({ render: (renderOptions) => this.compose(() => view(t), renderOptions) }),
58
+ view: {
59
+ render: (renderOptions) => this.compose(view, { ...renderOptions, frame: renderOptions.frame && { time: renderOptions.frame.time, host: this } }),
60
+ },
49
61
  });
50
- this.focus = new DefaultFocusManager(() => {
51
- const owners = ownersOn(this.app.frame);
52
- return this.drawn.filter((widget) => owners.has(widget));
62
+ this.focus = new DefaultFocusManager({
63
+ present: () => this.widgets,
64
+ onScreen: () => {
65
+ const owners = ownersOn(this.app.frame);
66
+ return this.widgets.filter((widget) => owners.has(widget));
67
+ },
68
+ frameTime: () => this.frameTime,
53
69
  });
54
70
  this.focusManager = this.focus;
55
71
  this.router = new EventRouter({ source: this, focusOnClick });
@@ -59,7 +75,7 @@ export class WidgetApp {
59
75
  this.app.onMouse((event) => this.router.mouse(event));
60
76
  this.app.onWheel((wheel) => this.router.wheel(wheel));
61
77
  this.app.onPaint(() => {
62
- this.focus.settle(this.drawn);
78
+ this.focus.settle();
63
79
  this.router.framePainted();
64
80
  });
65
81
  // [LAW:single-enforcer] `settle` judges every focus move, so every move
@@ -69,10 +85,43 @@ export class WidgetApp {
69
85
  get phase() {
70
86
  return this.app.phase;
71
87
  }
88
+ /** The clock the app reads the time from: where the host time a `Timeline` operation takes is read. */
89
+ get clock() {
90
+ return this.app.clock;
91
+ }
92
+ /** The map from the clock to the time the view is drawn for; setting it paints the frame for the new time, as `App.timeline` does. */
93
+ get timeline() {
94
+ return this.app.timeline;
95
+ }
96
+ set timeline(timeline) {
97
+ this.app.timeline = timeline;
98
+ }
99
+ /** How often the app paints while what it shows moves; setting it goes on at the new rate from the next frame, as `App.rate` does. */
100
+ get rate() {
101
+ return this.app.rate;
102
+ }
103
+ set rate(rate) {
104
+ this.app.rate = rate;
105
+ }
106
+ /**
107
+ * The widgets the view reported for the frame on screen, in document order,
108
+ * each drawn or held (`RenderOptions.onPresent`). A held widget is one its
109
+ * container handed no column: in the view, with no cell on the frame.
110
+ */
111
+ get present() {
112
+ return this._present;
113
+ }
114
+ get widgets() {
115
+ return this._present.map(({ widget }) => widget);
116
+ }
72
117
  /** The rows on screen now, as painted: what a pointer event lands on. */
73
118
  get frame() {
74
119
  return this.app.frame;
75
120
  }
121
+ /** The time a frame drawn now is drawn for, as `App.frameTime` reads it. */
122
+ get frameTime() {
123
+ return this.app.frameTime;
124
+ }
76
125
  /**
77
126
  * Take the terminal, paint the first frame, and route input to its widgets
78
127
  * until the app stops — `App.run`, with the input.
@@ -106,10 +155,6 @@ export class WidgetApp {
106
155
  suspend() {
107
156
  return this.app.suspend();
108
157
  }
109
- /** Paint at the app's rate until the returned function is called, as `App.animate` does. */
110
- animate() {
111
- return this.app.animate();
112
- }
113
158
  /**
114
159
  * Hear every key the app does not quit on. A `high` handler hears it before the focused widget, a
115
160
  * `normal` one after it — and after Tab has moved focus — and a handler
@@ -131,26 +176,28 @@ export class WidgetApp {
131
176
  return this.app.onWheel(handler);
132
177
  }
133
178
  compose(view, options) {
134
- const drawn = new Set();
135
- // [LAW:parse-dont-validate] An owner is any object; only a `WidgetBase`
136
- // reports itself as a widget, so this is where `object` becomes one.
137
- const onDraw = (owner) => {
138
- if (owner instanceof WidgetBase)
139
- drawn.add(owner);
179
+ // Each widget once, where it was first reported; drawn anywhere, it is drawn.
180
+ const present = new Map();
181
+ // [LAW:parse-dont-validate] What the view reports is any renderable; only
182
+ // a `WidgetBase` is a widget, so this is where a renderable becomes one.
183
+ const onPresent = (renderable, presence) => {
184
+ if (renderable instanceof WidgetBase)
185
+ present.set(renderable, present.get(renderable) === "drawn" ? "drawn" : presence);
140
186
  };
141
187
  // The view, its render and the overlays are all tracked, so an observable
142
188
  // any of them reads — a label, `focused`, the state that picks what the
143
189
  // view shows — paints the next frame when it changes.
144
- // An overlay reports what it draws too: a widget in it is on the frame,
145
- // and visiting `drawn` in insertion order paints its overlay in turn.
146
- const render = { ...options, onDraw };
190
+ // An overlay reports what it draws too: a widget in it is in the view,
191
+ // and visiting `present` in report order paints its overlay in turn. A
192
+ // held widget has no footprint on the frame, and so paints none.
193
+ const drawing = { ...options, onPresent };
147
194
  const lines = tracked(this.reaction, () => {
148
- const base = Segment.splitLines(view().render(render));
149
- for (const widget of drawn)
150
- paintOverlay(base, widget, render);
195
+ const base = Segment.splitLines(render(view, drawing));
196
+ for (const widget of present.keys())
197
+ paintOverlay(base, widget, drawing);
151
198
  return base;
152
199
  });
153
- this.drawn = [...drawn];
200
+ this._present = [...present].map(([widget, presence]) => ({ widget, presence }));
154
201
  return lines.flatMap((line) => [...line, Segment.line()]);
155
202
  }
156
203
  }
@@ -24,8 +24,8 @@ export declare abstract class WidgetBase implements InteractiveWidget {
24
24
  abstract handleKey(event: KeyEvent): void;
25
25
  handleMouse(_event: WidgetMouseEvent): void;
26
26
  handleFocus(event: WidgetFocusEvent): void;
27
- focus(): void;
28
- blur(): void;
27
+ focus(time: number): void;
28
+ blur(time: number): void;
29
29
  setDisabled(value: boolean): void;
30
30
  setHovered(value: boolean): void;
31
31
  onChange(handler: (widget: InteractiveWidget) => void): Unsubscribe;
@@ -41,9 +41,11 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
41
41
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
42
42
  done = true;
43
43
  };
44
- import { observable, action } from "mobx";
44
+ import { action } from "mobx";
45
+ import { observableRef } from "./observable.js";
45
46
  import { Segment } from "../core/segment.js";
46
47
  import { NULL_STYLE } from "../core/style.js";
48
+ import { State } from "../core/state.js";
47
49
  let WidgetBase = (() => {
48
50
  let _instanceExtraInitializers = [];
49
51
  let _focused_decorators;
@@ -66,10 +68,10 @@ let WidgetBase = (() => {
66
68
  return class WidgetBase {
67
69
  static {
68
70
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
69
- _focused_decorators = [observable];
70
- _hovered_decorators = [observable];
71
- _active_decorators = [observable];
72
- _disabled_decorators = [observable];
71
+ _focused_decorators = [observableRef];
72
+ _hovered_decorators = [observableRef];
73
+ _active_decorators = [observableRef];
74
+ _disabled_decorators = [observableRef];
73
75
  _handleFocus_decorators = [action];
74
76
  _focus_decorators = [action];
75
77
  _blur_decorators = [action];
@@ -98,8 +100,9 @@ let WidgetBase = (() => {
98
100
  #disabled_accessor_storage = (__runInitializers(this, _active_extraInitializers), __runInitializers(this, _disabled_initializers, false));
99
101
  get disabled() { return this.#disabled_accessor_storage; }
100
102
  set disabled(value) { this.#disabled_accessor_storage = value; }
101
- changeHandlers = (__runInitializers(this, _disabled_extraInitializers), new Set());
102
- submitHandlers = new Set();
103
+ // Subscribed and let go between frames; each set is replaced whole.
104
+ changeHandlers = (__runInitializers(this, _disabled_extraInitializers), new State(new Set()));
105
+ submitHandlers = new State(new Set());
103
106
  handleMouse(_event) { }
104
107
  handleFocus(event) {
105
108
  this.focused = event.type === "focus";
@@ -109,11 +112,11 @@ let WidgetBase = (() => {
109
112
  // subclass override (e.g. Dropdown collapsing its overlay on blur)
110
113
  // participates in the same transition path the event router uses. There
111
114
  // is no second code path that flips `focused` directly from the outside.
112
- focus() {
113
- this.handleFocus({ type: "focus" });
115
+ focus(time) {
116
+ this.handleFocus({ type: "focus", time });
114
117
  }
115
- blur() {
116
- this.handleFocus({ type: "blur" });
118
+ blur(time) {
119
+ this.handleFocus({ type: "blur", time });
117
120
  }
118
121
  setDisabled(value) {
119
122
  this.disabled = value;
@@ -127,36 +130,32 @@ let WidgetBase = (() => {
127
130
  }
128
131
  // --- Subscriptions ---
129
132
  onChange(handler) {
130
- this.changeHandlers.add(handler);
131
- return () => this.changeHandlers.delete(handler);
133
+ return subscribe(this.changeHandlers, handler);
132
134
  }
133
135
  onSubmit(handler) {
134
- this.submitHandlers.add(handler);
135
- return () => this.submitHandlers.delete(handler);
136
+ return subscribe(this.submitHandlers, handler);
136
137
  }
137
138
  emitChange() {
138
- for (const handler of this.changeHandlers) {
139
+ for (const handler of this.changeHandlers.get()) {
139
140
  handler(this);
140
141
  }
141
142
  }
142
143
  emitSubmit() {
143
- for (const handler of this.submitHandlers) {
144
+ for (const handler of this.submitHandlers.get()) {
144
145
  handler(this);
145
146
  }
146
147
  }
147
148
  // --- Renderable + Measurable ---
148
149
  // [LAW:single-enforcer] The one place a widget's cells get their anchor, so
149
150
  // a hit on any cell names this widget and the cell's place in its output
150
- // (`widgetAt`). Subclasses draw; none stamps itself. A `RichText` admits no
151
+ // (`widgetAt`). Subclasses draw; none stamps itself — what a draw stamps
152
+ // inside this stamp is its own (`WidgetMouseEvent.cell`). A `RichText` admits no
151
153
  // anchor, so stamping has to follow every text layout `draw` does.
152
154
  // Every row is padded to the widest, so the widget owns its whole rectangle
153
155
  // and a click past the end of a short row still reaches it; every row ends
154
156
  // in a newline, so a blank last row survives the next `splitLines`. The
155
157
  // padding stands on `ground`, whatever the widget draws its own cells on.
156
- // Reporting itself before it draws is what puts a widget ahead of the
157
- // widgets nested in it in document order — the order focus traverses.
158
158
  render(options) {
159
- options.onDraw?.(this);
160
159
  const drawn = Segment.splitLines(this.draw(options));
161
160
  const [width] = Segment.getShape(drawn);
162
161
  const lines = Segment.anchorLines(drawn.map((line) => Segment.adjustLineLength(line, width, this.ground)), this);
@@ -170,3 +169,12 @@ let WidgetBase = (() => {
170
169
  };
171
170
  })();
172
171
  export { WidgetBase };
172
+ /** `handler` added to `handlers` until the returned call takes it out. */
173
+ function subscribe(handlers, handler) {
174
+ handlers.set(new Set(handlers.get()).add(handler));
175
+ return () => {
176
+ const left = new Set(handlers.get());
177
+ left.delete(handler);
178
+ handlers.set(left);
179
+ };
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/rich-js",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Rich text and beautiful formatting in the terminal — a TypeScript port of Python's Rich",
5
5
  "repository": {
6
6
  "type": "git",
@@ -74,6 +74,13 @@
74
74
  "sessions": "node scripts/build-demos.ts && node dist-demo/examples/claude-sessions/node.js",
75
75
  "strip": "node scripts/build-demos.ts && node dist-demo/examples/rich-strip/main.js",
76
76
  "effects-feel": "node scripts/build-demos.ts && node dist-demo/examples/effects-feel/index.js",
77
+ "glyph-effects": "node scripts/build-demos.ts && node dist-demo/examples/glyph-effects/index.js",
78
+ "attribute-effects": "node scripts/build-demos.ts && node dist-demo/examples/attribute-effects/index.js",
79
+ "gradient-effects": "node scripts/build-demos.ts && node dist-demo/examples/gradient-effects/index.js",
80
+ "transitions": "node scripts/build-demos.ts && node dist-demo/examples/transitions/index.js",
81
+ "lifecycle": "node scripts/build-demos.ts && node dist-demo/examples/lifecycle/index.js",
82
+ "animation-playground": "node scripts/build-demos.ts && node dist-demo/examples/animation-playground/main.js",
83
+ "scroll": "node scripts/build-demos.ts && node dist-demo/examples/scroll/index.js",
77
84
  "viewport": "node scripts/build-demos.ts && node dist-demo/examples/rich-viewport/main.js",
78
85
  "markup-plugins": "node scripts/build-demos.ts && node dist-demo/examples/rich-markup-plugins/main.js",
79
86
  "demo-inputs": "node scripts/build-demos.ts && node dist-demo/examples/rich-config/main.js",
@@ -101,7 +108,7 @@
101
108
  "devEngines": {
102
109
  "runtime": {
103
110
  "name": "node",
104
- "version": "^22.18.0 || >=23.6.0",
111
+ "version": "^22.19.0 || >=23.9.0",
105
112
  "onFail": "error"
106
113
  }
107
114
  },
@@ -133,9 +140,6 @@
133
140
  "vitepress": "^1.6.4",
134
141
  "vitest": "^5.0.0"
135
142
  },
136
- "dependencies": {
137
- "string-width": "^8.2.2"
138
- },
139
143
  "peerDependencies": {
140
144
  "@promptctl/go-template-js": "^0.10.0",
141
145
  "mobx": "^7.0.3"