@promptctl/rich-js 0.18.0 β†’ 0.19.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 (244) hide show
  1. package/README.md +9 -7
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Checkbox widget β€” boolean toggle with label.
3
+ * [LAW:dataflow-not-control-flow] one Segment of fixed width every render;
4
+ * the indicator character and style come from observable state.
5
+ * [LAW:one-type-per-behavior] shared widget infrastructure lives on WidgetBase.
6
+ *
7
+ * Visual states:
8
+ * unchecked β€” "[ ] label"
9
+ * checked β€” "[βœ“] label" (ASCII fallback "[x] label" when options.asciiOnly)
10
+ * focused β€” underline on the rendered segment (no width change)
11
+ * disabled β€” dimmed
12
+ */
13
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
14
+ var useValue = arguments.length > 2;
15
+ for (var i = 0; i < initializers.length; i++) {
16
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
17
+ }
18
+ return useValue ? value : void 0;
19
+ };
20
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
21
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
22
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
23
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
24
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
25
+ var _, done = false;
26
+ for (var i = decorators.length - 1; i >= 0; i--) {
27
+ var context = {};
28
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
29
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
30
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
31
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
32
+ if (kind === "accessor") {
33
+ if (result === void 0) continue;
34
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
35
+ if (_ = accept(result.get)) descriptor.get = _;
36
+ if (_ = accept(result.set)) descriptor.set = _;
37
+ if (_ = accept(result.init)) initializers.unshift(_);
38
+ }
39
+ else if (_ = accept(result)) {
40
+ if (kind === "field") initializers.unshift(_);
41
+ else descriptor[key] = _;
42
+ }
43
+ }
44
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
45
+ done = true;
46
+ };
47
+ import { observable, action } from "mobx";
48
+ import { Segment } from "../core/segment.js";
49
+ import { Style } from "../core/style.js";
50
+ import { cellLen } from "../core/cells.js";
51
+ import { drawable } from "../core/protocol.js";
52
+ import { ThemedWidget } from "./themed-widget.js";
53
+ import { ink } from "./ink.js";
54
+ let Checkbox = (() => {
55
+ let _classSuper = ThemedWidget;
56
+ let _instanceExtraInitializers = [];
57
+ let _label_decorators;
58
+ let _label_initializers = [];
59
+ let _label_extraInitializers = [];
60
+ let _checked_decorators;
61
+ let _checked_initializers = [];
62
+ let _checked_extraInitializers = [];
63
+ let _handleKey_decorators;
64
+ let _handleMouse_decorators;
65
+ return class Checkbox extends _classSuper {
66
+ static {
67
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
68
+ _label_decorators = [observable];
69
+ _checked_decorators = [observable];
70
+ _handleKey_decorators = [action];
71
+ _handleMouse_decorators = [action];
72
+ __esDecorate(this, null, _label_decorators, { kind: "accessor", name: "label", static: false, private: false, access: { has: obj => "label" in obj, get: obj => obj.label, set: (obj, value) => { obj.label = value; } }, metadata: _metadata }, _label_initializers, _label_extraInitializers);
73
+ __esDecorate(this, null, _checked_decorators, { kind: "accessor", name: "checked", static: false, private: false, access: { has: obj => "checked" in obj, get: obj => obj.checked, set: (obj, value) => { obj.checked = value; } }, metadata: _metadata }, _checked_initializers, _checked_extraInitializers);
74
+ __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);
75
+ __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);
76
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
77
+ }
78
+ id = __runInitializers(this, _instanceExtraInitializers);
79
+ focusable = true;
80
+ #label_accessor_storage = __runInitializers(this, _label_initializers, void 0);
81
+ get label() { return this.#label_accessor_storage; }
82
+ set label(value) { this.#label_accessor_storage = value; }
83
+ #checked_accessor_storage = (__runInitializers(this, _label_extraInitializers), __runInitializers(this, _checked_initializers, void 0));
84
+ get checked() { return this.#checked_accessor_storage; }
85
+ set checked(value) { this.#checked_accessor_storage = value; }
86
+ constructor(options) {
87
+ super(options.theme);
88
+ __runInitializers(this, _checked_extraInitializers);
89
+ this.id = options.id ?? `checkbox-${options.label.toLowerCase().replace(/\s+/g, "-")}`;
90
+ this.label = options.label;
91
+ this.checked = options.checked ?? false;
92
+ this.disabled = options.disabled ?? false;
93
+ }
94
+ // --- Event handlers ---
95
+ handleKey(event) {
96
+ if (this.disabled)
97
+ return;
98
+ if (event.key === "space") {
99
+ this.checked = !this.checked;
100
+ this.emitChange();
101
+ event.stop();
102
+ return;
103
+ }
104
+ if (event.key === "enter") {
105
+ this.emitSubmit();
106
+ event.stop();
107
+ }
108
+ }
109
+ handleMouse(event) {
110
+ if (this.disabled)
111
+ return;
112
+ // A release that dragged off the checkbox cancels the press.
113
+ if (event.type === "mouse_up" && event.over) {
114
+ this.checked = !this.checked;
115
+ this.emitChange();
116
+ }
117
+ }
118
+ // --- Rendering ---
119
+ draw(options) {
120
+ const indicator = this.checked ? drawable(options, "βœ“", "x") : " ";
121
+ const text = `[${indicator}] ${this.label}`;
122
+ if (this.disabled) {
123
+ return [new Segment(text, new Style(this.disabledInk))];
124
+ }
125
+ const colors = ink(this.theme, this.checked ? "text-primary" : "foreground", "background");
126
+ return [new Segment(text, new Style({ ...colors, underline: this.focused }))];
127
+ }
128
+ measure(_options) {
129
+ // [LAW:one-source-of-truth] cellLen β€” see button.ts.
130
+ const width = cellLen(this.label) + 4;
131
+ return { minimum: width, maximum: width };
132
+ }
133
+ };
134
+ })();
135
+ export { Checkbox };
@@ -0,0 +1,64 @@
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
+ import { Segment } from "../core/segment.js";
26
+ import { type RenderOptions } from "../core/protocol.js";
27
+ import type { TerminalTheme } from "../core/color.js";
28
+ import { ThemedWidget } from "./themed-widget.js";
29
+ import type { KeyEvent, WidgetMouseEvent, WidgetFocusEvent, OverlayRenderable } from "./types.js";
30
+ export interface DropdownOptions {
31
+ options: string[];
32
+ selectedIndex?: number;
33
+ id?: string;
34
+ disabled?: boolean;
35
+ theme?: TerminalTheme;
36
+ }
37
+ export declare class Dropdown extends ThemedWidget implements OverlayRenderable {
38
+ readonly id: string;
39
+ readonly focusable = true;
40
+ accessor options: string[];
41
+ accessor selectedIndex: number;
42
+ accessor expanded: boolean;
43
+ accessor highlightedIndex: number;
44
+ accessor filter: string;
45
+ get filteredOptions(): {
46
+ label: string;
47
+ idx: number;
48
+ }[];
49
+ constructor(options: DropdownOptions);
50
+ private clampIndex;
51
+ handleKey(event: KeyEvent): void;
52
+ handleMouse(event: WidgetMouseEvent): void;
53
+ handleFocus(event: WidgetFocusEvent): void;
54
+ protected draw(options: RenderOptions): Iterable<Segment>;
55
+ private headerText;
56
+ renderOverlay(_options: RenderOptions): Iterable<Segment> | null;
57
+ private renderNoMatchRow;
58
+ private renderOptionRow;
59
+ private maxLabelLen;
60
+ measure(_options: RenderOptions): {
61
+ minimum: number;
62
+ maximum: number;
63
+ };
64
+ }
@@ -0,0 +1,416 @@
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, observableShallow } from "mobx";
60
+ import { Segment } from "../core/segment.js";
61
+ import { Style } from "../core/style.js";
62
+ import { cellLen, setCellSize, splitText, asCellCol } from "../core/cells.js";
63
+ import { drawable } from "../core/protocol.js";
64
+ import { ThemedWidget } from "./themed-widget.js";
65
+ import { ink } from "./ink.js";
66
+ let Dropdown = (() => {
67
+ let _classSuper = ThemedWidget;
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 _handleKey_decorators;
85
+ let _handleMouse_decorators;
86
+ let _handleFocus_decorators;
87
+ return class Dropdown extends _classSuper {
88
+ static {
89
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
90
+ _options_decorators = [observableShallow];
91
+ _selectedIndex_decorators = [observable];
92
+ _expanded_decorators = [observable];
93
+ _highlightedIndex_decorators = [observable];
94
+ _filter_decorators = [observable];
95
+ _handleKey_decorators = [action];
96
+ _handleMouse_decorators = [action];
97
+ _handleFocus_decorators = [action];
98
+ __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);
99
+ __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);
100
+ __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);
101
+ __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);
102
+ __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);
103
+ __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);
104
+ __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);
105
+ __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);
106
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
107
+ }
108
+ id = __runInitializers(this, _instanceExtraInitializers);
109
+ focusable = true;
110
+ #options_accessor_storage = __runInitializers(this, _options_initializers, void 0);
111
+ get options() { return this.#options_accessor_storage; }
112
+ set options(value) { this.#options_accessor_storage = value; }
113
+ #selectedIndex_accessor_storage = (__runInitializers(this, _options_extraInitializers), __runInitializers(this, _selectedIndex_initializers, void 0));
114
+ get selectedIndex() { return this.#selectedIndex_accessor_storage; }
115
+ set selectedIndex(value) { this.#selectedIndex_accessor_storage = value; }
116
+ #expanded_accessor_storage = (__runInitializers(this, _selectedIndex_extraInitializers), __runInitializers(this, _expanded_initializers, false));
117
+ get expanded() { return this.#expanded_accessor_storage; }
118
+ set expanded(value) { this.#expanded_accessor_storage = value; }
119
+ #highlightedIndex_accessor_storage = (__runInitializers(this, _expanded_extraInitializers), __runInitializers(this, _highlightedIndex_initializers, void 0));
120
+ get highlightedIndex() { return this.#highlightedIndex_accessor_storage; }
121
+ set highlightedIndex(value) { this.#highlightedIndex_accessor_storage = value; }
122
+ #filter_accessor_storage = (__runInitializers(this, _highlightedIndex_extraInitializers), __runInitializers(this, _filter_initializers, ""));
123
+ get filter() { return this.#filter_accessor_storage; }
124
+ set filter(value) { this.#filter_accessor_storage = value; }
125
+ // [LAW:dataflow-not-control-flow] Derived view; empty filter falls out
126
+ // naturally because "".includes("") is true for every label β€” no
127
+ // special-case branch.
128
+ get filteredOptions() {
129
+ const f = this.filter.toLowerCase();
130
+ const result = [];
131
+ for (let i = 0; i < this.options.length; i++) {
132
+ const label = this.options[i];
133
+ if (label.toLowerCase().includes(f))
134
+ result.push({ label, idx: i });
135
+ }
136
+ return result;
137
+ }
138
+ constructor(options) {
139
+ super(options.theme);
140
+ __runInitializers(this, _filter_extraInitializers);
141
+ this.id = options.id ?? `dropdown-${Math.random().toString(36).slice(2, 8)}`;
142
+ this.options = [...options.options];
143
+ // [LAW:types-are-the-program] selectedIndex must point to a valid
144
+ // option (or be 0 for the empty-options case). Clamp at the trust
145
+ // boundary so headerText / render / commit logic can all assume
146
+ // validity. Empty options is allowed β€” render falls back to an
147
+ // empty label and commit is a no-op until options are populated.
148
+ this.selectedIndex = this.clampIndex(options.selectedIndex ?? 0);
149
+ this.highlightedIndex = this.selectedIndex;
150
+ this.disabled = options.disabled ?? false;
151
+ }
152
+ // [LAW:single-enforcer] One place owns "is this index valid for the
153
+ // current options". selectedIndex and options are public observables β€”
154
+ // external code can move them out of range. Anywhere internal code
155
+ // seeds highlightedIndex from selectedIndex (constructor + every
156
+ // expand path), route through this clamp so the seeded value is
157
+ // always a legal option index (or 0 for empty options).
158
+ clampIndex(n) {
159
+ if (this.options.length === 0)
160
+ return 0;
161
+ return Math.max(0, Math.min(n, this.options.length - 1));
162
+ }
163
+ // --- Event handlers ---
164
+ handleKey(event) {
165
+ if (this.disabled)
166
+ return;
167
+ const isPrintable = event.character.length === 1 &&
168
+ !event.ctrl &&
169
+ !event.meta &&
170
+ event.character >= " " &&
171
+ event.character !== "\x7f";
172
+ if (!this.expanded) {
173
+ // enter/space remain the explicit "open with selection seeded" gesture.
174
+ if (event.key === "enter" || event.key === "space") {
175
+ this.expanded = true;
176
+ this.highlightedIndex = this.clampIndex(this.selectedIndex);
177
+ event.stop();
178
+ return;
179
+ }
180
+ // Any other printable char auto-expands and starts filtering.
181
+ if (isPrintable) {
182
+ this.expanded = true;
183
+ this.filter = this.filter + event.character;
184
+ this.highlightedIndex = 0;
185
+ event.stop();
186
+ }
187
+ return;
188
+ }
189
+ // expanded
190
+ switch (event.key) {
191
+ case "up":
192
+ this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
193
+ event.stop();
194
+ return;
195
+ case "down": {
196
+ const max = Math.max(0, this.filteredOptions.length - 1);
197
+ this.highlightedIndex = Math.min(max, this.highlightedIndex + 1);
198
+ event.stop();
199
+ return;
200
+ }
201
+ case "enter": {
202
+ // [LAW:one-source-of-truth] Map filtered position β†’ canonical idx.
203
+ // No-op when filteredOptions is empty (highlighted is undefined).
204
+ const picked = this.filteredOptions[this.highlightedIndex];
205
+ event.stop();
206
+ if (picked === undefined)
207
+ return;
208
+ this.selectedIndex = picked.idx;
209
+ this.filter = "";
210
+ this.expanded = false;
211
+ this.emitChange();
212
+ this.emitSubmit();
213
+ return;
214
+ }
215
+ case "escape":
216
+ // Single-step: clear filter + collapse.
217
+ this.filter = "";
218
+ this.expanded = false;
219
+ event.stop();
220
+ return;
221
+ case "backspace":
222
+ // [LAW:dataflow-not-control-flow] slice(0,-1) on "" is "" β€” no guard.
223
+ this.filter = this.filter.slice(0, -1);
224
+ this.highlightedIndex = 0;
225
+ event.stop();
226
+ return;
227
+ case "tab":
228
+ // First Tab while expanded cancels (clear filter + collapse) and
229
+ // KEEPS focus on the dropdown. event.stop() halts the chain so
230
+ // FocusManager's normal-priority Tab handler never runs β€” focus
231
+ // traversal only happens on the *next* Tab press, when this widget
232
+ // is collapsed and no longer stops the event. Shift direction is
233
+ // ignored: both Tab and Shift+Tab cancel.
234
+ this.filter = "";
235
+ this.expanded = false;
236
+ event.stop();
237
+ return;
238
+ }
239
+ if (isPrintable) {
240
+ this.filter = this.filter + event.character;
241
+ this.highlightedIndex = 0;
242
+ event.stop();
243
+ }
244
+ }
245
+ handleMouse(event) {
246
+ if (this.disabled)
247
+ return;
248
+ if (event.type !== "mouse_up")
249
+ return;
250
+ // [LAW:one-source-of-truth] Whether the release is on this dropdown is
251
+ // read off the frame (`over`), not its own idea of its size; a press on
252
+ // it that dragged away releases elsewhere. Row 0 is the header, and the
253
+ // overlay rows continue below it.
254
+ const inside = event.over;
255
+ if (!this.expanded) {
256
+ if (inside) {
257
+ this.expanded = true;
258
+ this.highlightedIndex = this.clampIndex(this.selectedIndex);
259
+ }
260
+ return;
261
+ }
262
+ // expanded: row 0 is the header; rows 1..M are filtered options.
263
+ if (!inside) {
264
+ // Click outside cancels β€” clear filter + collapse.
265
+ this.filter = "";
266
+ this.expanded = false;
267
+ return;
268
+ }
269
+ if (event.y === 0) {
270
+ // Click the header β†’ collapse + clear filter, no selection change.
271
+ this.filter = "";
272
+ this.expanded = false;
273
+ return;
274
+ }
275
+ // [LAW:one-source-of-truth] Filtered position β†’ canonical idx.
276
+ const picked = this.filteredOptions[event.y - 1];
277
+ if (picked === undefined)
278
+ return;
279
+ this.selectedIndex = picked.idx;
280
+ this.filter = "";
281
+ this.expanded = false;
282
+ this.emitChange();
283
+ this.emitSubmit();
284
+ }
285
+ // [LAW:single-enforcer] FocusManager is the single dispatcher of focus
286
+ // transitions; overriding handleFocus here catches every blur path (Tab
287
+ // cycling, programmatic focus, unregister) so the overlay cannot outlive
288
+ // the widget's focus. Collapse is the data; no separate "should I close"
289
+ // branch lives at the router or elsewhere.
290
+ handleFocus(event) {
291
+ super.handleFocus(event);
292
+ if (event.type === "blur") {
293
+ this.filter = "";
294
+ this.expanded = false;
295
+ }
296
+ }
297
+ // --- Rendering ---
298
+ draw(options) {
299
+ // Header only β€” the inline footprint that flow layout sees. Always
300
+ // 1 row regardless of `expanded`/`filter`. Option rows live in
301
+ // renderOverlay. Width invariant: maxLabelLen + 4.
302
+ const arrowChar = drawable(options, "β–Ύ", "v");
303
+ // [LAW:one-source-of-truth] Both glyphs are resolved here, once, and flow
304
+ // into headerText as data rather than headerText re-reading the options.
305
+ const caret = this.focused ? drawable(options, "β”‚", "|") : "";
306
+ const maxLabelLen = this.maxLabelLen();
307
+ const baseStyle = this.disabled
308
+ ? new Style(this.disabledInk)
309
+ : new Style({
310
+ ...ink(this.theme, "foreground", "surface"),
311
+ underline: this.focused,
312
+ });
313
+ const headerLabel = this.headerText(maxLabelLen, caret);
314
+ return [
315
+ new Segment("[", baseStyle),
316
+ new Segment(`${headerLabel} ${arrowChar}`, baseStyle),
317
+ new Segment("]", baseStyle),
318
+ ];
319
+ }
320
+ // [LAW:dataflow-not-control-flow] One header text function; the data
321
+ // (filter empty vs not) chooses between selected-label and query+cursor.
322
+ // Both branches yield exactly maxLabelLen cells β€” width invariant.
323
+ // Selected label is centered; filter input is left-aligned so the caret
324
+ // doesn't jitter horizontally while typing.
325
+ //
326
+ // [LAW:one-source-of-truth] All width math is in terminal cells via
327
+ // cellLen / setCellSize / splitText β€” plain `.length` would miscount
328
+ // wide / emoji characters and break the width invariant.
329
+ headerText(maxLabelLen, caret) {
330
+ if (this.filter === "") {
331
+ const raw = this.options[this.selectedIndex] ?? "";
332
+ const w = cellLen(raw);
333
+ if (w >= maxLabelLen)
334
+ return setCellSize(raw, asCellCol(maxLabelLen));
335
+ const pad = maxLabelLen - w;
336
+ const left = Math.floor(pad / 2);
337
+ const right = pad - left;
338
+ return " ".repeat(left) + raw + " ".repeat(right);
339
+ }
340
+ // Leading space gives the filter input a small gutter from the [ chrome,
341
+ // matching the visual breathing room of the centered label.
342
+ const filterRoom = Math.max(0, maxLabelLen - cellLen(caret) - 1);
343
+ const [filterText] = splitText(this.filter, asCellCol(filterRoom));
344
+ return setCellSize(" " + filterText + caret, asCellCol(maxLabelLen));
345
+ }
346
+ renderOverlay(_options) {
347
+ if (!this.expanded)
348
+ return null;
349
+ const maxLabelLen = this.maxLabelLen();
350
+ const fopts = this.filteredOptions;
351
+ if (fopts.length === 0)
352
+ return this.renderNoMatchRow(maxLabelLen);
353
+ const segments = [];
354
+ for (let i = 0; i < fopts.length; i++) {
355
+ if (i > 0)
356
+ segments.push(new Segment("\n"));
357
+ segments.push(...this.renderOptionRow(fopts[i], i, maxLabelLen));
358
+ }
359
+ return segments;
360
+ }
361
+ renderNoMatchRow(maxLabelLen) {
362
+ // Right-clipped + padded so width matches a normal option row.
363
+ const text = setCellSize("(no matches)", asCellCol(maxLabelLen));
364
+ const inner = ` ${text} `;
365
+ const style = this.disabled
366
+ ? new Style(this.disabledInk)
367
+ : new Style({
368
+ ...ink(this.theme, "foreground", "surface"),
369
+ dim: true,
370
+ });
371
+ return [
372
+ new Segment(" ", style),
373
+ new Segment(inner, style),
374
+ new Segment(" ", style),
375
+ ];
376
+ }
377
+ renderOptionRow(entry, fpos, maxLabelLen) {
378
+ // Row: " " + label.padEnd(maxLabelLen) + " "
379
+ // Width = 1 + 1 + maxLabelLen + 1 + 1 = maxLabelLen + 4.
380
+ // Overlay rows are unbracketed β€” selection/highlight are conveyed by bg
381
+ // color; the header keeps [ ] as its focus-indicator chrome.
382
+ const label = setCellSize(entry.label, asCellCol(maxLabelLen));
383
+ const inner = ` ${label} `;
384
+ const isSelected = entry.idx === this.selectedIndex;
385
+ const isHighlighted = fpos === this.highlightedIndex;
386
+ // Highlighted: muted bg β†’ text-primary (mostly-accent fg) is readable.
387
+ // Selected: full primary bg β†’ on-primary (WCAG contrast) for fg.
388
+ const rowStyle = this.disabled
389
+ ? new Style(this.disabledInk)
390
+ : isHighlighted
391
+ ? new Style(ink(this.theme, "text-primary", "primary-muted"))
392
+ : isSelected
393
+ ? new Style(ink(this.theme, "on-primary", "primary"))
394
+ : new Style(ink(this.theme, "foreground", "surface"));
395
+ return [
396
+ new Segment(" ", rowStyle),
397
+ new Segment(inner, rowStyle),
398
+ new Segment(" ", rowStyle),
399
+ ];
400
+ }
401
+ maxLabelLen() {
402
+ let m = 0;
403
+ for (const label of this.options) {
404
+ const w = cellLen(label);
405
+ if (w > m)
406
+ m = w;
407
+ }
408
+ return m;
409
+ }
410
+ measure(_options) {
411
+ const width = this.maxLabelLen() + 4;
412
+ return { minimum: width, maximum: width };
413
+ }
414
+ };
415
+ })();
416
+ export { Dropdown };