@promptctl/rich-js 0.17.0 → 0.18.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 (206) hide show
  1. package/README.md +13 -26
  2. package/package.json +7 -3
  3. package/dist/core/box.d.ts +0 -144
  4. package/dist/core/box.js +0 -391
  5. package/dist/core/cells.d.ts +0 -148
  6. package/dist/core/cells.js +0 -266
  7. package/dist/core/color.d.ts +0 -250
  8. package/dist/core/color.js +0 -1029
  9. package/dist/core/console.d.ts +0 -156
  10. package/dist/core/console.js +0 -472
  11. package/dist/core/emoji.d.ts +0 -24
  12. package/dist/core/emoji.js +0 -391
  13. package/dist/core/export-html.d.ts +0 -24
  14. package/dist/core/export-html.js +0 -106
  15. package/dist/core/export-lines.d.ts +0 -121
  16. package/dist/core/export-lines.js +0 -141
  17. package/dist/core/highlighter.d.ts +0 -33
  18. package/dist/core/highlighter.js +0 -124
  19. package/dist/core/json.d.ts +0 -27
  20. package/dist/core/json.js +0 -53
  21. package/dist/core/markup.d.ts +0 -97
  22. package/dist/core/markup.js +0 -619
  23. package/dist/core/measure.d.ts +0 -27
  24. package/dist/core/measure.js +0 -61
  25. package/dist/core/oklch.d.ts +0 -161
  26. package/dist/core/oklch.js +0 -312
  27. package/dist/core/osc8.d.ts +0 -68
  28. package/dist/core/osc8.js +0 -90
  29. package/dist/core/pretty.d.ts +0 -141
  30. package/dist/core/pretty.js +0 -472
  31. package/dist/core/protocol.d.ts +0 -137
  32. package/dist/core/protocol.js +0 -101
  33. package/dist/core/render.d.ts +0 -84
  34. package/dist/core/render.js +0 -145
  35. package/dist/core/segment.d.ts +0 -142
  36. package/dist/core/segment.js +0 -433
  37. package/dist/core/spinnerData.d.ts +0 -10
  38. package/dist/core/spinnerData.js +0 -115
  39. package/dist/core/strip.d.ts +0 -145
  40. package/dist/core/strip.js +0 -308
  41. package/dist/core/style.d.ts +0 -144
  42. package/dist/core/style.js +0 -723
  43. package/dist/core/subscription.d.ts +0 -12
  44. package/dist/core/subscription.js +0 -12
  45. package/dist/core/text.d.ts +0 -255
  46. package/dist/core/text.js +0 -990
  47. package/dist/core/wrap.d.ts +0 -41
  48. package/dist/core/wrap.js +0 -127
  49. package/dist/host/host-stream.d.ts +0 -30
  50. package/dist/host/host-stream.js +0 -35
  51. package/dist/host/index.d.ts +0 -3
  52. package/dist/host/index.js +0 -2
  53. package/dist/host/terminal-host.d.ts +0 -143
  54. package/dist/host/terminal-host.js +0 -132
  55. package/dist/index.d.ts +0 -87
  56. package/dist/index.js +0 -104
  57. package/dist/node/prompt.d.ts +0 -21
  58. package/dist/node/prompt.js +0 -30
  59. package/dist/node/save.d.ts +0 -17
  60. package/dist/node/save.js +0 -21
  61. package/dist/node/terminal-host.d.ts +0 -84
  62. package/dist/node/terminal-host.js +0 -160
  63. package/dist/node/traceback.d.ts +0 -27
  64. package/dist/node/traceback.js +0 -112
  65. package/dist/renderables/align.d.ts +0 -16
  66. package/dist/renderables/align.js +0 -46
  67. package/dist/renderables/columns.d.ts +0 -61
  68. package/dist/renderables/columns.js +0 -168
  69. package/dist/renderables/constrain.d.ts +0 -15
  70. package/dist/renderables/constrain.js +0 -37
  71. package/dist/renderables/flexStrip.d.ts +0 -41
  72. package/dist/renderables/flexStrip.js +0 -200
  73. package/dist/renderables/group.d.ts +0 -11
  74. package/dist/renderables/group.js +0 -15
  75. package/dist/renderables/layout.d.ts +0 -92
  76. package/dist/renderables/layout.js +0 -315
  77. package/dist/renderables/live.d.ts +0 -47
  78. package/dist/renderables/live.js +0 -131
  79. package/dist/renderables/markdown.d.ts +0 -24
  80. package/dist/renderables/markdown.js +0 -182
  81. package/dist/renderables/padding.d.ts +0 -38
  82. package/dist/renderables/padding.js +0 -140
  83. package/dist/renderables/panel.d.ts +0 -116
  84. package/dist/renderables/panel.js +0 -322
  85. package/dist/renderables/progress.d.ts +0 -90
  86. package/dist/renderables/progress.js +0 -241
  87. package/dist/renderables/progressBar.d.ts +0 -31
  88. package/dist/renderables/progressBar.js +0 -52
  89. package/dist/renderables/prompt.d.ts +0 -49
  90. package/dist/renderables/prompt.js +0 -125
  91. package/dist/renderables/rule.d.ts +0 -24
  92. package/dist/renderables/rule.js +0 -90
  93. package/dist/renderables/spinner.d.ts +0 -29
  94. package/dist/renderables/spinner.js +0 -64
  95. package/dist/renderables/status.d.ts +0 -23
  96. package/dist/renderables/status.js +0 -60
  97. package/dist/renderables/syntax.d.ts +0 -35
  98. package/dist/renderables/syntax.js +0 -114
  99. package/dist/renderables/table.d.ts +0 -182
  100. package/dist/renderables/table.js +0 -697
  101. package/dist/renderables/traceback.d.ts +0 -31
  102. package/dist/renderables/traceback.js +0 -109
  103. package/dist/renderables/tree.d.ts +0 -54
  104. package/dist/renderables/tree.js +0 -159
  105. package/dist/template-bindings/color-funcs.d.ts +0 -87
  106. package/dist/template-bindings/color-funcs.js +0 -219
  107. package/dist/template-bindings/helpers.d.ts +0 -24
  108. package/dist/template-bindings/helpers.js +0 -48
  109. package/dist/template-bindings/index.d.ts +0 -107
  110. package/dist/template-bindings/index.js +0 -144
  111. package/dist/template-bindings/palette-funcs.d.ts +0 -94
  112. package/dist/template-bindings/palette-funcs.js +0 -151
  113. package/dist/template-bindings/style-funcs.d.ts +0 -28
  114. package/dist/template-bindings/style-funcs.js +0 -151
  115. package/dist/themes/buildPalette.d.ts +0 -36
  116. package/dist/themes/buildPalette.js +0 -56
  117. package/dist/themes/colorMath.d.ts +0 -82
  118. package/dist/themes/colorMath.js +0 -254
  119. package/dist/themes/colorRef.d.ts +0 -51
  120. package/dist/themes/colorRef.js +0 -96
  121. package/dist/themes/data/atom-one-dark.d.ts +0 -3
  122. package/dist/themes/data/atom-one-dark.js +0 -158
  123. package/dist/themes/data/atom-one-light.d.ts +0 -3
  124. package/dist/themes/data/atom-one-light.js +0 -158
  125. package/dist/themes/data/catppuccin-frappe.d.ts +0 -3
  126. package/dist/themes/data/catppuccin-frappe.js +0 -158
  127. package/dist/themes/data/catppuccin-latte.d.ts +0 -3
  128. package/dist/themes/data/catppuccin-latte.js +0 -158
  129. package/dist/themes/data/catppuccin-macchiato.d.ts +0 -3
  130. package/dist/themes/data/catppuccin-macchiato.js +0 -158
  131. package/dist/themes/data/catppuccin-mocha.d.ts +0 -3
  132. package/dist/themes/data/catppuccin-mocha.js +0 -158
  133. package/dist/themes/data/cyberpunk.d.ts +0 -17
  134. package/dist/themes/data/cyberpunk.js +0 -191
  135. package/dist/themes/data/default.d.ts +0 -3
  136. package/dist/themes/data/default.js +0 -158
  137. package/dist/themes/data/dracula.d.ts +0 -3
  138. package/dist/themes/data/dracula.js +0 -158
  139. package/dist/themes/data/flexoki.d.ts +0 -3
  140. package/dist/themes/data/flexoki.js +0 -158
  141. package/dist/themes/data/gruvbox.d.ts +0 -3
  142. package/dist/themes/data/gruvbox.js +0 -158
  143. package/dist/themes/data/index.d.ts +0 -35
  144. package/dist/themes/data/index.js +0 -64
  145. package/dist/themes/data/monokai.d.ts +0 -3
  146. package/dist/themes/data/monokai.js +0 -158
  147. package/dist/themes/data/nord.d.ts +0 -3
  148. package/dist/themes/data/nord.js +0 -158
  149. package/dist/themes/data/rose-pine-dawn.d.ts +0 -3
  150. package/dist/themes/data/rose-pine-dawn.js +0 -158
  151. package/dist/themes/data/rose-pine-moon.d.ts +0 -3
  152. package/dist/themes/data/rose-pine-moon.js +0 -158
  153. package/dist/themes/data/rose-pine.d.ts +0 -3
  154. package/dist/themes/data/rose-pine.js +0 -158
  155. package/dist/themes/data/solarized-dark.d.ts +0 -3
  156. package/dist/themes/data/solarized-dark.js +0 -158
  157. package/dist/themes/data/solarized-light.d.ts +0 -3
  158. package/dist/themes/data/solarized-light.js +0 -158
  159. package/dist/themes/data/svg-export.d.ts +0 -3
  160. package/dist/themes/data/svg-export.js +0 -158
  161. package/dist/themes/data/textual-ansi.d.ts +0 -3
  162. package/dist/themes/data/textual-ansi.js +0 -158
  163. package/dist/themes/data/textual-dark.d.ts +0 -3
  164. package/dist/themes/data/textual-dark.js +0 -158
  165. package/dist/themes/data/textual-light.d.ts +0 -3
  166. package/dist/themes/data/textual-light.js +0 -158
  167. package/dist/themes/data/tokyo-night.d.ts +0 -3
  168. package/dist/themes/data/tokyo-night.js +0 -158
  169. package/dist/themes/data/types.d.ts +0 -15
  170. package/dist/themes/data/types.js +0 -1
  171. package/dist/themes/palette.d.ts +0 -21
  172. package/dist/themes/palette.js +0 -28
  173. package/dist/themes/ramp.d.ts +0 -83
  174. package/dist/themes/ramp.js +0 -122
  175. package/dist/themes/registry.d.ts +0 -43
  176. package/dist/themes/registry.js +0 -82
  177. package/dist/themes/terminalThemes.d.ts +0 -54
  178. package/dist/themes/terminalThemes.js +0 -68
  179. package/dist/themes/transpose.d.ts +0 -87
  180. package/dist/themes/transpose.js +0 -138
  181. package/dist/widgets/button.d.ts +0 -49
  182. package/dist/widgets/button.js +0 -206
  183. package/dist/widgets/checkbox.d.ts +0 -41
  184. package/dist/widgets/checkbox.js +0 -155
  185. package/dist/widgets/dropdown.d.ts +0 -67
  186. package/dist/widgets/dropdown.js +0 -453
  187. package/dist/widgets/event-router.d.ts +0 -78
  188. package/dist/widgets/event-router.js +0 -622
  189. package/dist/widgets/focus-manager.d.ts +0 -26
  190. package/dist/widgets/focus-manager.js +0 -193
  191. package/dist/widgets/index.d.ts +0 -22
  192. package/dist/widgets/index.js +0 -15
  193. package/dist/widgets/screen.d.ts +0 -98
  194. package/dist/widgets/screen.js +0 -530
  195. package/dist/widgets/slider.d.ts +0 -57
  196. package/dist/widgets/slider.js +0 -270
  197. package/dist/widgets/static-item.d.ts +0 -40
  198. package/dist/widgets/static-item.js +0 -60
  199. package/dist/widgets/text-input.d.ts +0 -274
  200. package/dist/widgets/text-input.js +0 -1162
  201. package/dist/widgets/toggle.d.ts +0 -44
  202. package/dist/widgets/toggle.js +0 -179
  203. package/dist/widgets/types.d.ts +0 -108
  204. package/dist/widgets/types.js +0 -31
  205. package/dist/widgets/widget-base.d.ts +0 -42
  206. package/dist/widgets/widget-base.js +0 -173
@@ -1,453 +0,0 @@
1
- /**
2
- * Dropdown widget — single-select from option list, with built-in filter.
3
- *
4
- * [LAW:one-source-of-truth] Inline footprint (always 1 row, the header)
5
- * is independent of rendered shape (1 row collapsed, 1 + M rows expanded
6
- * where M = filteredOptions.length, or 1 when no matches). render() emits
7
- * the header row only; renderOverlay() emits option rows or a
8
- * "(no matches)" placeholder.
9
- *
10
- * [LAW:dataflow-not-control-flow] `expanded` and `filter` are data;
11
- * layout never reserves space for the option list; the overlay pass
12
- * paints over whatever is below.
13
- *
14
- * [LAW:one-source-of-truth] `options` and `selectedIndex` are canonical.
15
- * `filter` is internal view state. `filteredOptions` is the derived
16
- * subsequence (case-insensitive substring match). `highlightedIndex`
17
- * indexes into `filteredOptions`. Commit maps the filtered position
18
- * back to canonical idx by reference, so selection survives any filter
19
- * mutation.
20
- *
21
- * Width invariant: measure() always returns maxLabelLen(options) + 4
22
- * regardless of filter state — the query is right-clipped, never wider
23
- * than the header. See docs/widgets.md → Dropdown.
24
- */
25
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
26
- var useValue = arguments.length > 2;
27
- for (var i = 0; i < initializers.length; i++) {
28
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
29
- }
30
- return useValue ? value : void 0;
31
- };
32
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
33
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
34
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
35
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
36
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
37
- var _, done = false;
38
- for (var i = decorators.length - 1; i >= 0; i--) {
39
- var context = {};
40
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
41
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
42
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
43
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
44
- if (kind === "accessor") {
45
- if (result === void 0) continue;
46
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
47
- if (_ = accept(result.get)) descriptor.get = _;
48
- if (_ = accept(result.set)) descriptor.set = _;
49
- if (_ = accept(result.init)) initializers.unshift(_);
50
- }
51
- else if (_ = accept(result)) {
52
- if (kind === "field") initializers.unshift(_);
53
- else descriptor[key] = _;
54
- }
55
- }
56
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
57
- done = true;
58
- };
59
- import { observable, action, observableRef, observableShallow } from "mobx";
60
- import { Segment } from "../core/segment.js";
61
- import { Style } from "../core/style.js";
62
- import { ColorSpec } from "../core/color.js";
63
- import { cellLen, setCellSize, splitText, asCellCol } from "../core/cells.js";
64
- import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
65
- import { WidgetBase } from "./widget-base.js";
66
- let Dropdown = (() => {
67
- let _classSuper = WidgetBase;
68
- let _instanceExtraInitializers = [];
69
- let _options_decorators;
70
- let _options_initializers = [];
71
- let _options_extraInitializers = [];
72
- let _selectedIndex_decorators;
73
- let _selectedIndex_initializers = [];
74
- let _selectedIndex_extraInitializers = [];
75
- let _expanded_decorators;
76
- let _expanded_initializers = [];
77
- let _expanded_extraInitializers = [];
78
- let _highlightedIndex_decorators;
79
- let _highlightedIndex_initializers = [];
80
- let _highlightedIndex_extraInitializers = [];
81
- let _filter_decorators;
82
- let _filter_initializers = [];
83
- let _filter_extraInitializers = [];
84
- let __theme_decorators;
85
- let __theme_initializers = [];
86
- let __theme_extraInitializers = [];
87
- let _setTheme_decorators;
88
- let _handleKey_decorators;
89
- let _handleMouse_decorators;
90
- let _handleFocus_decorators;
91
- return class Dropdown extends _classSuper {
92
- static {
93
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
94
- _options_decorators = [observableShallow];
95
- _selectedIndex_decorators = [observable];
96
- _expanded_decorators = [observable];
97
- _highlightedIndex_decorators = [observable];
98
- _filter_decorators = [observable];
99
- __theme_decorators = [observableRef];
100
- _setTheme_decorators = [action];
101
- _handleKey_decorators = [action];
102
- _handleMouse_decorators = [action];
103
- _handleFocus_decorators = [action];
104
- __esDecorate(this, null, _options_decorators, { kind: "accessor", name: "options", static: false, private: false, access: { has: obj => "options" in obj, get: obj => obj.options, set: (obj, value) => { obj.options = value; } }, metadata: _metadata }, _options_initializers, _options_extraInitializers);
105
- __esDecorate(this, null, _selectedIndex_decorators, { kind: "accessor", name: "selectedIndex", static: false, private: false, access: { has: obj => "selectedIndex" in obj, get: obj => obj.selectedIndex, set: (obj, value) => { obj.selectedIndex = value; } }, metadata: _metadata }, _selectedIndex_initializers, _selectedIndex_extraInitializers);
106
- __esDecorate(this, null, _expanded_decorators, { kind: "accessor", name: "expanded", static: false, private: false, access: { has: obj => "expanded" in obj, get: obj => obj.expanded, set: (obj, value) => { obj.expanded = value; } }, metadata: _metadata }, _expanded_initializers, _expanded_extraInitializers);
107
- __esDecorate(this, null, _highlightedIndex_decorators, { kind: "accessor", name: "highlightedIndex", static: false, private: false, access: { has: obj => "highlightedIndex" in obj, get: obj => obj.highlightedIndex, set: (obj, value) => { obj.highlightedIndex = value; } }, metadata: _metadata }, _highlightedIndex_initializers, _highlightedIndex_extraInitializers);
108
- __esDecorate(this, null, _filter_decorators, { kind: "accessor", name: "filter", static: false, private: false, access: { has: obj => "filter" in obj, get: obj => obj.filter, set: (obj, value) => { obj.filter = value; } }, metadata: _metadata }, _filter_initializers, _filter_extraInitializers);
109
- __esDecorate(this, null, __theme_decorators, { kind: "accessor", name: "_theme", static: false, private: false, access: { has: obj => "_theme" in obj, get: obj => obj._theme, set: (obj, value) => { obj._theme = value; } }, metadata: _metadata }, __theme_initializers, __theme_extraInitializers);
110
- __esDecorate(this, null, _setTheme_decorators, { kind: "method", name: "setTheme", static: false, private: false, access: { has: obj => "setTheme" in obj, get: obj => obj.setTheme }, metadata: _metadata }, null, _instanceExtraInitializers);
111
- __esDecorate(this, null, _handleKey_decorators, { kind: "method", name: "handleKey", static: false, private: false, access: { has: obj => "handleKey" in obj, get: obj => obj.handleKey }, metadata: _metadata }, null, _instanceExtraInitializers);
112
- __esDecorate(this, null, _handleMouse_decorators, { kind: "method", name: "handleMouse", static: false, private: false, access: { has: obj => "handleMouse" in obj, get: obj => obj.handleMouse }, metadata: _metadata }, null, _instanceExtraInitializers);
113
- __esDecorate(this, null, _handleFocus_decorators, { kind: "method", name: "handleFocus", static: false, private: false, access: { has: obj => "handleFocus" in obj, get: obj => obj.handleFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
114
- if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
115
- }
116
- id = __runInitializers(this, _instanceExtraInitializers);
117
- focusable = true;
118
- #options_accessor_storage = __runInitializers(this, _options_initializers, void 0);
119
- get options() { return this.#options_accessor_storage; }
120
- set options(value) { this.#options_accessor_storage = value; }
121
- #selectedIndex_accessor_storage = (__runInitializers(this, _options_extraInitializers), __runInitializers(this, _selectedIndex_initializers, void 0));
122
- get selectedIndex() { return this.#selectedIndex_accessor_storage; }
123
- set selectedIndex(value) { this.#selectedIndex_accessor_storage = value; }
124
- #expanded_accessor_storage = (__runInitializers(this, _selectedIndex_extraInitializers), __runInitializers(this, _expanded_initializers, false));
125
- get expanded() { return this.#expanded_accessor_storage; }
126
- set expanded(value) { this.#expanded_accessor_storage = value; }
127
- #highlightedIndex_accessor_storage = (__runInitializers(this, _expanded_extraInitializers), __runInitializers(this, _highlightedIndex_initializers, void 0));
128
- get highlightedIndex() { return this.#highlightedIndex_accessor_storage; }
129
- set highlightedIndex(value) { this.#highlightedIndex_accessor_storage = value; }
130
- #filter_accessor_storage = (__runInitializers(this, _highlightedIndex_extraInitializers), __runInitializers(this, _filter_initializers, ""));
131
- get filter() { return this.#filter_accessor_storage; }
132
- set filter(value) { this.#filter_accessor_storage = value; }
133
- // [LAW:dataflow-not-control-flow] Derived view; empty filter falls out
134
- // naturally because "".includes("") is true for every label — no
135
- // special-case branch.
136
- get filteredOptions() {
137
- const f = this.filter.toLowerCase();
138
- const result = [];
139
- for (let i = 0; i < this.options.length; i++) {
140
- const label = this.options[i];
141
- if (label.toLowerCase().includes(f))
142
- result.push({ label, idx: i });
143
- }
144
- return result;
145
- }
146
- #_theme_accessor_storage = (__runInitializers(this, _filter_extraInitializers), __runInitializers(this, __theme_initializers, void 0));
147
- // [LAW:types-are-the-program] @observableRef so setTheme() triggers a
148
- // re-render — render() and resolvePalette() read _theme.palette, so the
149
- // theme reference must participate in MobX reactivity.
150
- get _theme() { return this.#_theme_accessor_storage; }
151
- set _theme(value) { this.#_theme_accessor_storage = value; }
152
- constructor(options) {
153
- super();
154
- __runInitializers(this, __theme_extraInitializers);
155
- this.id = options.id ?? `dropdown-${Math.random().toString(36).slice(2, 8)}`;
156
- this.options = [...options.options];
157
- // [LAW:types-are-the-program] selectedIndex must point to a valid
158
- // option (or be 0 for the empty-options case). Clamp at the trust
159
- // boundary so headerText / render / commit logic can all assume
160
- // validity. Empty options is allowed — render falls back to an
161
- // empty label and commit is a no-op until options are populated.
162
- this.selectedIndex = this.clampIndex(options.selectedIndex ?? 0);
163
- this.highlightedIndex = this.selectedIndex;
164
- this.disabled = options.disabled ?? false;
165
- this._theme = options.theme ?? DEFAULT_TERMINAL_THEME;
166
- }
167
- // [LAW:single-enforcer] One place owns "is this index valid for the
168
- // current options". selectedIndex and options are public observables —
169
- // external code can move them out of range. Anywhere internal code
170
- // seeds highlightedIndex from selectedIndex (constructor + every
171
- // expand path), route through this clamp so the seeded value is
172
- // always a legal option index (or 0 for empty options).
173
- clampIndex(n) {
174
- if (this.options.length === 0)
175
- return 0;
176
- return Math.max(0, Math.min(n, this.options.length - 1));
177
- }
178
- setTheme(theme) { this._theme = theme; }
179
- // --- Event handlers ---
180
- handleKey(event) {
181
- if (this.disabled)
182
- return;
183
- const isPrintable = event.character.length === 1 &&
184
- !event.ctrl &&
185
- !event.meta &&
186
- event.character >= " " &&
187
- event.character !== "\x7f";
188
- if (!this.expanded) {
189
- // enter/space remain the explicit "open with selection seeded" gesture.
190
- if (event.key === "enter" || event.key === "space") {
191
- this.expanded = true;
192
- this.highlightedIndex = this.clampIndex(this.selectedIndex);
193
- event.stop();
194
- return;
195
- }
196
- // Any other printable char auto-expands and starts filtering.
197
- if (isPrintable) {
198
- this.expanded = true;
199
- this.filter = this.filter + event.character;
200
- this.highlightedIndex = 0;
201
- event.stop();
202
- }
203
- return;
204
- }
205
- // expanded
206
- switch (event.key) {
207
- case "up":
208
- this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
209
- event.stop();
210
- return;
211
- case "down": {
212
- const max = Math.max(0, this.filteredOptions.length - 1);
213
- this.highlightedIndex = Math.min(max, this.highlightedIndex + 1);
214
- event.stop();
215
- return;
216
- }
217
- case "enter": {
218
- // [LAW:one-source-of-truth] Map filtered position → canonical idx.
219
- // No-op when filteredOptions is empty (highlighted is undefined).
220
- const picked = this.filteredOptions[this.highlightedIndex];
221
- event.stop();
222
- if (picked === undefined)
223
- return;
224
- this.selectedIndex = picked.idx;
225
- this.filter = "";
226
- this.expanded = false;
227
- this.emitChange();
228
- this.emitSubmit();
229
- return;
230
- }
231
- case "escape":
232
- // Single-step: clear filter + collapse.
233
- this.filter = "";
234
- this.expanded = false;
235
- event.stop();
236
- return;
237
- case "backspace":
238
- // [LAW:dataflow-not-control-flow] slice(0,-1) on "" is "" — no guard.
239
- this.filter = this.filter.slice(0, -1);
240
- this.highlightedIndex = 0;
241
- event.stop();
242
- return;
243
- case "tab":
244
- // First Tab while expanded cancels (clear filter + collapse) and
245
- // KEEPS focus on the dropdown. event.stop() halts the chain so
246
- // FocusManager's normal-priority Tab handler never runs — focus
247
- // traversal only happens on the *next* Tab press, when this widget
248
- // is collapsed and no longer stops the event. Shift direction is
249
- // ignored: both Tab and Shift+Tab cancel.
250
- this.filter = "";
251
- this.expanded = false;
252
- event.stop();
253
- return;
254
- }
255
- if (isPrintable) {
256
- this.filter = this.filter + event.character;
257
- this.highlightedIndex = 0;
258
- event.stop();
259
- }
260
- }
261
- handleMouse(event) {
262
- if (this.disabled)
263
- return;
264
- if (event.type !== "mouse_up")
265
- return;
266
- const b = this.bounds;
267
- if (!b)
268
- return;
269
- const inside = event.x >= b.x &&
270
- event.x < b.x + b.width &&
271
- event.y >= b.y &&
272
- event.y < b.y + b.height;
273
- if (!this.expanded) {
274
- if (inside) {
275
- this.expanded = true;
276
- this.highlightedIndex = this.clampIndex(this.selectedIndex);
277
- }
278
- return;
279
- }
280
- // expanded: row 0 is the header; rows 1..M are filtered options.
281
- if (!inside) {
282
- // Click outside cancels — clear filter + collapse.
283
- this.filter = "";
284
- this.expanded = false;
285
- return;
286
- }
287
- const rowOffset = event.y - b.y;
288
- if (rowOffset === 0) {
289
- // Click the header → collapse + clear filter, no selection change.
290
- this.filter = "";
291
- this.expanded = false;
292
- return;
293
- }
294
- // [LAW:one-source-of-truth] Filtered position → canonical idx.
295
- const picked = this.filteredOptions[rowOffset - 1];
296
- if (picked === undefined)
297
- return;
298
- this.selectedIndex = picked.idx;
299
- this.filter = "";
300
- this.expanded = false;
301
- this.emitChange();
302
- this.emitSubmit();
303
- }
304
- // [LAW:single-enforcer] FocusManager is the single dispatcher of focus
305
- // transitions; overriding handleFocus here catches every blur path (Tab
306
- // cycling, programmatic focus, unregister) so the overlay cannot outlive
307
- // the widget's focus. Collapse is the data; no separate "should I close"
308
- // branch lives at the router or elsewhere.
309
- handleFocus(event) {
310
- super.handleFocus(event);
311
- if (event.type === "blur") {
312
- this.filter = "";
313
- this.expanded = false;
314
- }
315
- }
316
- // --- Rendering ---
317
- render(options) {
318
- // Header only — the inline footprint that flow layout sees. Always
319
- // 1 row regardless of `expanded`/`filter`. Option rows live in
320
- // renderOverlay. Width invariant: maxLabelLen + 4.
321
- const arrowChar = options.asciiOnly ? "v" : "▾";
322
- // [LAW:one-source-of-truth] render() owns the asciiOnly switch; the
323
- // caret character flows into headerText as data rather than headerText
324
- // re-reading the option flag. Same arrow / caret resolution path.
325
- const caret = this.focused ? (options.asciiOnly ? "|" : "│") : "";
326
- const maxLabelLen = this.maxLabelLen();
327
- const baseStyle = this.disabled
328
- ? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
329
- : new Style({
330
- color: this.resolvePalette("foreground"),
331
- bgcolor: this.resolvePalette("surface"),
332
- underline: this.focused,
333
- });
334
- const headerLabel = this.headerText(maxLabelLen, caret);
335
- return [
336
- new Segment("[", baseStyle),
337
- new Segment(`${headerLabel} ${arrowChar}`, baseStyle),
338
- new Segment("]", baseStyle),
339
- ];
340
- }
341
- // [LAW:dataflow-not-control-flow] One header text function; the data
342
- // (filter empty vs not) chooses between selected-label and query+cursor.
343
- // Both branches yield exactly maxLabelLen cells — width invariant.
344
- // Selected label is centered; filter input is left-aligned so the caret
345
- // doesn't jitter horizontally while typing.
346
- //
347
- // [LAW:one-source-of-truth] All width math is in terminal cells via
348
- // cellLen / setCellSize / splitText — plain `.length` would miscount
349
- // wide / emoji characters and break the width invariant.
350
- headerText(maxLabelLen, caret) {
351
- if (this.filter === "") {
352
- const raw = this.options[this.selectedIndex] ?? "";
353
- const w = cellLen(raw);
354
- if (w >= maxLabelLen)
355
- return setCellSize(raw, asCellCol(maxLabelLen));
356
- const pad = maxLabelLen - w;
357
- const left = Math.floor(pad / 2);
358
- const right = pad - left;
359
- return " ".repeat(left) + raw + " ".repeat(right);
360
- }
361
- // Leading space gives the filter input a small gutter from the [ chrome,
362
- // matching the visual breathing room of the centered label.
363
- const filterRoom = Math.max(0, maxLabelLen - cellLen(caret) - 1);
364
- const [filterText] = splitText(this.filter, asCellCol(filterRoom));
365
- return setCellSize(" " + filterText + caret, asCellCol(maxLabelLen));
366
- }
367
- renderOverlay(_options) {
368
- if (!this.expanded)
369
- return null;
370
- const maxLabelLen = this.maxLabelLen();
371
- const fopts = this.filteredOptions;
372
- if (fopts.length === 0)
373
- return this.renderNoMatchRow(maxLabelLen);
374
- const segments = [];
375
- for (let i = 0; i < fopts.length; i++) {
376
- if (i > 0)
377
- segments.push(new Segment("\n"));
378
- segments.push(...this.renderOptionRow(fopts[i], i, maxLabelLen));
379
- }
380
- return segments;
381
- }
382
- renderNoMatchRow(maxLabelLen) {
383
- // Right-clipped + padded so width matches a normal option row.
384
- const text = setCellSize("(no matches)", asCellCol(maxLabelLen));
385
- const inner = ` ${text} `;
386
- const style = this.disabled
387
- ? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
388
- : new Style({
389
- color: this.resolvePalette("foreground"),
390
- bgcolor: this.resolvePalette("surface"),
391
- dim: true,
392
- });
393
- return [
394
- new Segment(" ", style),
395
- new Segment(inner, style),
396
- new Segment(" ", style),
397
- ];
398
- }
399
- renderOptionRow(entry, fpos, maxLabelLen) {
400
- // Row: " " + label.padEnd(maxLabelLen) + " "
401
- // Width = 1 + 1 + maxLabelLen + 1 + 1 = maxLabelLen + 4.
402
- // Overlay rows are unbracketed — selection/highlight are conveyed by bg
403
- // color; the header keeps [ ] as its focus-indicator chrome.
404
- const label = setCellSize(entry.label, asCellCol(maxLabelLen));
405
- const inner = ` ${label} `;
406
- const isSelected = entry.idx === this.selectedIndex;
407
- const isHighlighted = fpos === this.highlightedIndex;
408
- // Highlighted: muted bg → text-primary (mostly-accent fg) is readable.
409
- // Selected: full primary bg → on-primary (WCAG contrast) for fg.
410
- const rowStyle = this.disabled
411
- ? new Style({ color: "#666666", bgcolor: "#333333", dim: true })
412
- : isHighlighted
413
- ? new Style({
414
- color: this.resolvePalette("text-primary"),
415
- bgcolor: this.resolvePalette("primary-muted"),
416
- })
417
- : isSelected
418
- ? new Style({
419
- color: this.resolvePalette("on-primary"),
420
- bgcolor: this.resolvePalette("primary"),
421
- })
422
- : new Style({
423
- color: this.resolvePalette("foreground"),
424
- bgcolor: this.resolvePalette("surface"),
425
- });
426
- return [
427
- new Segment(" ", rowStyle),
428
- new Segment(inner, rowStyle),
429
- new Segment(" ", rowStyle),
430
- ];
431
- }
432
- maxLabelLen() {
433
- let m = 0;
434
- for (const label of this.options) {
435
- const w = cellLen(label);
436
- if (w > m)
437
- m = w;
438
- }
439
- return m;
440
- }
441
- measure(_options) {
442
- const width = this.maxLabelLen() + 4;
443
- return { minimum: width, maximum: width };
444
- }
445
- // --- Palette resolution ---
446
- resolvePalette(key) {
447
- const rgba = this._theme.palette.get(key);
448
- // [LAW:no-defensive-null-guards] palette must contain all keys.
449
- return ColorSpec.fromRgba(rgba);
450
- }
451
- };
452
- })();
453
- export { Dropdown };
@@ -1,78 +0,0 @@
1
- /**
2
- * EventRouter — parses raw stdin into KeyEvent / WidgetMouseEvent values
3
- * and dispatches them to widgets via the Screen's FocusManager and bounds.
4
- *
5
- * [LAW:single-enforcer] ANSI escape parsing lives only here — widgets never
6
- * see raw bytes. One parser, one dispatch surface.
7
- *
8
- * [LAW:dataflow-not-control-flow] The same parse pipeline runs for every
9
- * input chunk: append → consume one event at a time until the buffer can't
10
- * yield more. The byte's value, not a side-mode, decides which event is
11
- * emitted. There is no "are we mid-sequence" mode flag — the buffer head
12
- * carries all the state we need.
13
- *
14
- * Lone-ESC handling: a bare `\x1b` is ambiguous (escape key vs. start of a
15
- * CSI sequence). When the buffer is drained but ends with a lone ESC, the
16
- * router defers emission via setTimeout(0). The next chunk cancels the timer
17
- * if it extends the sequence. Tests can call `flush()` to drain synchronously.
18
- */
19
- import { KeyEvent } from "./types.js";
20
- import type { Screen, FocusManager, InteractiveWidget, WidgetMouseEvent, KeyHandlerOptions } from "./types.js";
21
- import type { Unsubscribe } from "../core/subscription.js";
22
- import type { TerminalHost } from "../host/terminal-host.js";
23
- type KeyHandler = (event: KeyEvent) => void;
24
- type WidgetSource = {
25
- focusManager: FocusManager;
26
- getWidgets: () => readonly InteractiveWidget[];
27
- };
28
- export interface EventRouterOptions {
29
- screen: Screen | WidgetSource;
30
- /**
31
- * The I/O capability the router reads input from and writes
32
- * mouse-tracking sequences to. Construct a `NodeTerminalHost` for
33
- * production node demos; tests pass a host that wraps mock streams.
34
- */
35
- host: TerminalHost;
36
- /**
37
- * When true (default when the host reports `isTTY`), enable mouse
38
- * tracking on `start()` and disable on `stop()`. Disable in tests /
39
- * non-TTY environments.
40
- */
41
- manageMouse?: boolean;
42
- /**
43
- * When true (default when the host reports `isTTY`), switch the host
44
- * into raw mode on `start()` and restore on `stop()`.
45
- */
46
- manageRawMode?: boolean;
47
- }
48
- export declare class EventRouter {
49
- private readonly source;
50
- private readonly host;
51
- private readonly manageMouse;
52
- private readonly manageRawMode;
53
- private buffer;
54
- private running;
55
- private dataUnsubscribe;
56
- private escTimer;
57
- private capturedWidget;
58
- private readonly keyChain;
59
- private readonly mouseHandlers;
60
- constructor(options: EventRouterOptions);
61
- start(): void;
62
- stop(): void;
63
- onKey(handler: KeyHandler, options?: KeyHandlerOptions): Unsubscribe;
64
- onMouse(handler: (event: WidgetMouseEvent) => void): Unsubscribe;
65
- /** Feed a chunk of bytes (or a string of bytes) into the parser. */
66
- feed(chunk: Uint8Array | string): void;
67
- /** Force any pending lone ESC out of the buffer. */
68
- flush(): void;
69
- private drain;
70
- private consumeOne;
71
- private consumePrintable;
72
- private consumeEscape;
73
- private consumeSS3;
74
- private consumeCSI;
75
- private dispatchKey;
76
- private dispatchMouse;
77
- }
78
- export {};