@textui/widgets 0.1.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 (423) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/chart/area-chart.d.ts +4 -0
  4. package/dist/chart/area-chart.d.ts.map +1 -0
  5. package/dist/chart/area-chart.js +3 -0
  6. package/dist/chart/bar-chart.d.ts +32 -0
  7. package/dist/chart/bar-chart.d.ts.map +1 -0
  8. package/dist/chart/bar-chart.js +32 -0
  9. package/dist/chart/gauge.d.ts +26 -0
  10. package/dist/chart/gauge.d.ts.map +1 -0
  11. package/dist/chart/gauge.js +26 -0
  12. package/dist/chart/heatmap.d.ts +13 -0
  13. package/dist/chart/heatmap.d.ts.map +1 -0
  14. package/dist/chart/heatmap.js +27 -0
  15. package/dist/chart/histogram.d.ts +12 -0
  16. package/dist/chart/histogram.d.ts.map +1 -0
  17. package/dist/chart/histogram.js +22 -0
  18. package/dist/chart/index.d.ts +20 -0
  19. package/dist/chart/index.d.ts.map +1 -0
  20. package/dist/chart/index.js +32 -0
  21. package/dist/chart/line-chart.d.ts +7 -0
  22. package/dist/chart/line-chart.d.ts.map +1 -0
  23. package/dist/chart/line-chart.js +75 -0
  24. package/dist/chart/shared.d.ts +34 -0
  25. package/dist/chart/shared.d.ts.map +1 -0
  26. package/dist/chart/shared.js +78 -0
  27. package/dist/chart/sparkline.d.ts +15 -0
  28. package/dist/chart/sparkline.d.ts.map +1 -0
  29. package/dist/chart/sparkline.js +22 -0
  30. package/dist/control/button.d.ts +27 -0
  31. package/dist/control/button.d.ts.map +1 -0
  32. package/dist/control/button.js +130 -0
  33. package/dist/control/checkbox.d.ts +10 -0
  34. package/dist/control/checkbox.d.ts.map +1 -0
  35. package/dist/control/checkbox.js +31 -0
  36. package/dist/control/index.d.ts +20 -0
  37. package/dist/control/index.d.ts.map +1 -0
  38. package/dist/control/index.js +36 -0
  39. package/dist/control/radio-group.d.ts +17 -0
  40. package/dist/control/radio-group.d.ts.map +1 -0
  41. package/dist/control/radio-group.js +32 -0
  42. package/dist/control/search-box.d.ts +7 -0
  43. package/dist/control/search-box.d.ts.map +1 -0
  44. package/dist/control/search-box.js +3 -0
  45. package/dist/control/select.d.ts +42 -0
  46. package/dist/control/select.d.ts.map +1 -0
  47. package/dist/control/select.js +166 -0
  48. package/dist/control/shared.d.ts +41 -0
  49. package/dist/control/shared.d.ts.map +1 -0
  50. package/dist/control/shared.js +1 -0
  51. package/dist/control/slider.d.ts +14 -0
  52. package/dist/control/slider.d.ts.map +1 -0
  53. package/dist/control/slider.js +45 -0
  54. package/dist/control/switch.d.ts +10 -0
  55. package/dist/control/switch.d.ts.map +1 -0
  56. package/dist/control/switch.js +32 -0
  57. package/dist/control/text-area.d.ts +87 -0
  58. package/dist/control/text-area.d.ts.map +1 -0
  59. package/dist/control/text-area.js +299 -0
  60. package/dist/control/text-input.d.ts +10 -0
  61. package/dist/control/text-input.d.ts.map +1 -0
  62. package/dist/control/text-input.js +129 -0
  63. package/dist/data/code-viewer.d.ts +71 -0
  64. package/dist/data/code-viewer.d.ts.map +1 -0
  65. package/dist/data/code-viewer.js +232 -0
  66. package/dist/data/feed.d.ts +43 -0
  67. package/dist/data/feed.d.ts.map +1 -0
  68. package/dist/data/feed.js +178 -0
  69. package/dist/data/index.d.ts +54 -0
  70. package/dist/data/index.d.ts.map +1 -0
  71. package/dist/data/index.js +187 -0
  72. package/dist/data/list.d.ts +31 -0
  73. package/dist/data/list.d.ts.map +1 -0
  74. package/dist/data/list.js +104 -0
  75. package/dist/data/log-viewer.d.ts +26 -0
  76. package/dist/data/log-viewer.d.ts.map +1 -0
  77. package/dist/data/log-viewer.js +69 -0
  78. package/dist/data/markdown-view.d.ts +31 -0
  79. package/dist/data/markdown-view.d.ts.map +1 -0
  80. package/dist/data/markdown-view.js +139 -0
  81. package/dist/data/pagination.d.ts +9 -0
  82. package/dist/data/pagination.d.ts.map +1 -0
  83. package/dist/data/pagination.js +18 -0
  84. package/dist/data/scroll-thumb.d.ts +15 -0
  85. package/dist/data/scroll-thumb.d.ts.map +1 -0
  86. package/dist/data/scroll-thumb.js +32 -0
  87. package/dist/data/shared.d.ts +12 -0
  88. package/dist/data/shared.d.ts.map +1 -0
  89. package/dist/data/shared.js +17 -0
  90. package/dist/data/tree.d.ts +35 -0
  91. package/dist/data/tree.d.ts.map +1 -0
  92. package/dist/data/tree.js +158 -0
  93. package/dist/decorations.d.ts +88 -0
  94. package/dist/decorations.d.ts.map +1 -0
  95. package/dist/decorations.js +135 -0
  96. package/dist/display/alert.d.ts +8 -0
  97. package/dist/display/alert.d.ts.map +1 -0
  98. package/dist/display/alert.js +12 -0
  99. package/dist/display/badge.d.ts +10 -0
  100. package/dist/display/badge.d.ts.map +1 -0
  101. package/dist/display/badge.js +21 -0
  102. package/dist/display/card.d.ts +8 -0
  103. package/dist/display/card.d.ts.map +1 -0
  104. package/dist/display/card.js +12 -0
  105. package/dist/display/empty-state.d.ts +10 -0
  106. package/dist/display/empty-state.d.ts.map +1 -0
  107. package/dist/display/empty-state.js +5 -0
  108. package/dist/display/error-state.d.ts +9 -0
  109. package/dist/display/error-state.d.ts.map +1 -0
  110. package/dist/display/error-state.js +7 -0
  111. package/dist/display/heading.d.ts +6 -0
  112. package/dist/display/heading.d.ts.map +1 -0
  113. package/dist/display/heading.js +8 -0
  114. package/dist/display/index.d.ts +25 -0
  115. package/dist/display/index.d.ts.map +1 -0
  116. package/dist/display/index.js +52 -0
  117. package/dist/display/key-value.d.ts +14 -0
  118. package/dist/display/key-value.d.ts.map +1 -0
  119. package/dist/display/key-value.js +11 -0
  120. package/dist/display/label.d.ts +6 -0
  121. package/dist/display/label.d.ts.map +1 -0
  122. package/dist/display/label.js +3 -0
  123. package/dist/display/marquee.d.ts +36 -0
  124. package/dist/display/marquee.d.ts.map +1 -0
  125. package/dist/display/marquee.js +55 -0
  126. package/dist/display/progress.d.ts +39 -0
  127. package/dist/display/progress.d.ts.map +1 -0
  128. package/dist/display/progress.js +46 -0
  129. package/dist/display/skeleton.d.ts +20 -0
  130. package/dist/display/skeleton.d.ts.map +1 -0
  131. package/dist/display/skeleton.js +20 -0
  132. package/dist/display/spinner.d.ts +7 -0
  133. package/dist/display/spinner.d.ts.map +1 -0
  134. package/dist/display/spinner.js +9 -0
  135. package/dist/display/status-dot.d.ts +8 -0
  136. package/dist/display/status-dot.d.ts.map +1 -0
  137. package/dist/display/status-dot.js +13 -0
  138. package/dist/display/timeline.d.ts +12 -0
  139. package/dist/display/timeline.d.ts.map +1 -0
  140. package/dist/display/timeline.js +22 -0
  141. package/dist/find.d.ts +74 -0
  142. package/dist/find.d.ts.map +1 -0
  143. package/dist/find.js +102 -0
  144. package/dist/form/danger-zone.d.ts +11 -0
  145. package/dist/form/danger-zone.d.ts.map +1 -0
  146. package/dist/form/danger-zone.js +15 -0
  147. package/dist/form/field.d.ts +19 -0
  148. package/dist/form/field.d.ts.map +1 -0
  149. package/dist/form/field.js +35 -0
  150. package/dist/form/form-actions.d.ts +11 -0
  151. package/dist/form/form-actions.d.ts.map +1 -0
  152. package/dist/form/form-actions.js +16 -0
  153. package/dist/form/form-section.d.ts +7 -0
  154. package/dist/form/form-section.d.ts.map +1 -0
  155. package/dist/form/form-section.js +5 -0
  156. package/dist/form/form.d.ts +7 -0
  157. package/dist/form/form.d.ts.map +1 -0
  158. package/dist/form/form.js +3 -0
  159. package/dist/form/index.d.ts +42 -0
  160. package/dist/form/index.d.ts.map +1 -0
  161. package/dist/form/index.js +140 -0
  162. package/dist/form/shared.d.ts +25 -0
  163. package/dist/form/shared.d.ts.map +1 -0
  164. package/dist/form/shared.js +9 -0
  165. package/dist/index.d.ts +26 -0
  166. package/dist/index.d.ts.map +1 -0
  167. package/dist/index.js +62 -0
  168. package/dist/layout/center.d.ts +7 -0
  169. package/dist/layout/center.d.ts.map +1 -0
  170. package/dist/layout/center.js +8 -0
  171. package/dist/layout/column.d.ts +3 -0
  172. package/dist/layout/column.d.ts.map +1 -0
  173. package/dist/layout/column.js +2 -0
  174. package/dist/layout/divider.d.ts +11 -0
  175. package/dist/layout/divider.d.ts.map +1 -0
  176. package/dist/layout/divider.js +13 -0
  177. package/dist/layout/grid.d.ts +10 -0
  178. package/dist/layout/grid.d.ts.map +1 -0
  179. package/dist/layout/grid.js +22 -0
  180. package/dist/layout/index.d.ts +19 -0
  181. package/dist/layout/index.d.ts.map +1 -0
  182. package/dist/layout/index.js +36 -0
  183. package/dist/layout/panel.d.ts +37 -0
  184. package/dist/layout/panel.d.ts.map +1 -0
  185. package/dist/layout/panel.js +71 -0
  186. package/dist/layout/row.d.ts +7 -0
  187. package/dist/layout/row.d.ts.map +1 -0
  188. package/dist/layout/row.js +2 -0
  189. package/dist/layout/scroll-view.d.ts +27 -0
  190. package/dist/layout/scroll-view.d.ts.map +1 -0
  191. package/dist/layout/scroll-view.js +93 -0
  192. package/dist/layout/splitter.d.ts +10 -0
  193. package/dist/layout/splitter.d.ts.map +1 -0
  194. package/dist/layout/splitter.js +9 -0
  195. package/dist/layout/stack.d.ts +7 -0
  196. package/dist/layout/stack.d.ts.map +1 -0
  197. package/dist/layout/stack.js +5 -0
  198. package/dist/navigation/breadcrumb.d.ts +14 -0
  199. package/dist/navigation/breadcrumb.d.ts.map +1 -0
  200. package/dist/navigation/breadcrumb.js +24 -0
  201. package/dist/navigation/index.d.ts +17 -0
  202. package/dist/navigation/index.d.ts.map +1 -0
  203. package/dist/navigation/index.js +30 -0
  204. package/dist/navigation/key-hints.d.ts +11 -0
  205. package/dist/navigation/key-hints.d.ts.map +1 -0
  206. package/dist/navigation/key-hints.js +13 -0
  207. package/dist/navigation/menu.d.ts +35 -0
  208. package/dist/navigation/menu.d.ts.map +1 -0
  209. package/dist/navigation/menu.js +88 -0
  210. package/dist/navigation/status-bar.d.ts +18 -0
  211. package/dist/navigation/status-bar.d.ts.map +1 -0
  212. package/dist/navigation/status-bar.js +12 -0
  213. package/dist/navigation/tabs.d.ts +20 -0
  214. package/dist/navigation/tabs.d.ts.map +1 -0
  215. package/dist/navigation/tabs.js +41 -0
  216. package/dist/navigation/toolbar.d.ts +14 -0
  217. package/dist/navigation/toolbar.d.ts.map +1 -0
  218. package/dist/navigation/toolbar.js +10 -0
  219. package/dist/navigation/wizard.d.ts +15 -0
  220. package/dist/navigation/wizard.d.ts.map +1 -0
  221. package/dist/navigation/wizard.js +17 -0
  222. package/dist/overlay/command-palette.d.ts +63 -0
  223. package/dist/overlay/command-palette.d.ts.map +1 -0
  224. package/dist/overlay/command-palette.js +415 -0
  225. package/dist/overlay/dialog.d.ts +16 -0
  226. package/dist/overlay/dialog.d.ts.map +1 -0
  227. package/dist/overlay/dialog.js +44 -0
  228. package/dist/overlay/index.d.ts +84 -0
  229. package/dist/overlay/index.d.ts.map +1 -0
  230. package/dist/overlay/index.js +157 -0
  231. package/dist/overlay/layer-scope.d.ts +21 -0
  232. package/dist/overlay/layer-scope.d.ts.map +1 -0
  233. package/dist/overlay/layer-scope.js +5 -0
  234. package/dist/overlay/path-picker.d.ts +14 -0
  235. package/dist/overlay/path-picker.d.ts.map +1 -0
  236. package/dist/overlay/path-picker.js +200 -0
  237. package/dist/overlay/prompt-dialog.d.ts +12 -0
  238. package/dist/overlay/prompt-dialog.d.ts.map +1 -0
  239. package/dist/overlay/prompt-dialog.js +29 -0
  240. package/dist/overlay/shared.d.ts +17 -0
  241. package/dist/overlay/shared.d.ts.map +1 -0
  242. package/dist/overlay/shared.js +17 -0
  243. package/dist/overlay/toast-host.d.ts +8 -0
  244. package/dist/overlay/toast-host.d.ts.map +1 -0
  245. package/dist/overlay/toast-host.js +19 -0
  246. package/dist/overlay/toast.d.ts +9 -0
  247. package/dist/overlay/toast.d.ts.map +1 -0
  248. package/dist/overlay/toast.js +20 -0
  249. package/dist/overlay/tooltip.d.ts +6 -0
  250. package/dist/overlay/tooltip.d.ts.map +1 -0
  251. package/dist/overlay/tooltip.js +11 -0
  252. package/dist/panel/index.d.ts +79 -0
  253. package/dist/panel/index.d.ts.map +1 -0
  254. package/dist/panel/index.js +256 -0
  255. package/dist/panel/resource-panel.d.ts +93 -0
  256. package/dist/panel/resource-panel.d.ts.map +1 -0
  257. package/dist/panel/resource-panel.js +213 -0
  258. package/dist/panel/shared.d.ts +6 -0
  259. package/dist/panel/shared.d.ts.map +1 -0
  260. package/dist/panel/shared.js +4 -0
  261. package/dist/shells/console-shell.d.ts +8 -0
  262. package/dist/shells/console-shell.d.ts.map +1 -0
  263. package/dist/shells/console-shell.js +30 -0
  264. package/dist/shells/index.d.ts +20 -0
  265. package/dist/shells/index.d.ts.map +1 -0
  266. package/dist/shells/index.js +61 -0
  267. package/dist/shells/paper-shell.d.ts +9 -0
  268. package/dist/shells/paper-shell.d.ts.map +1 -0
  269. package/dist/shells/paper-shell.js +23 -0
  270. package/dist/shells/plain-shell.d.ts +3 -0
  271. package/dist/shells/plain-shell.d.ts.map +1 -0
  272. package/dist/shells/plain-shell.js +11 -0
  273. package/dist/shells/shared.d.ts +12 -0
  274. package/dist/shells/shared.d.ts.map +1 -0
  275. package/dist/shells/shared.js +21 -0
  276. package/dist/shells/workbench-shell.d.ts +9 -0
  277. package/dist/shells/workbench-shell.d.ts.map +1 -0
  278. package/dist/shells/workbench-shell.js +43 -0
  279. package/dist/surface/bar-layout.d.ts +4 -0
  280. package/dist/surface/bar-layout.d.ts.map +1 -0
  281. package/dist/surface/bar-layout.js +7 -0
  282. package/dist/surface/index.d.ts +58 -0
  283. package/dist/surface/index.d.ts.map +1 -0
  284. package/dist/surface/index.js +48 -0
  285. package/dist/surface/inline-layout.d.ts +4 -0
  286. package/dist/surface/inline-layout.d.ts.map +1 -0
  287. package/dist/surface/inline-layout.js +7 -0
  288. package/dist/surface/mount-view.d.ts +7 -0
  289. package/dist/surface/mount-view.d.ts.map +1 -0
  290. package/dist/surface/mount-view.js +19 -0
  291. package/dist/surface/rail-layout.d.ts +4 -0
  292. package/dist/surface/rail-layout.d.ts.map +1 -0
  293. package/dist/surface/rail-layout.js +19 -0
  294. package/dist/surface/shared.d.ts +7 -0
  295. package/dist/surface/shared.d.ts.map +1 -0
  296. package/dist/surface/shared.js +1 -0
  297. package/dist/surface/single-layout.d.ts +4 -0
  298. package/dist/surface/single-layout.d.ts.map +1 -0
  299. package/dist/surface/single-layout.js +18 -0
  300. package/dist/surface/split-layout.d.ts +4 -0
  301. package/dist/surface/split-layout.d.ts.map +1 -0
  302. package/dist/surface/split-layout.js +7 -0
  303. package/dist/surface/stack-layout.d.ts +4 -0
  304. package/dist/surface/stack-layout.d.ts.map +1 -0
  305. package/dist/surface/stack-layout.js +22 -0
  306. package/dist/surface/surface-area.d.ts +16 -0
  307. package/dist/surface/surface-area.d.ts.map +1 -0
  308. package/dist/surface/surface-area.js +32 -0
  309. package/dist/surface/tabs-layout.d.ts +4 -0
  310. package/dist/surface/tabs-layout.d.ts.map +1 -0
  311. package/dist/surface/tabs-layout.js +24 -0
  312. package/dist/tone.d.ts +16 -0
  313. package/dist/tone.d.ts.map +1 -0
  314. package/dist/tone.js +45 -0
  315. package/dist/viewport.d.ts +30 -0
  316. package/dist/viewport.d.ts.map +1 -0
  317. package/dist/viewport.js +17 -0
  318. package/package.json +54 -0
  319. package/src/chart/area-chart.ts +9 -0
  320. package/src/chart/bar-chart.ts +74 -0
  321. package/src/chart/gauge.ts +60 -0
  322. package/src/chart/heatmap.ts +49 -0
  323. package/src/chart/histogram.ts +36 -0
  324. package/src/chart/index.ts +36 -0
  325. package/src/chart/line-chart.ts +100 -0
  326. package/src/chart/shared.ts +112 -0
  327. package/src/chart/sparkline.ts +46 -0
  328. package/src/control/button.ts +184 -0
  329. package/src/control/checkbox.ts +48 -0
  330. package/src/control/index.ts +40 -0
  331. package/src/control/radio-group.ts +60 -0
  332. package/src/control/search-box.ts +15 -0
  333. package/src/control/select.ts +230 -0
  334. package/src/control/shared.ts +41 -0
  335. package/src/control/slider.ts +63 -0
  336. package/src/control/switch.ts +39 -0
  337. package/src/control/text-area.ts +390 -0
  338. package/src/control/text-input.ts +150 -0
  339. package/src/data/code-viewer.ts +337 -0
  340. package/src/data/feed.ts +204 -0
  341. package/src/data/index.ts +243 -0
  342. package/src/data/list.ts +149 -0
  343. package/src/data/log-viewer.ts +105 -0
  344. package/src/data/markdown-view.ts +182 -0
  345. package/src/data/pagination.ts +31 -0
  346. package/src/data/scroll-thumb.ts +40 -0
  347. package/src/data/shared.ts +23 -0
  348. package/src/data/tree.ts +215 -0
  349. package/src/decorations.ts +176 -0
  350. package/src/display/alert.ts +28 -0
  351. package/src/display/badge.ts +40 -0
  352. package/src/display/card.ts +23 -0
  353. package/src/display/empty-state.ts +21 -0
  354. package/src/display/error-state.ts +23 -0
  355. package/src/display/heading.ts +17 -0
  356. package/src/display/index.ts +55 -0
  357. package/src/display/key-value.ts +32 -0
  358. package/src/display/label.ts +11 -0
  359. package/src/display/marquee.ts +88 -0
  360. package/src/display/progress.ts +92 -0
  361. package/src/display/skeleton.ts +34 -0
  362. package/src/display/spinner.ts +20 -0
  363. package/src/display/status-dot.ts +24 -0
  364. package/src/display/timeline.ts +47 -0
  365. package/src/find.ts +138 -0
  366. package/src/form/danger-zone.ts +34 -0
  367. package/src/form/field.ts +61 -0
  368. package/src/form/form-actions.ts +35 -0
  369. package/src/form/form-section.ts +16 -0
  370. package/src/form/form.ts +13 -0
  371. package/src/form/index.ts +191 -0
  372. package/src/form/shared.ts +36 -0
  373. package/src/index.ts +63 -0
  374. package/src/layout/center.ts +17 -0
  375. package/src/layout/column.ts +6 -0
  376. package/src/layout/divider.ts +31 -0
  377. package/src/layout/grid.ts +34 -0
  378. package/src/layout/index.ts +39 -0
  379. package/src/layout/panel.ts +114 -0
  380. package/src/layout/row.ts +11 -0
  381. package/src/layout/scroll-view.ts +115 -0
  382. package/src/layout/splitter.ts +24 -0
  383. package/src/layout/stack.ts +12 -0
  384. package/src/navigation/breadcrumb.ts +43 -0
  385. package/src/navigation/index.ts +33 -0
  386. package/src/navigation/key-hints.ts +25 -0
  387. package/src/navigation/menu.ts +134 -0
  388. package/src/navigation/status-bar.ts +43 -0
  389. package/src/navigation/tabs.ts +73 -0
  390. package/src/navigation/toolbar.ts +25 -0
  391. package/src/navigation/wizard.ts +43 -0
  392. package/src/overlay/command-palette.ts +485 -0
  393. package/src/overlay/dialog.ts +65 -0
  394. package/src/overlay/index.ts +232 -0
  395. package/src/overlay/layer-scope.ts +26 -0
  396. package/src/overlay/path-picker.ts +215 -0
  397. package/src/overlay/prompt-dialog.ts +45 -0
  398. package/src/overlay/shared.ts +19 -0
  399. package/src/overlay/toast-host.ts +30 -0
  400. package/src/overlay/toast.ts +35 -0
  401. package/src/overlay/tooltip.ts +17 -0
  402. package/src/panel/index.ts +317 -0
  403. package/src/panel/resource-panel.ts +304 -0
  404. package/src/panel/shared.ts +8 -0
  405. package/src/shells/console-shell.ts +47 -0
  406. package/src/shells/index.ts +66 -0
  407. package/src/shells/paper-shell.ts +31 -0
  408. package/src/shells/plain-shell.ts +19 -0
  409. package/src/shells/shared.ts +24 -0
  410. package/src/shells/workbench-shell.ts +62 -0
  411. package/src/surface/bar-layout.ts +11 -0
  412. package/src/surface/index.ts +53 -0
  413. package/src/surface/inline-layout.ts +11 -0
  414. package/src/surface/mount-view.ts +30 -0
  415. package/src/surface/rail-layout.ts +27 -0
  416. package/src/surface/shared.ts +7 -0
  417. package/src/surface/single-layout.ts +25 -0
  418. package/src/surface/split-layout.ts +11 -0
  419. package/src/surface/stack-layout.ts +32 -0
  420. package/src/surface/surface-area.ts +46 -0
  421. package/src/surface/tabs-layout.ts +30 -0
  422. package/src/tone.ts +48 -0
  423. package/src/viewport.ts +45 -0
@@ -0,0 +1,243 @@
1
+ import type { BoxProps, ComponentDefinition, RenderOutput, SemanticVariant } from '@textui/core';
2
+ import {
3
+ chorded,
4
+ defineComponent,
5
+ fitTo,
6
+ h,
7
+ stringWidth,
8
+ useFocus,
9
+ useInput,
10
+ useMeasure,
11
+ useState,
12
+ useTheme,
13
+ } from '@textui/core';
14
+ import { EmptyState } from '../display/index.js';
15
+ import { TONE } from '../tone.js';
16
+ import { viewportRows } from '../viewport.js';
17
+ import { CodeViewer } from './code-viewer.js';
18
+ import { Feed } from './feed.js';
19
+ import { List } from './list.js';
20
+ import { LogViewer } from './log-viewer.js';
21
+ import { MarkdownView } from './markdown-view.js';
22
+ import { Pagination } from './pagination.js';
23
+ import { Tree } from './tree.js';
24
+
25
+ /**
26
+ * Lists, tables and trees.
27
+ *
28
+ * These are the components that own real interaction state - selection,
29
+ * keyboard navigation, scrolling - so they are the ones where a controlled and
30
+ * an uncontrolled mode both have to work. The rule throughout: if the caller
31
+ * passes the value, the caller owns it; otherwise the component keeps it.
32
+ */
33
+ export * from './code-viewer.js';
34
+ export * from './feed.js';
35
+ export * from './list.js';
36
+ export * from './log-viewer.js';
37
+ export * from './markdown-view.js';
38
+ export * from './pagination.js';
39
+ export * from './scroll-thumb.js';
40
+ export * from './tree.js';
41
+
42
+ export interface TableColumn<T extends object = Record<string, unknown>> {
43
+ key: string;
44
+ header: string;
45
+ /** Fixed cell width. Omit to size from the content. */
46
+ width?: number;
47
+ align?: 'left' | 'center' | 'right';
48
+ /** Lower priority columns are dropped first as the table narrows. */
49
+ priority?: number;
50
+ /** Render a cell to a string. Defaults to `String(row[key])`. */
51
+ format?(value: unknown, row: T): string;
52
+ tone?(value: unknown, row: T): SemanticVariant | undefined;
53
+ /** Take the whole remaining width. At most one column should. */
54
+ flex?: boolean;
55
+ }
56
+
57
+ export interface TableProps<T extends object = Record<string, unknown>> extends BoxProps {
58
+ columns: TableColumn<T>[];
59
+ rows: T[];
60
+ /** Field used as the row key. Defaults to `id`. */
61
+ rowKey?: string;
62
+ selectedKey?: string;
63
+ onSelect?(key: string, row: T): void;
64
+ onActivate?(key: string, row: T): void;
65
+ visibleRows?: number;
66
+ showHeader?: boolean;
67
+ emptyMessage?: string;
68
+ focusable?: boolean;
69
+ /** Width below which low-priority columns start being dropped. */
70
+ responsive?: boolean;
71
+ }
72
+
73
+ /**
74
+ * A data table.
75
+ *
76
+ * Responsiveness is a column-priority problem, not a font-size problem: as the
77
+ * table narrows it drops the lowest-priority columns rather than squeezing
78
+ * every column until none of them is readable.
79
+ */
80
+ const TableImpl = defineComponent<TableProps<Record<string, unknown>>>('Table', (props) => {
81
+ const theme = useTheme();
82
+ const {
83
+ columns, rows, rowKey = 'id', selectedKey, onSelect, onActivate,
84
+ visibleRows, showHeader = true, emptyMessage = 'No rows',
85
+ focusable = true, responsive = true, width, ...rest
86
+ } = props;
87
+
88
+ const focus = useFocus({ disabled: !focusable });
89
+ const measured = useMeasure();
90
+ const [internalKey, setInternalKey] = useState<string | null>(null);
91
+ const keyOf = (row: Record<string, unknown>): string => String(row[rowKey] ?? '');
92
+
93
+ // The header is a row of the pane too, so it comes out of the body's share.
94
+ const bodyRows = viewportRows(props, measured, rows.length, {
95
+ requested: visibleRows,
96
+ reserved: showHeader ? 1 : 0,
97
+ });
98
+ const pageSize = bodyRows;
99
+
100
+ const currentKey = selectedKey ?? internalKey ?? (rows[0] ? keyOf(rows[0]) : null);
101
+ const index = Math.max(0, rows.findIndex((r) => keyOf(r) === currentKey));
102
+
103
+ const select = (next: number): void => {
104
+ const row = rows[Math.max(0, Math.min(rows.length - 1, next))];
105
+ if (!row) return;
106
+ if (selectedKey === undefined) setInternalKey(keyOf(row));
107
+ onSelect?.(keyOf(row), row);
108
+ };
109
+
110
+ useInput(
111
+ (event) => {
112
+ if (rows.length === 0 || chorded(event)) return false;
113
+ switch (event.name) {
114
+ case 'up': select(index - 1); return true;
115
+ case 'down': select(index + 1); return true;
116
+ case 'home': select(0); return true;
117
+ case 'end': select(rows.length - 1); return true;
118
+ case 'pageup': select(index - pageSize); return true;
119
+ case 'pagedown': select(index + pageSize); return true;
120
+ case 'enter': {
121
+ const row = rows[index];
122
+ if (row) onActivate?.(keyOf(row), row);
123
+ return true;
124
+ }
125
+ default: return false;
126
+ }
127
+ },
128
+ { focusId: focus.id, enabled: focusable },
129
+ );
130
+
131
+ if (rows.length === 0) return h(EmptyState, { title: emptyMessage, ...rest });
132
+
133
+ // Column widths: fixed where stated, else the widest cell, capped.
134
+ const columnWidths = columns.map((col) => {
135
+ if (col.width !== undefined) return col.width;
136
+ const header = showHeader ? stringWidth(col.header) : 0;
137
+ const cells = rows.map((row) => {
138
+ const value = (row as Record<string, unknown>)[col.key];
139
+ return stringWidth(col.format ? col.format(value, row) : String(value ?? ''));
140
+ });
141
+ return Math.max(header, ...cells, 1);
142
+ });
143
+
144
+ const gap = theme.density === 'compact' ? 1 : 2;
145
+ const available = typeof width === 'number' ? width : Infinity;
146
+
147
+ // Drop the lowest-priority columns until what remains fits.
148
+ //
149
+ // Two rules keep this from making a table worse. A column that states no
150
+ // priority inherits its position - earlier columns matter more - so an
151
+ // unstated column never ties with one explicitly marked unimportant. And the
152
+ // first column is never dropped: it is what identifies the row, and a table
153
+ // of measurements with nothing to attach them to is not a smaller table, it
154
+ // is a useless one.
155
+ let shown = columns.map((col, i) => ({
156
+ col,
157
+ width: columnWidths[i] as number,
158
+ priority: col.priority ?? columns.length - i,
159
+ index: i,
160
+ }));
161
+
162
+ if (responsive && Number.isFinite(available)) {
163
+ const total = (): number =>
164
+ shown.reduce((sum, c) => sum + c.width, 0) + gap * Math.max(0, shown.length - 1);
165
+
166
+ while (total() > available && shown.length > 1) {
167
+ let worstAt = -1;
168
+ let worst = Infinity;
169
+ // Skip index 0, and prefer the rightmost on a tie.
170
+ for (let i = 1; i < shown.length; i++) {
171
+ const p = (shown[i] as { priority: number }).priority;
172
+ if (p <= worst) { worst = p; worstAt = i; }
173
+ }
174
+ if (worstAt === -1) break;
175
+ shown = shown.filter((_, i) => i !== worstAt);
176
+ }
177
+ }
178
+
179
+ const rowsWindow = (): { row: Record<string, unknown>; key: string }[] => {
180
+ const all = rows.map((row) => ({ row: row as Record<string, unknown>, key: keyOf(row) }));
181
+ if (all.length <= bodyRows) return all;
182
+ const start = Math.max(0, Math.min(index - Math.floor(bodyRows / 2), all.length - bodyRows));
183
+ return all.slice(start, start + bodyRows);
184
+ };
185
+
186
+ const cell = (text: string, entry: { col: TableColumn; width: number }, tone?: SemanticVariant): unknown =>
187
+ h('box', { key: entry.col.key, width: entry.col.flex ? undefined : entry.width, flex: entry.col.flex ? 1 : undefined },
188
+ h('text', {
189
+ content: fitTo(text, entry.width, entry.col.align ?? 'left'),
190
+ fg: tone ? TONE[tone] : undefined,
191
+ truncate: 'end',
192
+ }));
193
+
194
+ return h('box', { id: focus.id, role: 'table', direction: 'column', width, ...rest },
195
+ showHeader
196
+ ? h('box', { role: 'row', direction: 'row', gap, dim: true },
197
+ ...shown.map((entry) =>
198
+ h('box', { key: entry.col.key, width: entry.col.flex ? undefined : entry.width, flex: entry.col.flex ? 1 : undefined, role: 'columnheader' },
199
+ h('text', { content: fitTo(entry.col.header, entry.width, entry.col.align ?? 'left') }))))
200
+ : null,
201
+
202
+ ...rowsWindow().map(({ row, key }) => {
203
+ const active = key === currentKey;
204
+ return h('box', {
205
+ key,
206
+ role: 'row',
207
+ selected: active,
208
+ direction: 'row',
209
+ gap,
210
+ bg: active && focus.focused ? 'selected' : active ? 'active' : undefined,
211
+ fg: active && focus.focused ? 'inverted' : undefined,
212
+ onClick: () => {
213
+ if (selectedKey === undefined) setInternalKey(key);
214
+ onSelect?.(key, row);
215
+ },
216
+ },
217
+ ...shown.map((entry) => {
218
+ const value = row[entry.col.key];
219
+ const text = entry.col.format ? entry.col.format(value, row) : String(value ?? '');
220
+ return cell(text, entry, entry.col.tone?.(value, row));
221
+ }),
222
+ );
223
+ }),
224
+ );
225
+ });
226
+
227
+ /**
228
+ * Exported with its own generic signature so a caller's row type flows into
229
+ * `format` and `tone` - the alternative is every table taking
230
+ * `Record<string, unknown>` and every cell function starting with a cast.
231
+ */
232
+ export const Table = TableImpl as <T extends object>(props: TableProps<T>) => RenderOutput;
233
+
234
+ export const DATA_COMPONENTS: ComponentDefinition[] = [
235
+ { component: 'List', category: 'data', renderer: { kind: 'function', render: List }, role: 'list', description: 'Selectable rows with keyboard navigation.' },
236
+ { component: 'Table', category: 'data', renderer: { kind: 'function', render: TableImpl }, role: 'table', description: 'Columns that drop by priority as space runs out.' },
237
+ { component: 'Tree', category: 'data', renderer: { kind: 'function', render: Tree }, role: 'tree', description: 'Expandable hierarchy.' },
238
+ { component: 'Pagination', category: 'data', renderer: { kind: 'function', render: Pagination }, description: 'Page of pages.' },
239
+ { component: 'LogViewer', category: 'data', renderer: { kind: 'function', render: LogViewer }, role: 'log', description: 'Streaming lines that follow the tail until you scroll.' },
240
+ { component: 'CodeViewer', category: 'data', renderer: { kind: 'function', render: CodeViewer }, role: 'document', description: 'A scrolling, syntax-coloured file viewer.' },
241
+ { component: 'MarkdownView', category: 'data', renderer: { kind: 'function', render: MarkdownView }, role: 'document', description: 'Markdown drawn into the width it was given. Does not scroll.' },
242
+ { component: 'Feed', category: 'data', renderer: { kind: 'function', render: Feed }, role: 'list', description: 'A viewport over entries of any height, with a cursor and a tail it follows.' },
243
+ ];
@@ -0,0 +1,149 @@
1
+ import type { BoxProps, SemanticVariant } from '@textui/core';
2
+ import {
3
+ chorded,
4
+ defineComponent,
5
+ h,
6
+ useFocus,
7
+ useInput,
8
+ useMeasure,
9
+ useState,
10
+ useTheme,
11
+ } from '@textui/core';
12
+ import { EmptyState } from '../display/index.js';
13
+ import { TONE } from '../tone.js';
14
+ import { viewportRows } from '../viewport.js';
15
+
16
+
17
+ export interface ListItem {
18
+ id: string;
19
+ label: string;
20
+ description?: string;
21
+ icon?: string;
22
+ tone?: SemanticVariant;
23
+ meta?: string;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ export interface ListProps extends BoxProps {
28
+ items: ListItem[];
29
+ selectedId?: string;
30
+ onSelect?(id: string, item: ListItem): void;
31
+ onActivate?(id: string, item: ListItem): void;
32
+ /** Rows visible at once. Scrolls when there are more. */
33
+ visibleRows?: number;
34
+ emptyMessage?: string;
35
+ /** Draw a marker column for the selected row. */
36
+ marker?: boolean;
37
+ focusable?: boolean;
38
+ autoFocus?: boolean;
39
+ /**
40
+ * A stable focus id, so a command - or the screen that owns this - can put
41
+ * the reader here by name. Without one the id comes from the instance, which
42
+ * nothing outside the render can know.
43
+ */
44
+ focusId?: string;
45
+ }
46
+
47
+ export const List = defineComponent<ListProps>('List', (props) => {
48
+ const theme = useTheme();
49
+ const {
50
+ items, selectedId, onSelect, onActivate, visibleRows,
51
+ emptyMessage = 'Nothing here', marker = true, focusable = true,
52
+ autoFocus, focusId, ...rest
53
+ } = props;
54
+
55
+ const focus = useFocus({
56
+ ...(focusId ? { id: focusId } : {}),
57
+ disabled: !focusable,
58
+ ...(autoFocus ? { autoFocus } : {}),
59
+ });
60
+ const measured = useMeasure();
61
+ const [internalId, setInternalId] = useState<string | null>(
62
+ items.find((i) => !i.disabled)?.id ?? null,
63
+ );
64
+ const currentId = selectedId ?? internalId;
65
+ const pageSize = viewportRows(props, measured, 10, { requested: visibleRows });
66
+ const index = Math.max(0, items.findIndex((i) => i.id === currentId));
67
+
68
+ /**
69
+ * Move to `target`, skipping past anything unselectable on the way.
70
+ *
71
+ * The direction has to be given rather than inferred, because `home` and
72
+ * `end` land on an index without coming from one. Stopping at a disabled row
73
+ * instead of stepping over it makes a heading into a wall - which is what a
74
+ * grouped list is made of, so it could not be built at all.
75
+ */
76
+ const selectAt = (target: number, direction: 1 | -1): void => {
77
+ let i = Math.max(0, Math.min(items.length - 1, target));
78
+ while (i >= 0 && i < items.length && items[i]?.disabled === true) i += direction;
79
+ const item = items[i];
80
+ if (!item) return;
81
+ if (selectedId === undefined) setInternalId(item.id);
82
+ onSelect?.(item.id, item);
83
+ };
84
+
85
+ useInput(
86
+ (event) => {
87
+ if (items.length === 0 || chorded(event)) return false;
88
+ switch (event.name) {
89
+ case 'up': selectAt(index - 1, -1); return true;
90
+ case 'down': selectAt(index + 1, 1); return true;
91
+ case 'home': selectAt(0, 1); return true;
92
+ case 'end': selectAt(items.length - 1, -1); return true;
93
+ case 'pageup': selectAt(index - pageSize, -1); return true;
94
+ case 'pagedown': selectAt(index + pageSize, 1); return true;
95
+ case 'enter': {
96
+ const item = items[index];
97
+ if (item && item.disabled !== true) onActivate?.(item.id, item);
98
+ return true;
99
+ }
100
+ default: return false;
101
+ }
102
+ },
103
+ { focusId: focus.id, enabled: focusable },
104
+ );
105
+
106
+ if (items.length === 0) {
107
+ return h(EmptyState, { title: emptyMessage, ...rest });
108
+ }
109
+
110
+ // Keep the selection in view without moving it more than necessary.
111
+ //
112
+ // Unstated, the row count comes from the space the list was given rather
113
+ // than from how many items it holds - a thousand-row list must not decide
114
+ // how tall its own pane is.
115
+ const rows = viewportRows(props, measured, items.length, { requested: visibleRows });
116
+ const start = Math.max(0, Math.min(index - Math.floor(rows / 2), items.length - rows));
117
+ const window = items.slice(start, start + rows);
118
+
119
+ return h('box', { id: focus.id, role: 'list', direction: 'column', ...rest },
120
+ ...window.map((item) => {
121
+ const active = item.id === currentId;
122
+ return h('box', {
123
+ key: item.id,
124
+ role: 'listitem',
125
+ label: item.label,
126
+ selected: active,
127
+ direction: 'row',
128
+ gap: 1,
129
+ bg: active && focus.focused ? 'selected' : active ? 'active' : undefined,
130
+ fg: item.disabled ? 'disabled' : active && focus.focused ? 'inverted' : undefined,
131
+ onClick: () => {
132
+ if (item.disabled) return;
133
+ if (selectedId === undefined) setInternalId(item.id);
134
+ onSelect?.(item.id, item);
135
+ },
136
+ },
137
+ marker
138
+ ? h('text', { content: active ? theme.glyphs.chevronRight : ' ' })
139
+ : null,
140
+ item.icon ? h('text', { content: item.icon, fg: item.tone ? TONE[item.tone] : undefined }) : null,
141
+ h('text', { content: item.label, flex: 1, truncate: 'end' }),
142
+ // The secondary columns keep the row's colour once it is selected;
143
+ // `muted` on a selected background is unreadable.
144
+ item.description ? h('text', { content: item.description, fg: active ? undefined : 'muted', truncate: 'end' }) : null,
145
+ item.meta ? h('text', { content: item.meta, fg: active ? undefined : 'muted' }) : null,
146
+ );
147
+ }),
148
+ );
149
+ });
@@ -0,0 +1,105 @@
1
+ import type { BoxProps, SemanticVariant } from '@textui/core';
2
+ import {
3
+ chorded,
4
+ defineComponent,
5
+ h,
6
+ useFocus,
7
+ useInput,
8
+ useMeasure,
9
+ useState,
10
+ } from '@textui/core';
11
+ import { TONE } from '../tone.js';
12
+ import { viewportRows } from '../viewport.js';
13
+
14
+ export interface LogLine {
15
+ time?: string;
16
+ level?: 'debug' | 'info' | 'warn' | 'error';
17
+ source?: string;
18
+ message: string;
19
+ }
20
+
21
+ export interface LogViewerProps extends BoxProps {
22
+ lines: LogLine[];
23
+ /** Rows shown. Older lines scroll off the top. */
24
+ visibleRows?: number;
25
+ /** Stick to the newest line. Turned off when the reader scrolls up. */
26
+ follow?: boolean;
27
+ showTime?: boolean;
28
+ showLevel?: boolean;
29
+ onFollowChange?(follow: boolean): void;
30
+ }
31
+
32
+ const LEVEL_TONE: Record<string, SemanticVariant> = {
33
+ debug: 'muted', info: 'info', warn: 'warning', error: 'danger',
34
+ };
35
+
36
+ /**
37
+ * Streaming text.
38
+ *
39
+ * A log viewer follows the tail until the reader scrolls, then stops - the one
40
+ * behaviour that makes the difference between a log you can read and a log
41
+ * that yanks itself out from under you.
42
+ */
43
+ export const LogViewer = defineComponent<LogViewerProps>('LogViewer', (props) => {
44
+ const {
45
+ lines, visibleRows: visibleRowsProp, follow: followProp, showTime = true,
46
+ showLevel = true, onFollowChange, ...rest
47
+ } = props;
48
+
49
+ const focus = useFocus({});
50
+ const measured = useMeasure();
51
+ const [offset, setOffset] = useState(0);
52
+ const [following, setFollowing] = useState(followProp ?? true);
53
+ const follow = followProp ?? following;
54
+
55
+ // Ten rows was a guess. The pane knows the answer.
56
+ const visibleRows = viewportRows(props, measured, visibleRowsProp ?? 10, {
57
+ requested: visibleRowsProp,
58
+ });
59
+ const maxOffset = Math.max(0, lines.length - visibleRows);
60
+ const top = follow ? maxOffset : Math.min(offset, maxOffset);
61
+
62
+ const scroll = (delta: number): void => {
63
+ const next = Math.max(0, Math.min(maxOffset, top + delta));
64
+ setOffset(next);
65
+ const nowFollowing = next >= maxOffset;
66
+ if (followProp === undefined) setFollowing(nowFollowing);
67
+ onFollowChange?.(nowFollowing);
68
+ };
69
+
70
+ useInput(
71
+ (event) => {
72
+ if (chorded(event)) return false;
73
+ switch (event.name) {
74
+ case 'up': scroll(-1); return true;
75
+ case 'down': scroll(1); return true;
76
+ case 'pageup': scroll(-visibleRows); return true;
77
+ case 'pagedown': scroll(visibleRows); return true;
78
+ case 'home': scroll(-lines.length); return true;
79
+ case 'end': scroll(lines.length); return true;
80
+ default: return false;
81
+ }
82
+ },
83
+ { focusId: focus.id },
84
+ );
85
+
86
+ const window = lines.slice(top, top + visibleRows);
87
+
88
+ return h('box', { id: focus.id, role: 'log', direction: 'column', ...rest },
89
+ ...window.map((line, i) =>
90
+ h('box', { key: top + i, direction: 'row', gap: 1 },
91
+ showTime && line.time ? h('text', { content: line.time, fg: 'subtle' }) : null,
92
+ showLevel && line.level
93
+ ? h('text', {
94
+ content: line.level.toUpperCase().padEnd(5),
95
+ fg: TONE[LEVEL_TONE[line.level] ?? 'muted'],
96
+ })
97
+ : null,
98
+ line.source ? h('text', { content: line.source, fg: 'muted' }) : null,
99
+ h('text', { content: line.message, flex: 1, truncate: 'end' }),
100
+ )),
101
+ !follow
102
+ ? h('text', { content: ' paused - end to follow', fg: 'warning', dim: true })
103
+ : null,
104
+ );
105
+ });
@@ -0,0 +1,182 @@
1
+ import type { BoxProps, MarkdownRow, MarkdownRun, ResolvedTheme, StyleColor } from '@textui/core';
2
+ import {
3
+ defineComponent,
4
+ h,
5
+ layoutMarkdown,
6
+ repeatToWidth,
7
+ useMeasure,
8
+ useMemo,
9
+ useTheme,
10
+ } from '@textui/core';
11
+ import { CodeViewer } from './code-viewer.js';
12
+
13
+ // ------------------------------------------------------------------ markdown
14
+
15
+ export interface MarkdownViewProps extends BoxProps {
16
+ /** The document. Ignored when `rows` is passed. */
17
+ content?: string;
18
+ /** Already laid out - for a viewer that windows the rows itself. */
19
+ rows?: MarkdownRow[];
20
+ /** Paint only this slice. The caller owns the scrolling when it passes one. */
21
+ window?: { first: number; count: number };
22
+ /** Collapse past this many rows, with a count of what is hidden. */
23
+ maxLines?: number;
24
+ /** Dim everything, for reasoning and other second-voice text. */
25
+ quiet?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Markdown, drawn into the width it was given.
30
+ *
31
+ * It fills the space it is in and does not scroll: a document viewer owns its
32
+ * viewport and a message in a transcript does not, so scrolling belongs to
33
+ * whoever holds the rows, not to the thing that paints them. Pass `content`
34
+ * and it lays out what it measured; pass `rows` and a `window` and it paints
35
+ * that slice of somebody else's layout.
36
+ *
37
+ * Inline emphasis, code and links survive, because they are meaning rather
38
+ * than markup wherever the text was written by an agent or a service for a
39
+ * person to read.
40
+ */
41
+ export const MarkdownView = defineComponent<MarkdownViewProps>('MarkdownView', (props) => {
42
+ const { content = '', rows: given, window: slice, maxLines, quiet, ...rest } = props;
43
+ const theme = useTheme();
44
+ const measured = useMeasure();
45
+ const width = measured.width > 0 ? measured.width : 0;
46
+ const ruled = theme.border !== 'none';
47
+
48
+ const laid = useMemo(
49
+ () => given ?? layoutMarkdown(content, {
50
+ width,
51
+ bullet: theme.glyphs.bulletFilled,
52
+ quoteBar: theme.borderChars().left,
53
+ ruled,
54
+ }),
55
+ [given, content, width, theme, ruled],
56
+ );
57
+
58
+ const first = slice?.first ?? 0;
59
+ const shown = slice ? laid.slice(first, first + slice.count) : laid;
60
+ const capped = maxLines !== undefined ? shown.slice(0, maxLines) : shown;
61
+ const hidden = shown.length - capped.length;
62
+
63
+ const out: unknown[] = [];
64
+ for (let i = 0; i < capped.length;) {
65
+ const row = capped[i] as MarkdownRow;
66
+ const key = first + i;
67
+
68
+ if (row.kind === 'fence') {
69
+ // A fence the window cuts through is still one box, with only the rules
70
+ // that are actually on screen.
71
+ let end = i;
72
+ while (end < capped.length) {
73
+ const next = capped[end] as MarkdownRow;
74
+ if (next.kind !== 'fence' || next.fence !== row.fence) break;
75
+ end++;
76
+ }
77
+ const group = capped.slice(i, end) as Extract<MarkdownRow, { kind: 'fence' }>[];
78
+ const code = group.filter((r) => r.part === 'code').map((r) => r.text ?? '');
79
+ const language = group.find((r) => r.language)?.language;
80
+
81
+ if (!ruled) {
82
+ out.push(h('box', { key, padding: [0, 1], bg: 'surfaceAlt' }, h(CodeViewer, {
83
+ content: code.join('\n'),
84
+ lineNumbers: false, scrollbar: false, showCaret: false, disabled: true,
85
+ ...(language ? { language } : {}),
86
+ })));
87
+ } else if (code.length === 0) {
88
+ // Only one edge of the box is on screen, and a box one row tall cannot
89
+ // say which. Drawn directly, so the seam is invisible as it scrolls.
90
+ out.push(h('text', {
91
+ key,
92
+ content: fenceEdge(theme, width, group.some((r) => r.part === 'open')),
93
+ fg: 'borderSubtle',
94
+ wrap: 'none',
95
+ }));
96
+ } else {
97
+ out.push(h('box', {
98
+ key,
99
+ border: {
100
+ style: theme.border,
101
+ color: 'borderSubtle',
102
+ sides: {
103
+ top: group.some((r) => r.part === 'open'),
104
+ bottom: group.some((r) => r.part === 'close'),
105
+ left: true,
106
+ right: true,
107
+ },
108
+ },
109
+ padding: [0, 1],
110
+ }, h(CodeViewer, {
111
+ content: code.join('\n'),
112
+ lineNumbers: false, scrollbar: false, showCaret: false,
113
+ // A fence inside a rendered document is typography, not a control.
114
+ // Focusable, every code block in a README becomes a tab stop.
115
+ disabled: true,
116
+ ...(language ? { language } : {}),
117
+ })));
118
+ }
119
+ i = end;
120
+ continue;
121
+ }
122
+
123
+ if (row.kind === 'rule') {
124
+ out.push(h('box', { key, height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }));
125
+ i++;
126
+ continue;
127
+ }
128
+
129
+ if (row.kind === 'heading') {
130
+ out.push(h('box', { key, direction: 'row', overflow: 'hidden' },
131
+ ...runNodes(row.runs, {
132
+ bold: row.level <= 2,
133
+ underline: row.level === 1,
134
+ fg: quiet ? 'muted' : row.level <= 2 ? 'text' : 'muted',
135
+ })));
136
+ i++;
137
+ continue;
138
+ }
139
+
140
+ const style = { ...(quiet ? { fg: 'muted' as StyleColor } : row.fg ? { fg: row.fg } : {}) };
141
+ if (row.prefix !== undefined) {
142
+ out.push(h('box', { key, direction: 'row', gap: 1, overflow: 'hidden' },
143
+ h('text', { content: row.prefix, fg: quiet ? 'subtle' : row.prefixFg ?? 'accent' }),
144
+ h('box', { direction: 'row', flex: 1, overflow: 'hidden' }, ...runNodes(row.runs, style))));
145
+ i++;
146
+ continue;
147
+ }
148
+
149
+ out.push(h('box', { key, direction: 'row', overflow: 'hidden' }, ...runNodes(row.runs, style)));
150
+ i++;
151
+ }
152
+
153
+ return h('box', { role: 'document', direction: 'column', ...rest },
154
+ ...out,
155
+ hidden > 0
156
+ ? h('text', { content: `${theme.glyphs.ellipsis} ${hidden} more lines`, fg: 'subtle' })
157
+ : null,
158
+ );
159
+ });
160
+
161
+ /** One `text` per run, so a bold half of a sentence stays bold after wrapping. */
162
+ function runNodes(runs: MarkdownRun[], style: Record<string, unknown>): unknown[] {
163
+ return runs.map((run, i) => h('text', {
164
+ key: i,
165
+ content: run.text,
166
+ wrap: 'none',
167
+ ...style,
168
+ ...(run.bold ? { bold: true } : {}),
169
+ ...(run.italic ? { italic: true } : {}),
170
+ ...(run.code ? { fg: 'accent', bg: 'surfaceAlt' } : {}),
171
+ ...(run.link ? { underline: true, fg: 'info', link: run.link } : {}),
172
+ }));
173
+ }
174
+
175
+ /** The top or bottom edge of a fence, for when the window shows only that row. */
176
+ function fenceEdge(theme: ResolvedTheme, width: number, top: boolean): string {
177
+ const chars = theme.borderChars();
178
+ const [left, mid, right] = top
179
+ ? [chars.topLeft, chars.top, chars.topRight]
180
+ : [chars.bottomLeft, chars.bottom, chars.bottomRight];
181
+ return `${left}${repeatToWidth(mid, Math.max(0, width - 2))}${right}`;
182
+ }