@visulima/tui-kit 0.0.0 → 1.0.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 (371) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE.md +2095 -0
  3. package/README.md +115 -29
  4. package/dist/accordion.d.ts +59 -0
  5. package/dist/accordion.js +1 -0
  6. package/dist/alert.d.ts +26 -0
  7. package/dist/alert.js +1 -0
  8. package/dist/animate-presence.d.ts +18 -0
  9. package/dist/animate-presence.js +1 -0
  10. package/dist/approval-prompt.d.ts +65 -0
  11. package/dist/approval-prompt.js +1 -0
  12. package/dist/area-chart.d.ts +66 -0
  13. package/dist/area-chart.js +1 -0
  14. package/dist/badge.d.ts +22 -0
  15. package/dist/badge.js +1 -0
  16. package/dist/bar-chart.d.ts +67 -0
  17. package/dist/bar-chart.js +1 -0
  18. package/dist/big-text.d.ts +81 -0
  19. package/dist/big-text.js +1 -0
  20. package/dist/blink-dot.d.ts +36 -0
  21. package/dist/blink-dot.js +1 -0
  22. package/dist/breadcrumb.d.ts +33 -0
  23. package/dist/breadcrumb.js +1 -0
  24. package/dist/button.d.ts +45 -0
  25. package/dist/button.js +1 -0
  26. package/dist/calendar.d.ts +72 -0
  27. package/dist/calendar.js +1 -0
  28. package/dist/card.d.ts +57 -0
  29. package/dist/card.js +1 -0
  30. package/dist/checkbox-group.d.ts +59 -0
  31. package/dist/checkbox-group.js +1 -0
  32. package/dist/checkbox.d.ts +48 -0
  33. package/dist/checkbox.js +1 -0
  34. package/dist/code.d.ts +48 -0
  35. package/dist/code.js +2 -0
  36. package/dist/collapsible.d.ts +53 -0
  37. package/dist/collapsible.js +1 -0
  38. package/dist/color-picker.d.ts +71 -0
  39. package/dist/color-picker.js +1 -0
  40. package/dist/command-block.d.ts +47 -0
  41. package/dist/command-block.js +4 -0
  42. package/dist/command-palette.d.ts +82 -0
  43. package/dist/command-palette.js +1 -0
  44. package/dist/confirm-dialog.d.ts +75 -0
  45. package/dist/confirm-dialog.js +1 -0
  46. package/dist/confirm-input.d.ts +38 -0
  47. package/dist/confirm-input.js +1 -0
  48. package/dist/console-overlay.d.ts +54 -0
  49. package/dist/console-overlay.js +1 -0
  50. package/dist/content-switcher.d.ts +57 -0
  51. package/dist/content-switcher.js +1 -0
  52. package/dist/controlled-scroll-view.d.ts +3 -0
  53. package/dist/controlled-scroll-view.js +1 -0
  54. package/dist/data-grid.d.ts +49 -0
  55. package/dist/data-grid.js +1 -0
  56. package/dist/date-picker.d.ts +65 -0
  57. package/dist/date-picker.js +1 -0
  58. package/dist/definition-list.d.ts +44 -0
  59. package/dist/definition-list.js +1 -0
  60. package/dist/dialog.d.ts +110 -0
  61. package/dist/dialog.js +1 -0
  62. package/dist/diff-view.d.ts +85 -0
  63. package/dist/diff-view.js +1 -0
  64. package/dist/digits.d.ts +30 -0
  65. package/dist/digits.js +1 -0
  66. package/dist/directory-tree.d.ts +51 -0
  67. package/dist/directory-tree.js +1 -0
  68. package/dist/divider.d.ts +49 -0
  69. package/dist/divider.js +1 -0
  70. package/dist/drawer.d.ts +53 -0
  71. package/dist/drawer.js +1 -0
  72. package/dist/file-change.d.ts +40 -0
  73. package/dist/file-change.js +1 -0
  74. package/dist/file-picker-entry.d.ts +2 -0
  75. package/dist/file-picker-entry.js +1 -0
  76. package/dist/form.d.ts +81 -0
  77. package/dist/form.js +1 -0
  78. package/dist/gauge.d.ts +87 -0
  79. package/dist/gauge.js +1 -0
  80. package/dist/gradient.d.ts +42 -0
  81. package/dist/gradient.js +1 -0
  82. package/dist/grid.d.ts +40 -0
  83. package/dist/grid.js +1 -0
  84. package/dist/heading.d.ts +33 -0
  85. package/dist/heading.js +1 -0
  86. package/dist/heatmap.d.ts +62 -0
  87. package/dist/heatmap.js +1 -0
  88. package/dist/help.d.ts +52 -0
  89. package/dist/help.js +1 -0
  90. package/dist/histogram.d.ts +62 -0
  91. package/dist/histogram.js +1 -0
  92. package/dist/image.d.ts +26 -0
  93. package/dist/image.js +1 -0
  94. package/dist/index.d.ts +125 -0
  95. package/dist/index.js +1 -0
  96. package/dist/json.d.ts +58 -0
  97. package/dist/json.js +1 -0
  98. package/dist/kbd.d.ts +31 -0
  99. package/dist/kbd.js +1 -0
  100. package/dist/line-chart.d.ts +109 -0
  101. package/dist/line-chart.js +1 -0
  102. package/dist/link.d.ts +38 -0
  103. package/dist/link.js +1 -0
  104. package/dist/loading-indicator.d.ts +29 -0
  105. package/dist/loading-indicator.js +1 -0
  106. package/dist/log.d.ts +38 -0
  107. package/dist/log.js +1 -0
  108. package/dist/markdown.d.ts +40 -0
  109. package/dist/markdown.js +1 -0
  110. package/dist/masked-input.d.ts +50 -0
  111. package/dist/masked-input.js +1 -0
  112. package/dist/menu.d.ts +78 -0
  113. package/dist/menu.js +1 -0
  114. package/dist/message-bubble.d.ts +43 -0
  115. package/dist/message-bubble.js +1 -0
  116. package/dist/model-badge.d.ts +39 -0
  117. package/dist/model-badge.js +1 -0
  118. package/dist/model-selector.d.ts +52 -0
  119. package/dist/model-selector.js +1 -0
  120. package/dist/multi-progress.d.ts +55 -0
  121. package/dist/multi-progress.js +1 -0
  122. package/dist/multi-select.d.ts +65 -0
  123. package/dist/multi-select.js +1 -0
  124. package/dist/notification-badge.d.ts +47 -0
  125. package/dist/notification-badge.js +1 -0
  126. package/dist/number-input.d.ts +85 -0
  127. package/dist/number-input.js +1 -0
  128. package/dist/operation-tree.d.ts +49 -0
  129. package/dist/operation-tree.js +1 -0
  130. package/dist/option-list.d.ts +47 -0
  131. package/dist/option-list.js +1 -0
  132. package/dist/ordered-list.d.ts +24 -0
  133. package/dist/ordered-list.js +1 -0
  134. package/dist/packem_shared/ControlledScrollView-BzlbIUph.js +1 -0
  135. package/dist/packem_shared/FilePicker-V5H2fb-v.js +1 -0
  136. package/dist/packem_shared/ScrollBar-C9i3z3Ft.js +1 -0
  137. package/dist/packem_shared/ScrollBarBox-DZEC8YnV.js +1 -0
  138. package/dist/packem_shared/ScrollList-Djtb2vVy.js +1 -0
  139. package/dist/packem_shared/ScrollView-ESxpl6Hy.js +1 -0
  140. package/dist/packem_shared/TreeNodeMap-cJlYqckB.js +1 -0
  141. package/dist/packem_shared/TreeView-q0l446NV.js +1 -0
  142. package/dist/packem_shared/controlled-scroll-view-DerVKKv6.js +1 -0
  143. package/dist/packem_shared/controlled-scroll-view.d-vF4wevEJ.d.ts +54 -0
  144. package/dist/packem_shared/file-picker.d-Cie6aAMD.d.ts +104 -0
  145. package/dist/packem_shared/line-chart-BbhMhcZg.js +1 -0
  146. package/dist/packem_shared/scroll-bar-box.d-DSBbBsRc.d.ts +30 -0
  147. package/dist/packem_shared/scroll-bar.d-B4sD1hBR.d.ts +18 -0
  148. package/dist/packem_shared/scroll-list.d-BP0UZSY4.d.ts +12 -0
  149. package/dist/packem_shared/scroll-view.d-BqKpI-F-.d.ts +119 -0
  150. package/dist/packem_shared/treeViewTheme-Dq3ekRAC.js +1 -0
  151. package/dist/packem_shared/use-tree-view.d-dXTKoDPM.d.ts +350 -0
  152. package/dist/packem_shared/useTreeView-D0JEqT6W.js +1 -0
  153. package/dist/packem_shared/useTreeViewState-BiTJakkh.js +1 -0
  154. package/dist/packem_shared/variant-config-BJqpNo7H.js +1 -0
  155. package/dist/packem_shared/variant-config.d-D5NnrrQK.d.ts +2 -0
  156. package/dist/paginator.d.ts +69 -0
  157. package/dist/paginator.js +1 -0
  158. package/dist/paragraph.d.ts +38 -0
  159. package/dist/paragraph.js +1 -0
  160. package/dist/path-input.d.ts +67 -0
  161. package/dist/path-input.js +1 -0
  162. package/dist/pie-chart.d.ts +50 -0
  163. package/dist/pie-chart.js +1 -0
  164. package/dist/placeholder.d.ts +55 -0
  165. package/dist/placeholder.js +1 -0
  166. package/dist/popover.d.ts +46 -0
  167. package/dist/popover.js +1 -0
  168. package/dist/progress-bar.d.ts +51 -0
  169. package/dist/progress-bar.js +1 -0
  170. package/dist/progress-circle.d.ts +59 -0
  171. package/dist/progress-circle.js +1 -0
  172. package/dist/qr-code.d.ts +45 -0
  173. package/dist/qr-code.js +1 -0
  174. package/dist/r/accordion.json +24 -0
  175. package/dist/r/alert.json +24 -0
  176. package/dist/r/animate-presence.json +22 -0
  177. package/dist/r/approval-prompt.json +24 -0
  178. package/dist/r/area-chart.json +27 -0
  179. package/dist/r/badge.json +24 -0
  180. package/dist/r/bar-chart.json +24 -0
  181. package/dist/r/big-text.json +23 -0
  182. package/dist/r/blink-dot.json +24 -0
  183. package/dist/r/breadcrumb.json +24 -0
  184. package/dist/r/button.json +24 -0
  185. package/dist/r/calendar.json +24 -0
  186. package/dist/r/card.json +26 -0
  187. package/dist/r/chart-utils.json +24 -0
  188. package/dist/r/checkbox-group.json +24 -0
  189. package/dist/r/checkbox.json +24 -0
  190. package/dist/r/code.json +23 -0
  191. package/dist/r/collapsible.json +24 -0
  192. package/dist/r/color-picker.json +24 -0
  193. package/dist/r/command-block.json +26 -0
  194. package/dist/r/command-palette.json +24 -0
  195. package/dist/r/confirm-dialog.json +24 -0
  196. package/dist/r/confirm-input.json +22 -0
  197. package/dist/r/console-overlay.json +22 -0
  198. package/dist/r/content-switcher.json +24 -0
  199. package/dist/r/controlled-scroll-view.json +24 -0
  200. package/dist/r/data-grid.json +24 -0
  201. package/dist/r/date-picker.json +26 -0
  202. package/dist/r/definition-list.json +24 -0
  203. package/dist/r/dialog.json +24 -0
  204. package/dist/r/diff-view.json +24 -0
  205. package/dist/r/digits.json +24 -0
  206. package/dist/r/directory-tree.json +24 -0
  207. package/dist/r/divider.json +24 -0
  208. package/dist/r/drawer.json +24 -0
  209. package/dist/r/file-change.json +24 -0
  210. package/dist/r/file-picker.json +44 -0
  211. package/dist/r/form.json +24 -0
  212. package/dist/r/gauge.json +24 -0
  213. package/dist/r/gradient.json +24 -0
  214. package/dist/r/grid.json +22 -0
  215. package/dist/r/heading.json +24 -0
  216. package/dist/r/heatmap.json +24 -0
  217. package/dist/r/help.json +22 -0
  218. package/dist/r/histogram.json +26 -0
  219. package/dist/r/image.json +24 -0
  220. package/dist/r/json.json +24 -0
  221. package/dist/r/kbd.json +24 -0
  222. package/dist/r/line-chart.json +26 -0
  223. package/dist/r/link.json +23 -0
  224. package/dist/r/loading-indicator.json +27 -0
  225. package/dist/r/log.json +24 -0
  226. package/dist/r/markdown.json +29 -0
  227. package/dist/r/masked-input.json +22 -0
  228. package/dist/r/menu.json +24 -0
  229. package/dist/r/message-bubble.json +24 -0
  230. package/dist/r/model-badge.json +24 -0
  231. package/dist/r/model-selector.json +24 -0
  232. package/dist/r/multi-progress.json +24 -0
  233. package/dist/r/multi-select.json +24 -0
  234. package/dist/r/notification-badge.json +24 -0
  235. package/dist/r/number-input.json +24 -0
  236. package/dist/r/operation-tree.json +26 -0
  237. package/dist/r/option-list.json +24 -0
  238. package/dist/r/ordered-list.json +22 -0
  239. package/dist/r/paginator.json +22 -0
  240. package/dist/r/paragraph.json +24 -0
  241. package/dist/r/path-input.json +26 -0
  242. package/dist/r/pie-chart.json +24 -0
  243. package/dist/r/placeholder.json +24 -0
  244. package/dist/r/popover.json +24 -0
  245. package/dist/r/progress-bar.json +22 -0
  246. package/dist/r/progress-circle.json +24 -0
  247. package/dist/r/qr-code.json +24 -0
  248. package/dist/r/radio-group.json +24 -0
  249. package/dist/r/registry.json +1051 -0
  250. package/dist/r/scatter-plot.json +27 -0
  251. package/dist/r/scroll-bar-box.json +24 -0
  252. package/dist/r/scroll-bar.json +24 -0
  253. package/dist/r/scroll-list.json +24 -0
  254. package/dist/r/scroll-view.json +24 -0
  255. package/dist/r/scroll.json +58 -0
  256. package/dist/r/search-input.json +26 -0
  257. package/dist/r/select-input-indicator.json +24 -0
  258. package/dist/r/select-input-item.json +24 -0
  259. package/dist/r/select-input.json +28 -0
  260. package/dist/r/shimmer-text.json +24 -0
  261. package/dist/r/sidebar.json +24 -0
  262. package/dist/r/slider.json +24 -0
  263. package/dist/r/sparkline.json +24 -0
  264. package/dist/r/spinner.json +23 -0
  265. package/dist/r/status-line.json +24 -0
  266. package/dist/r/status-message.json +24 -0
  267. package/dist/r/stepper.json +24 -0
  268. package/dist/r/stopwatch.json +22 -0
  269. package/dist/r/streaming-text.json +24 -0
  270. package/dist/r/switch.json +24 -0
  271. package/dist/r/tab.json +22 -0
  272. package/dist/r/table.json +23 -0
  273. package/dist/r/tabs.json +26 -0
  274. package/dist/r/tag-input.json +24 -0
  275. package/dist/r/tag.json +24 -0
  276. package/dist/r/text-input.json +22 -0
  277. package/dist/r/textarea.json +22 -0
  278. package/dist/r/thinking-block.json +24 -0
  279. package/dist/r/time-picker.json +24 -0
  280. package/dist/r/timer.json +22 -0
  281. package/dist/r/toast.json +24 -0
  282. package/dist/r/token-usage.json +24 -0
  283. package/dist/r/tool-call.json +24 -0
  284. package/dist/r/tooltip.json +24 -0
  285. package/dist/r/transition.json +22 -0
  286. package/dist/r/tree-select.json +24 -0
  287. package/dist/r/tree-view.json +64 -0
  288. package/dist/r/unordered-list.json +22 -0
  289. package/dist/r/use-linked-scroll.json +24 -0
  290. package/dist/r/variant-config.json +22 -0
  291. package/dist/radio-group.d.ts +74 -0
  292. package/dist/radio-group.js +1 -0
  293. package/dist/scatter-plot.d.ts +35 -0
  294. package/dist/scatter-plot.js +1 -0
  295. package/dist/scroll-bar-box.d.ts +3 -0
  296. package/dist/scroll-bar-box.js +1 -0
  297. package/dist/scroll-bar.d.ts +3 -0
  298. package/dist/scroll-bar.js +1 -0
  299. package/dist/scroll-list.d.ts +4 -0
  300. package/dist/scroll-list.js +1 -0
  301. package/dist/scroll-view.d.ts +3 -0
  302. package/dist/scroll-view.js +1 -0
  303. package/dist/search-input.d.ts +55 -0
  304. package/dist/search-input.js +1 -0
  305. package/dist/select-input-indicator.d.ts +32 -0
  306. package/dist/select-input-indicator.js +1 -0
  307. package/dist/select-input-item.d.ts +41 -0
  308. package/dist/select-input-item.js +1 -0
  309. package/dist/select-input.d.ts +110 -0
  310. package/dist/select-input.js +1 -0
  311. package/dist/shimmer-text.d.ts +47 -0
  312. package/dist/shimmer-text.js +1 -0
  313. package/dist/sidebar.d.ts +47 -0
  314. package/dist/sidebar.js +1 -0
  315. package/dist/slider.d.ts +103 -0
  316. package/dist/slider.js +1 -0
  317. package/dist/sparkline.d.ts +42 -0
  318. package/dist/sparkline.js +1 -0
  319. package/dist/spinner.d.ts +17 -0
  320. package/dist/spinner.js +1 -0
  321. package/dist/status-line.d.ts +37 -0
  322. package/dist/status-line.js +1 -0
  323. package/dist/status-message.d.ts +21 -0
  324. package/dist/status-message.js +1 -0
  325. package/dist/stepper.d.ts +48 -0
  326. package/dist/stepper.js +1 -0
  327. package/dist/stopwatch.d.ts +49 -0
  328. package/dist/stopwatch.js +1 -0
  329. package/dist/streaming-text.d.ts +46 -0
  330. package/dist/streaming-text.js +1 -0
  331. package/dist/switch.d.ts +55 -0
  332. package/dist/switch.js +1 -0
  333. package/dist/tab.d.ts +19 -0
  334. package/dist/tab.js +1 -0
  335. package/dist/table.d.ts +119 -0
  336. package/dist/table.js +1 -0
  337. package/dist/tabs.d.ts +107 -0
  338. package/dist/tabs.js +1 -0
  339. package/dist/tag-input.d.ts +71 -0
  340. package/dist/tag-input.js +1 -0
  341. package/dist/tag.d.ts +35 -0
  342. package/dist/tag.js +1 -0
  343. package/dist/text-input.d.ts +48 -0
  344. package/dist/text-input.js +1 -0
  345. package/dist/textarea.d.ts +68 -0
  346. package/dist/textarea.js +2 -0
  347. package/dist/thinking-block.d.ts +57 -0
  348. package/dist/thinking-block.js +1 -0
  349. package/dist/time-picker.d.ts +67 -0
  350. package/dist/time-picker.js +1 -0
  351. package/dist/timer.d.ts +59 -0
  352. package/dist/timer.js +1 -0
  353. package/dist/toast.d.ts +49 -0
  354. package/dist/toast.js +1 -0
  355. package/dist/token-usage.d.ts +50 -0
  356. package/dist/token-usage.js +1 -0
  357. package/dist/tool-call.d.ts +33 -0
  358. package/dist/tool-call.js +1 -0
  359. package/dist/tooltip.d.ts +43 -0
  360. package/dist/tooltip.js +1 -0
  361. package/dist/transition.d.ts +74 -0
  362. package/dist/transition.js +1 -0
  363. package/dist/tree-select.d.ts +65 -0
  364. package/dist/tree-select.js +1 -0
  365. package/dist/tree-view-entry.d.ts +4 -0
  366. package/dist/tree-view-entry.js +1 -0
  367. package/dist/unordered-list.d.ts +30 -0
  368. package/dist/unordered-list.js +1 -0
  369. package/dist/use-linked-scroll.d.ts +42 -0
  370. package/dist/use-linked-scroll.js +1 -0
  371. package/package.json +553 -7
@@ -0,0 +1 @@
1
+ import{jsx as t}from"react/jsx-runtime";import n from"@visulima/tui/components/box";import x from"@visulima/tui/components/text";import g from"@visulima/tui/hooks/use-animation";import v from"@visulima/tui/hooks/use-window-size";const y=[1,.85,.6];function E({animated:o=!0,character:l="█",color:m="gray",interval:c=500,rows:h=3,width:e,widths:r=y}){const{frame:d}=g({interval:c,isActive:o}),{columns:f}=v(),s=o&&d%2===0,u=e??Math.max(1,f);return t(n,{flexDirection:"column",width:e,children:Array.from({length:h},(M,a)=>{const p=r.length>0?r[a%r.length]??1:1,i=Math.max(1,Math.round(u*p)),w=l.repeat(i);return t(n,{width:i,children:t(x,{color:m,dimColor:s,wrap:"truncate-end",children:w})},a)})})}export{E as Placeholder,E as default};
@@ -0,0 +1,46 @@
1
+ import { AnsiColors } from '@visulima/colorize';
2
+ import { ReactNode, ReactElement } from 'react';
3
+ import { LiteralUnion } from 'type-fest';
4
+ type Props = {
5
+ /**
6
+ * Accent color for the content border.
7
+ * @default "blue"
8
+ */
9
+ readonly accentColor?: LiteralUnion<AnsiColors, string>;
10
+ /**
11
+ * The trigger element the popover is anchored to.
12
+ */
13
+ readonly anchor: ReactNode;
14
+ /**
15
+ * The floating content, shown in a bordered box while open.
16
+ */
17
+ readonly children: ReactNode;
18
+ /**
19
+ * Whether the popover content is shown.
20
+ */
21
+ readonly isOpen: boolean;
22
+ /**
23
+ * Fires when Escape is pressed while open.
24
+ */
25
+ readonly onClose?: () => void;
26
+ /**
27
+ * Whether the content sits above or below the anchor.
28
+ * @default "bottom"
29
+ */
30
+ readonly placement?: "bottom" | "top";
31
+ };
32
+ /**
33
+ * Floating content anchored to a trigger element. The anchor always renders;
34
+ * the bordered content appears above or below it while `isOpen`, and Escape
35
+ * fires `onClose`. Terminals have no true overlay layer, so the content takes
36
+ * layout space rather than floating over other content.
37
+ */
38
+ declare function Popover({
39
+ accentColor,
40
+ anchor,
41
+ children,
42
+ isOpen,
43
+ onClose,
44
+ placement
45
+ }: Props): ReactElement;
46
+ export { Popover, type Props as PopoverProps, Props, Popover as default };
@@ -0,0 +1 @@
1
+ import{jsx as a,jsxs as m}from"react/jsx-runtime";import i from"@visulima/tui/components/box";import p from"@visulima/tui/hooks/use-input";import{useCallback as f}from"react";function C({accentColor:n="blue",anchor:l,children:c,isOpen:o,onClose:r,placement:e="bottom"}){const d=f((u,s)=>{s.escape&&r?.()},[r]);p(d,{isActive:o});const t=o?a(i,{alignSelf:"flex-start",borderColor:n,borderStyle:"round",paddingX:1,children:c}):void 0;return m(i,{flexDirection:"column",children:[e==="top"?t:void 0,l,e==="bottom"?t:void 0]})}export{C as Popover,C as default};
@@ -0,0 +1,51 @@
1
+ import { Props as Props$1 } from '@visulima/tui/components/text';
2
+ import { ReactElement } from 'react';
3
+ type Props = {
4
+ /** Fill glyph repeated across the completed portion of the progress bar. Defaults to "█". */
5
+ readonly character?: string;
6
+ /**
7
+ * Override the terminal width used for calculation.
8
+ * When 0, uses the actual terminal width.
9
+ * @default 0
10
+ */
11
+ readonly columns?: number;
12
+ /**
13
+ * Number of columns reserved on the left side (e.g. for labels).
14
+ * @default 0
15
+ */
16
+ readonly left?: number;
17
+ /**
18
+ * Completion percentage between 0 and 1.
19
+ * @default 1
20
+ */
21
+ readonly percent?: number;
22
+ /**
23
+ * Number of columns reserved on the right side (e.g. for percentage text).
24
+ * @default 0
25
+ */
26
+ readonly right?: number;
27
+ /**
28
+ * Whether to pad the remaining space with whitespace.
29
+ * Useful when rendering the bar over a background color.
30
+ * @default false
31
+ */
32
+ readonly rightPad?: boolean;
33
+ } & Omit<Props$1, "children">;
34
+ /**
35
+ * A terminal progress bar that fills proportionally to a percentage value.
36
+ *
37
+ * ```tsx
38
+ * &lt;ProgressBar percent={0.5} />
39
+ * &lt;ProgressBar percent={0.75} color="green" left={10} right={5} />
40
+ * ```
41
+ */
42
+ declare function ProgressBar({
43
+ character,
44
+ columns: columnsProp,
45
+ left,
46
+ percent,
47
+ right,
48
+ rightPad,
49
+ ...textProps
50
+ }: Props): ReactElement;
51
+ export { ProgressBar, type Props as ProgressBarProps, Props, ProgressBar as default };
@@ -0,0 +1 @@
1
+ import{jsx as f}from"react/jsx-runtime";import l from"@visulima/tui/components/text";import p from"@visulima/tui/hooks/use-window-size";function d({character:a="█",columns:e=0,left:o=0,percent:m=1,right:i=0,rightPad:n=!1,...c}){const{columns:h}=p(),r=Math.max(0,(e||h||80)-i-o),t=Math.min(Math.floor(r*Math.max(0,Math.min(1,m))),r),s=a.repeat(t)+(n?" ".repeat(r-t):"");return f(l,{...c,children:s})}export{d as ProgressBar,d as default};
@@ -0,0 +1,59 @@
1
+ import { AnsiColors } from '@visulima/colorize';
2
+ import { ReactElement } from 'react';
3
+ import { LiteralUnion } from 'type-fest';
4
+ type Props = {
5
+ /**
6
+ * Color of the filled portion of the ring.
7
+ * @default "cyan"
8
+ */
9
+ readonly color?: LiteralUnion<AnsiColors, string>;
10
+ /**
11
+ * Format the centered readout. Defaults to a rounded percentage.
12
+ */
13
+ readonly formatValue?: (ratio: number) => string;
14
+ /**
15
+ * Upper bound of the range.
16
+ * @default 100
17
+ */
18
+ readonly max?: number;
19
+ /**
20
+ * Lower bound of the range.
21
+ * @default 0
22
+ */
23
+ readonly min?: number;
24
+ /**
25
+ * Show the readout in the middle of the ring.
26
+ * @default true
27
+ */
28
+ readonly showValue?: boolean;
29
+ /**
30
+ * Diameter of the ring in character rows.
31
+ * @default 8
32
+ */
33
+ readonly size?: number;
34
+ /**
35
+ * Color of the unfilled track behind the ring.
36
+ * @default "gray"
37
+ */
38
+ readonly trackColor?: LiteralUnion<AnsiColors, string>;
39
+ /**
40
+ * Current value, clamped into `[min, max]`.
41
+ */
42
+ readonly value: number;
43
+ };
44
+ /**
45
+ * A circular progress ring rendered at 2x4 braille sub-cell resolution. The
46
+ * filled arc grows clockwise from the top in proportion to `value`, with an
47
+ * optional centered readout.
48
+ */
49
+ declare function ProgressCircle({
50
+ color,
51
+ formatValue,
52
+ max,
53
+ min,
54
+ showValue,
55
+ size,
56
+ trackColor,
57
+ value
58
+ }: Props): ReactElement;
59
+ export { ProgressCircle, type Props as ProgressCircleProps, Props, ProgressCircle as default };
@@ -0,0 +1 @@
1
+ import{jsxs as V,jsx as e}from"react/jsx-runtime";import{createBrailleGrid as p}from"@visulima/tui/canvas";import w from"@visulima/tui/components/box";import b from"@visulima/tui/components/canvas";import z from"@visulima/tui/components/text";const A=o=>`${Math.round(o*100)}%`;function E({color:o="cyan",formatValue:P=A,max:l=100,min:r=0,showValue:I=!0,size:m=8,trackColor:c="gray",value:M}){const v=l-r||1,s=Math.max(0,Math.min(1,(M-r)/v)),h=Math.max(3,m),y=Math.max(3,Math.round(h*4/2/2));return V(w,{alignItems:"center",flexDirection:"column",children:[e(b,{draw:t=>{t.clear();const C=t.width*2,j=t.height*4,u=C/2,d=j/2,i=Math.max(1,Math.min(u,d)-1),f=p(t.width,t.height),x=p(t.width,t.height),n=Math.max(64,Math.round(Math.PI*2*i)),k=Math.round(s*n);for(let a=0;a<n;a+=1){const g=a/n*Math.PI*2-Math.PI/2,B=Math.round(u+i*Math.cos(g)),G=Math.round(d+i*Math.sin(g));(a<k?x:f).plotPoint(B,G)}f.flush(t,{color:c}),x.flush(t,{color:o})},height:h,version:[M,r,l,m,o,c],width:y}),I?e(w,{marginTop:-Math.ceil(h/2)-1,children:e(z,{bold:!0,color:o,children:P(s)})}):void 0]})}export{E as ProgressCircle,E as default};
@@ -0,0 +1,45 @@
1
+ import { AnsiColors } from '@visulima/colorize';
2
+ import { ReactElement } from 'react';
3
+ import { LiteralUnion } from 'type-fest';
4
+ type Color = LiteralUnion<AnsiColors, string>;
5
+ type Props = {
6
+ /**
7
+ * Color of the "dark" modules.
8
+ * @default "black"
9
+ */
10
+ readonly darkColor?: Color;
11
+ /**
12
+ * Color of the "light" modules and quiet zone.
13
+ * @default "white"
14
+ */
15
+ readonly lightColor?: Color;
16
+ /**
17
+ * A pre-encoded square grid of modules (`true` = dark). Provide this to
18
+ * render without the optional `qrcode` peer; it takes precedence over
19
+ * `value`.
20
+ */
21
+ readonly matrix?: ReadonlyArray<ReadonlyArray<boolean>>;
22
+ /**
23
+ * Width of the light border around the code, in modules.
24
+ * @default 2
25
+ */
26
+ readonly quietZone?: number;
27
+ /**
28
+ * A string to encode. Requires the optional `qrcode` peer dependency; when
29
+ * absent, provide `matrix` instead.
30
+ */
31
+ readonly value?: string;
32
+ };
33
+ /**
34
+ * Render a QR code from a module matrix using the upper-half-block technique,
35
+ * so two module rows share one character row. Pass `matrix` for a self-contained
36
+ * render, or `value` to encode a string via the optional `qrcode` peer.
37
+ */
38
+ declare function QrCode({
39
+ darkColor,
40
+ lightColor,
41
+ matrix,
42
+ quietZone,
43
+ value
44
+ }: Props): ReactElement;
45
+ export { Props, QrCode, type Props as QrCodeProps, QrCode as default };
@@ -0,0 +1 @@
1
+ import{jsx as h}from"react/jsx-runtime";import g from"@visulima/tui/components/box";import C from"@visulima/tui/components/text";import{useState as k,useEffect as y,useMemo as z}from"react";const E="▀",b=(s,e)=>{if(e<=0)return s.map(o=>[...o]);let c=0;for(const o of s)c=Math.max(c,o.length);const m=c+e*2,t=()=>{const o=[];for(let r=0;r<m;r+=1)o.push(!1);return o},n=[];for(let o=0;o<e;o+=1)n.push(t());for(const o of s){const r=t();o.forEach((l,d)=>{r[d+e]=l}),n.push(r)}for(let o=0;o<e;o+=1)n.push(t());return n};function q({darkColor:s="black",lightColor:e="white",matrix:c,quietZone:m=2,value:t}){const[n,o]=k(void 0);y(()=>{if(c!==void 0||t===void 0)return;let i=!1;return(async()=>{try{const v=await import("qrcode"),{modules:u}=v.create(t),a=[];for(let f=0;f<u.size;f+=1){const p=[];for(let x=0;x<u.size;x+=1)p.push(!!u.data[f*u.size+x]);a.push(p)}i||o(a)}catch{}})().catch(()=>{}),()=>{i=!0}},[c,t]);const r=c??n,l=z(()=>{if(r!==void 0)return b(r,m)},[m,r]);if(l===void 0)return h(g,{children:h(C,{dimColor:!0,children:t===void 0?"":"Encoding…"})});const d=[];for(let i=0;i<l.length;i+=2){const v=l[i]??[],u=l[i+1]??[],a=[];for(const[f,p]of v.entries())a.push(h(C,{backgroundColor:u[f]?s:e,color:p?s:e,children:E},f));d.push(h(g,{children:a},i))}return h(g,{flexDirection:"column",children:d})}export{q as QrCode,q as default};
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "accordion",
4
+ "type": "registry:ui",
5
+ "title": "Accordion",
6
+ "description": "Accordion component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type AccordionItem = {\n /**\n * Content rendered when the item is expanded.\n */\n readonly content: ReactNode;\n\n /**\n * Unique identifier.\n */\n readonly id: string;\n\n /**\n * Header label.\n */\n readonly title: ReactNode;\n};\n\nexport type Props = {\n /**\n * Accent color for the focused header.\n * @default \"blue\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * When true, multiple panels can be open at once.\n * @default false\n */\n readonly allowMultiple?: boolean;\n\n /**\n * Auto-focus the first item on mount.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Initially open item ids.\n */\n readonly defaultExpanded?: ReadonlyArray<string>;\n\n /**\n * Disable interaction.\n */\n readonly isDisabled?: boolean;\n\n /**\n * List of panels.\n */\n readonly items: ReadonlyArray<AccordionItem>;\n};\n\n/**\n * Keyboard-navigable accordion. Use arrow keys to move between panels and\n * Enter / Space to toggle.\n * @returns A `ReactElement` rendering the accordion column.\n */\nexport default function Accordion({\n accentColor = \"blue\",\n allowMultiple = false,\n autoFocus = false,\n defaultExpanded,\n isDisabled = false,\n items,\n}: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n const [expanded, setExpanded] = useState<ReadonlyArray<string>>(() => {\n if (defaultExpanded === undefined || defaultExpanded.length === 0) {\n return [];\n }\n\n // When `allowMultiple` is false the accordion only ever shows one open\n // panel — clamp the seed so the initial render matches that contract.\n if (!allowMultiple) {\n return defaultExpanded.slice(0, 1);\n }\n\n return defaultExpanded;\n });\n const [focusedIndex, setFocusedIndex] = useState(0);\n const focusedIndexRef = useRef(focusedIndex);\n\n focusedIndexRef.current = focusedIndex;\n\n const toggle = useCallback(\n (id: string) => {\n setExpanded((previous) => {\n const isOpen = previous.includes(id);\n\n if (allowMultiple) {\n return isOpen ? previous.filter((x) => x !== id) : [...previous, id];\n }\n\n return isOpen ? [] : [id];\n });\n },\n [allowMultiple],\n );\n\n useInput(\n useCallback(\n (input, key) => {\n if (!isFocused) {\n return;\n }\n\n const total = items.length;\n\n if ((key.downArrow || input === \"j\") && focusedIndexRef.current < total - 1) {\n setFocusedIndex(focusedIndexRef.current + 1);\n } else if ((key.upArrow || input === \"k\") && focusedIndexRef.current > 0) {\n setFocusedIndex(focusedIndexRef.current - 1);\n } else if (key.return || input === \" \") {\n const target = items[focusedIndexRef.current];\n\n if (target) {\n toggle(target.id);\n }\n }\n },\n [isFocused, items, toggle],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n return (\n <Box flexDirection=\"column\">\n {items.map((item, index) => {\n const isOpen = expanded.includes(item.id);\n const isItemFocused = isFocused && index === focusedIndex;\n const color = isItemFocused ? accentColor : undefined;\n\n return (\n <Box flexDirection=\"column\" key={item.id}>\n <Box>\n <Text color={color} dimColor={isDisabled}>\n {isItemFocused ? \"▸ \" : \" \"}\n {isOpen ? \"▼ \" : \"▶ \"}\n </Text>\n <Text bold={isItemFocused} color={color} dimColor={isDisabled}>\n {item.title}\n </Text>\n </Box>\n {isOpen\n ? (\n <Box flexDirection=\"column\" marginBottom={1} marginLeft={4}>\n {item.content}\n </Box>\n )\n : undefined}\n </Box>\n );\n })}\n </Box>\n );\n}\n\nexport { Accordion };\nexport type { Props as AccordionProps };\n",
19
+ "path": "registry/ui/accordion.tsx",
20
+ "target": "components/ui/accordion.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "alert",
4
+ "type": "registry:ui",
5
+ "title": "Alert",
6
+ "description": "Alert component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/tui"
12
+ ],
13
+ "registryDependencies": [
14
+ "https://visulima.com/r/variant-config.json"
15
+ ],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\n\n/**\n * Alert component for Ink.\n *\n * Inspired by ink-ui Alert by Vadim Demedes.\n * @see https://github.com/vadimdemedes/ink-ui\n *\n * MIT License\n * Copyright (c) Vadym Demedes (github.com/vadimdemedes)\n */\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement, ReactNode } from \"react\";\n\nimport type { Variant } from \"@/lib/variant-config\";\nimport VARIANT_CONFIG from \"@/lib/variant-config\";\n\nexport type AlertVariant = Variant;\n\nexport type Props = {\n /**\n * Alert message content.\n */\n readonly children: ReactNode;\n\n /**\n * Optional heading displayed above the message.\n */\n readonly title?: string;\n\n /**\n * Determines the border color and icon.\n */\n readonly variant: AlertVariant;\n};\n\n/**\n * Renders a bordered alert box with a variant-specific icon, optional title, and message.\n */\nexport default function Alert({ children, title, variant }: Props): ReactElement {\n const { color, icon } = VARIANT_CONFIG[variant];\n\n return (\n <Box borderColor={color} borderStyle=\"round\" flexGrow={1} gap={1} paddingX={1}>\n <Box flexShrink={0}>\n <Text color={color}>{icon}</Text>\n </Box>\n <Box flexDirection=\"column\" flexGrow={1} flexShrink={1} gap={1} minWidth={0}>\n {title ? <Text bold>{title}</Text> : undefined}\n <Text>{children}</Text>\n </Box>\n </Box>\n );\n}\n\nexport { Alert };\nexport type { Props as AlertProps };\n",
19
+ "path": "registry/ui/alert.tsx",
20
+ "target": "components/ui/alert.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "animate-presence",
4
+ "type": "registry:ui",
5
+ "title": "Animate Presence",
6
+ "description": "Animate Presence component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/tui"
12
+ ],
13
+ "registryDependencies": [],
14
+ "files": [
15
+ {
16
+ "content": "/* eslint-disable react/function-component-definition */\nimport Box from \"@visulima/tui/components/box\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Children, cloneElement, isValidElement, useCallback, useEffect, useState } from \"react\";\n\ntype AnimatableProps = {\n readonly onExit?: () => void;\n readonly show?: boolean;\n};\n\nexport type Props = {\n /**\n * Keyed children. Any child that accepts a `show: boolean` prop and calls\n * `onExit` when its exit transition finishes (such as `&lt;Transition />`) is\n * orchestrated automatically. Other children render as-is.\n */\n readonly children: ReactNode;\n};\n\ntype Slot = {\n readonly element: ReactElement<AnimatableProps>;\n readonly key: string;\n readonly show: boolean;\n};\n\n/**\n * Normalize a ReactElement's `key` to a string (or `undefined` when the\n * caller forgot to set one). AnimatePresence ignores children without a key\n * because tracking identity across renders requires it.\n */\nconst getKey = (element: ReactElement): string | undefined => {\n const { key } = element;\n\n if (key === null) {\n return undefined;\n }\n\n return key;\n};\n\n/**\n * Type-guard used to narrow a child to something AnimatePresence can drive.\n * Matches two cases.\n *\n * 1. The element's component type carries a static `isAnimatable = true`\n * marker (this is how built-in `&lt;Transition />` is recognized — consumers\n * can opt in their own wrappers the same way).\n * 2. The element's props object already declares a `show` key, letting\n * inline components participate without a marker.\n */\nconst isAnimatable = (element: ReactElement): element is ReactElement<AnimatableProps> => {\n const type = element.type as { isAnimatable?: boolean } | undefined;\n\n if (type?.isAnimatable === true) {\n return true;\n }\n\n return typeof element.props === \"object\" && element.props !== null && \"show\" in element.props;\n};\n\n/* eslint-disable react-x/no-children-for-each, react-x/no-clone-element -- AnimatePresence intentionally walks and clones React children to track keyed enter/exit */\n\n/**\n * Orchestrates enter / exit animations for keyed children. Wrap a dynamic\n * list of `&lt;Transition />` (or any component accepting `show` + `onExit`) to\n * keep outgoing children mounted until their exit animation finishes.\n */\nexport default function AnimatePresence({ children }: Props): ReactElement {\n const [slots, setSlots] = useState<ReadonlyArray<Slot>>(() => {\n const collected: Slot[] = [];\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) {\n return;\n }\n\n const key = getKey(child);\n\n if (key === undefined) {\n return;\n }\n\n collected.push({\n element: isAnimatable(child) ? child : (child as ReactElement<AnimatableProps>),\n key,\n show: true,\n });\n });\n\n return collected;\n });\n\n // Reconcile children on every render — additions marked show=true,\n // removals flipped to show=false so they can animate out. Persisting\n // keys simply get their element updated.\n useEffect(() => {\n const incoming = new Map<string, ReactElement<AnimatableProps>>();\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) {\n return;\n }\n\n const key = getKey(child);\n\n if (key === undefined) {\n return;\n }\n\n incoming.set(key, isAnimatable(child) ? child : (child as ReactElement<AnimatableProps>));\n });\n\n // eslint-disable-next-line react-x/set-state-in-effect -- AnimatePresence reconciles slots from children in an effect; \"removed\" slots persist with show=false until exit animation completes, so this is not pure derivation\n setSlots((previous) => {\n const next: Slot[] = [];\n const seen = new Set<string>();\n let changed = false;\n\n for (const slot of previous) {\n if (incoming.has(slot.key)) {\n const nextElement = incoming.get(slot.key)!;\n\n if (!slot.show || slot.element !== nextElement) {\n changed = true;\n }\n\n next.push({\n element: nextElement,\n key: slot.key,\n show: true,\n });\n seen.add(slot.key);\n } else {\n if (slot.show) {\n changed = true;\n }\n\n // Removed in the incoming render; keep rendering with show=false.\n next.push({ ...slot, show: false });\n }\n }\n\n for (const [key, element] of incoming) {\n if (!seen.has(key)) {\n changed = true;\n next.push({ element, key, show: true });\n }\n }\n\n // React bails out of the update if we return the same reference,\n // preventing an infinite effect/setState loop when `children` gets\n // a fresh JSX identity each parent render but is functionally\n // unchanged.\n return changed ? next : previous;\n });\n }, [children]);\n\n const removeSlot = useCallback((key: string) => {\n setSlots((previous) => previous.filter((slot) => slot.key !== key));\n }, []);\n\n return (\n <Box flexDirection=\"column\">\n {slots.map((slot) => {\n if (!isAnimatable(slot.element)) {\n // Still clone so React sees the exact key we stored in\n // the slot (our reconciler and React's key-tracking now\n // agree even if the original element carried a different\n // one).\n return cloneElement(slot.element, { key: slot.key });\n }\n\n const originalOnExit = slot.element.props.onExit;\n\n return cloneElement(slot.element, {\n key: slot.key,\n onExit: () => {\n originalOnExit?.();\n removeSlot(slot.key);\n },\n show: slot.show,\n });\n })}\n </Box>\n );\n}\n\nexport { AnimatePresence };\nexport type { Props as AnimatePresenceProps };\n",
17
+ "path": "registry/ui/animate-presence.tsx",
18
+ "target": "components/ui/animate-presence.tsx",
19
+ "type": "registry:ui"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "approval-prompt",
4
+ "type": "registry:ui",
5
+ "title": "Approval Prompt",
6
+ "description": "Approval Prompt component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type ApprovalRisk = \"high\" | \"low\" | \"medium\";\n\nexport type ApprovalDecision = \"allow-always\" | \"allow-once\" | \"deny\";\n\nexport type Props = {\n /**\n * Accent color for the highlighted option letters. Overrides the risk color.\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Auto-focus the prompt on mount. Set to `false` if a parent already\n * owns focus and routes input to this component.\n * @default true\n */\n readonly autoFocus?: boolean;\n\n /**\n * Optional body rendered between the header and the prompt line (e.g. a\n * rendered diff or command preview).\n */\n readonly children?: ReactNode;\n\n /**\n * Accessible description of what approving will do.\n */\n readonly description?: string;\n\n /**\n * Disable input; use when a parent manages focus and input routing.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Resolution callback. Called once the user answers.\n */\n readonly onDecision: (decision: ApprovalDecision) => void;\n\n /**\n * Params shown as a compact key=value preview.\n */\n readonly params?: Readonly<Record<string, unknown>>;\n\n /**\n * Severity badge; controls the border color.\n * @default \"medium\"\n */\n readonly risk?: ApprovalRisk;\n\n /**\n * Name of the tool / action under review.\n */\n readonly tool: string;\n};\n\nconst RISK_COLOR: Record<ApprovalRisk, LiteralUnion<AnsiColors, string>> = {\n high: \"red\",\n low: \"green\",\n medium: \"yellow\",\n};\n\nconst RISK_LABEL: Record<ApprovalRisk, string> = {\n high: \"HIGH RISK\",\n low: \"LOW RISK\",\n medium: \"MEDIUM RISK\",\n};\n\nconst formatParameterValue = (value: unknown): string => {\n if (value === null) {\n return \"null\";\n }\n\n if (typeof value === \"string\") {\n return value;\n }\n\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- last-resort fallback when value isn't JSON-serializable\n return String(value);\n }\n};\n\n/**\n * Three-way approval prompt for tool calls. Resolves via `onDecision`:\n * `y` / Enter → allow-once, `a` → allow-always, `n` / Esc → deny.\n * @returns A bordered `ReactElement` containing the risk banner, params\n * preview, optional body, and the prompt line.\n */\nexport default function ApprovalPrompt({\n accentColor,\n autoFocus = true,\n children,\n description,\n isDisabled = false,\n onDecision,\n params,\n risk = \"medium\",\n tool,\n}: Props): ReactElement {\n const riskColor = RISK_COLOR[risk];\n const color = accentColor ?? riskColor;\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n\n useInput(\n useCallback(\n (input, key) => {\n const typed = input.toLowerCase();\n\n if (typed === \"y\" || key.return) {\n onDecision(\"allow-once\");\n\n return;\n }\n\n if (typed === \"a\") {\n onDecision(\"allow-always\");\n\n return;\n }\n\n if (typed === \"n\" || key.escape) {\n onDecision(\"deny\");\n }\n },\n [onDecision],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n const parameterEntries = params === undefined ? [] : Object.entries(params);\n\n return (\n <Box borderColor={riskColor} borderStyle=\"round\" flexDirection=\"column\" paddingX={1}>\n <Box>\n <Text backgroundColor={riskColor} color=\"black\">\n {\" \"}\n {RISK_LABEL[risk]}\n{\" \"}\n </Text>\n <Text>\n {\" \"}\n <Text bold>{tool}</Text>\n </Text>\n </Box>\n {description === undefined\n ? undefined\n : (\n <Box marginTop={1}>\n <Text>{description}</Text>\n </Box>\n )}\n {parameterEntries.length === 0\n ? undefined\n : (\n <Box flexDirection=\"column\" marginTop={1}>\n {parameterEntries.map(([name, value]) => (\n <Box key={name}>\n <Text color=\"cyan\">\n{name}\n=\n </Text>\n <Text wrap=\"truncate-end\">{formatParameterValue(value)}</Text>\n </Box>\n ))}\n </Box>\n )}\n {children === undefined ? undefined : <Box marginTop={1}>{children}</Box>}\n <Box marginTop={1}>\n <Text>\n Allow? [\n <Text bold color={color}>\n y\n </Text>\n es /\n{\" \"}\n <Text bold color={color}>\n a\n </Text>\n lways /\n{\" \"}\n <Text bold color={color}>\n n\n </Text>\n o ]\n </Text>\n </Box>\n </Box>\n );\n}\n\nexport { ApprovalPrompt };\nexport type { Props as ApprovalPromptProps };\n",
19
+ "path": "registry/ui/approval-prompt.tsx",
20
+ "target": "components/ui/approval-prompt.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "area-chart",
4
+ "type": "registry:ui",
5
+ "title": "Area Chart",
6
+ "description": "Area Chart component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [
16
+ "https://visulima.com/r/chart-utils.json",
17
+ "https://visulima.com/r/line-chart.json"
18
+ ],
19
+ "files": [
20
+ {
21
+ "content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport type { CanvasContext } from \"@visulima/tui/canvas\";\nimport { createBrailleGrid } from \"@visulima/tui/canvas\";\nimport Box from \"@visulima/tui/components/box\";\nimport Canvas from \"@visulima/tui/components/canvas\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement } from \"react\";\nimport { useMemo } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nimport { computeExtents, DEFAULT_CHART_PALETTE, pickSeriesColor, toPoints } from \"@/lib/chart-utils\";\nimport type { LineSeries } from \"@/components/ui/line-chart\";\nimport { projectPoint } from \"@/components/ui/line-chart\";\n\nexport type Props = {\n /**\n * Axis label color.\n * @default \"gray\"\n */\n readonly axisColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Density of the shaded fill below each line.\n * - `light` → `░`\n * - `medium` → `▒` (default)\n * - `heavy` → `▓`\n */\n readonly fillDensity?: \"heavy\" | \"light\" | \"medium\";\n\n /**\n * Chart body height in character rows.\n * @default 10\n */\n readonly height?: number;\n\n /**\n * Upper bound for the y-axis.\n */\n readonly maxY?: number;\n\n /**\n * Lower bound for the y-axis.\n */\n readonly minY?: number;\n\n /**\n * Fallback palette.\n */\n readonly palette?: ReadonlyArray<LiteralUnion<AnsiColors, string>>;\n\n /**\n * Series to plot.\n */\n readonly series: ReadonlyArray<LineSeries>;\n\n /**\n * Render the legend below the plot.\n * @default true\n */\n readonly showLegend?: boolean;\n\n /**\n * Chart body width in character columns.\n * @default 40\n */\n readonly width?: number;\n};\n\nconst DEFAULT_PALETTE = DEFAULT_CHART_PALETTE;\n\nconst FILL_GLYPH: Record<Required<Props>[\"fillDensity\"], string> = {\n heavy: \"▓\",\n light: \"░\",\n medium: \"▒\",\n};\n\n/**\n * Area chart: line chart with the area between the line and the baseline\n * filled with shaded blocks.\n */\nexport default function AreaChart({\n axisColor = \"gray\",\n fillDensity = \"medium\",\n height = 10,\n maxY,\n minY,\n palette = DEFAULT_PALETTE,\n series,\n showLegend = true,\n width = 40,\n}: Props): ReactElement {\n const prepared = useMemo(() => {\n const list = series.map((input, index) => {\n return {\n color: pickSeriesColor(input, index, palette),\n points: toPoints(input.data),\n series: input,\n };\n });\n\n const extents = computeExtents(list);\n\n return {\n list,\n xMax: extents.xMax === Number.NEGATIVE_INFINITY ? 1 : extents.xMax,\n xMin: extents.xMin === Number.POSITIVE_INFINITY ? 0 : extents.xMin,\n yMax: maxY ?? (extents.yMax === Number.NEGATIVE_INFINITY ? 1 : extents.yMax),\n yMin: minY ?? (extents.yMin === Number.POSITIVE_INFINITY ? 0 : extents.yMin),\n };\n }, [series, palette, minY, maxY]);\n\n const glyph = FILL_GLYPH[fillDensity];\n\n return (\n <Box flexDirection=\"column\">\n <Canvas\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop -- canvas re-renders on `version` change, not draw identity\n draw={(context: CanvasContext) => {\n context.clear();\n\n const pixelWidth = context.width * 2;\n const pixelHeight = context.height * 4;\n const config = {\n xMax: prepared.xMax,\n xMin: prepared.xMin,\n yMax: prepared.yMax,\n yMin: prepared.yMin,\n };\n\n // Fill layer — drawn below the line so the braille overlay stays crisp.\n for (const { color, points } of prepared.list) {\n if (points.length === 0) {\n continue;\n }\n\n // Sample the line once per character column and fill from\n // that row down to the baseline.\n for (let cellX = 0; cellX < context.width; cellX += 1) {\n // Find the data point that projects into this column.\n // Linear scan is fine for reasonable point counts;\n // larger datasets should pre-resample. One\n // projection per point — reuse px AND py.\n let sampleY: number | undefined;\n\n for (const point of points) {\n const { px, py } = projectPoint(point, config, pixelWidth, pixelHeight);\n const candidateCellX = Math.floor(px / 2);\n\n if (candidateCellX === cellX) {\n const candidateCellY = Math.floor(py / 4);\n\n if (sampleY === undefined || candidateCellY < sampleY) {\n sampleY = candidateCellY;\n }\n }\n }\n\n if (sampleY === undefined) {\n continue;\n }\n\n for (let cellY = sampleY + 1; cellY < context.height; cellY += 1) {\n context.setCell(cellX, cellY, glyph, { color, dim: true });\n }\n }\n }\n\n // Dashed baseline.\n for (let cellX = 0; cellX < context.width; cellX += 2) {\n context.setCell(cellX, context.height - 1, \"·\", { color: axisColor, dim: true });\n }\n\n // Line overlay using braille.\n for (const { color, points } of prepared.list) {\n if (points.length === 0) {\n continue;\n }\n\n const grid = createBrailleGrid(context.width, context.height);\n let previous = projectPoint(points[0]!, config, pixelWidth, pixelHeight);\n\n grid.plotPoint(previous.px, previous.py);\n\n for (let index = 1; index < points.length; index += 1) {\n const current = projectPoint(points[index]!, config, pixelWidth, pixelHeight);\n\n grid.plotLine(previous.px, previous.py, current.px, current.py);\n previous = current;\n }\n\n grid.flush(context, { bold: true, color });\n }\n }}\n height={height}\n // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop -- version array is the canvas redraw key\n version={[series, width, height, minY, maxY, fillDensity, axisColor, palette]}\n width={width}\n />\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showLegend ? (\n <Box gap={2} marginTop={1}>\n {prepared.list.map(({ color, series: input }, index) => (\n // Composite key: series index plus label so two\n // series sharing a label don't collide.\n // eslint-disable-next-line react-x/no-array-index-key -- series index is stable for the render\n <Box gap={1} key={`${index}:${input.label ?? \"\"}`}>\n <Text color={color}>{glyph}</Text>\n <Text>{input.label ?? `Series ${index + 1}`}</Text>\n </Box>\n ))}\n </Box>\n ) : undefined}\n </Box>\n );\n}\n\nexport { AreaChart };\nexport type { Props as AreaChartProps };\n",
22
+ "path": "registry/ui/area-chart.tsx",
23
+ "target": "components/ui/area-chart.tsx",
24
+ "type": "registry:ui"
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "badge",
4
+ "type": "registry:ui",
5
+ "title": "Badge",
6
+ "description": "Badge component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\n\n/**\n * Badge component for Ink.\n *\n * Inspired by ink-ui Badge by Vadim Demedes.\n * @see https://github.com/vadimdemedes/ink-ui\n *\n * MIT License\n * Copyright (c) Vadym Demedes (github.com/vadimdemedes)\n */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type Props = {\n /**\n * Label content. Strings are automatically uppercased.\n */\n readonly children: ReactNode;\n\n /**\n * Background color for the badge.\n * @default \"magenta\"\n */\n readonly color?: LiteralUnion<AnsiColors, string>;\n};\n\n/**\n * Renders an uppercase colored label badge, useful for status indicators.\n */\nexport default function Badge({ children, color = \"magenta\" }: Props): ReactElement {\n const formattedChildren = typeof children === \"string\" ? children.toUpperCase() : children;\n\n return (\n <Text backgroundColor={color}>\n {\" \"}\n <Text color=\"black\">{formattedChildren}</Text>\n{\" \"}\n </Text>\n );\n}\n\nexport { Badge };\nexport type { Props as BadgeProps };\n",
19
+ "path": "registry/ui/badge.tsx",
20
+ "target": "components/ui/badge.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "bar-chart",
4
+ "type": "registry:ui",
5
+ "title": "Bar Chart",
6
+ "description": "Bar Chart component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport type { CanvasContext } from \"@visulima/tui/canvas\";\nimport Box from \"@visulima/tui/components/box\";\nimport Canvas from \"@visulima/tui/components/canvas\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement } from \"react\";\nimport { useMemo } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type BarDatum = {\n /**\n * Per-bar color override. Falls back to the chart-level palette.\n */\n readonly color?: LiteralUnion<AnsiColors, string>;\n readonly label?: string;\n readonly value: number;\n};\n\nexport type Props = {\n /**\n * Bars to render.\n */\n readonly data: ReadonlyArray<BarDatum>;\n\n /**\n * Total height of the plot area in character rows.\n * @default 10\n */\n readonly height?: number;\n\n /**\n * Optional upper bound for the axis. When omitted the chart uses the\n * largest value in `data`.\n */\n readonly max?: number;\n\n /**\n * Orientation of the bars.\n * @default \"vertical\"\n */\n readonly orientation?: \"horizontal\" | \"vertical\";\n\n /**\n * Fallback palette cycled through when a bar does not provide its own color.\n * @default [\"cyan\", \"magenta\", \"yellow\", \"green\", \"blue\", \"red\"]\n */\n readonly palette?: ReadonlyArray<LiteralUnion<AnsiColors, string>>;\n\n /**\n * Render labels underneath vertical bars (or to the left of horizontal bars).\n * @default true\n */\n readonly showLabels?: boolean;\n\n /**\n * Render numeric values next to each bar.\n * @default false\n */\n readonly showValues?: boolean;\n\n /**\n * Total width of the plot area in character columns. Defaults to\n * `data.length * 3` for vertical charts, or `24` for horizontal.\n */\n readonly width?: number;\n};\n\nconst DEFAULT_PALETTE: ReadonlyArray<LiteralUnion<AnsiColors, string>> = [\"cyan\", \"magenta\", \"yellow\", \"green\", \"blue\", \"red\"];\n\n/**\n * Walk `data` once to find the maximum value. Avoids `Math.max(...values)`\n * which blows the call-stack on large arrays (same rule as Sparkline).\n */\nconst findMax = (data: ReadonlyArray<BarDatum>, userMax: number | undefined): number => {\n if (userMax !== undefined) {\n return userMax;\n }\n\n if (data.length === 0) {\n return 0;\n }\n\n // Seed from the first sample so all-negative datasets still yield a\n // sensible upper bound (otherwise initializing `max = 0` would wipe out\n // every datum < 0).\n let max = data[0]!.value;\n\n for (let index = 1; index < data.length; index += 1) {\n const { value } = data[index]!;\n\n if (value > max) {\n max = value;\n }\n }\n\n return max;\n};\n\nconst colorFor = (index: number, palette: ReadonlyArray<LiteralUnion<AnsiColors, string>>, datum: BarDatum): LiteralUnion<AnsiColors, string> =>\n datum.color ?? palette[index % palette.length] ?? \"cyan\";\n\n/**\n * Bar chart rendered on top of `Canvas`. Uses fractional block glyphs for\n * sub-cell precision (▁▂▃▄▅▆▇█ vertical, ▏▎▍▌▋▊▉█ horizontal).\n */\nexport default function BarChart({\n data,\n height = 10,\n max: maxOverride,\n orientation = \"vertical\",\n palette = DEFAULT_PALETTE,\n showLabels = true,\n showValues = false,\n width: widthOverride,\n}: Props): ReactElement {\n const computedMax = useMemo(() => findMax(data, maxOverride), [data, maxOverride]);\n\n if (orientation === \"horizontal\") {\n const plotWidth = widthOverride ?? 24;\n\n return (\n <Box flexDirection=\"column\">\n {data.map((datum, index) => {\n const ratio = computedMax === 0 ? 0 : datum.value / computedMax;\n const color = colorFor(index, palette, datum);\n\n return (\n // eslint-disable-next-line react-x/no-array-index-key -- chart bar index is stable for the render\n <Box gap={1} key={index}>\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showLabels && datum.label !== undefined ? (\n <Box flexShrink={0} minWidth={8}>\n <Text dimColor wrap=\"truncate-end\">\n {datum.label}\n </Text>\n </Box>\n ) : undefined}\n <Box flexGrow={1} flexShrink={1}>\n <Canvas\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop -- canvas re-renders on `version` change, not draw identity\n draw={(context: CanvasContext) => {\n context.clear();\n context.drawHBar(0, 0, plotWidth, ratio, { color });\n }}\n height={1}\n // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop -- version array is the canvas redraw key\n version={[datum.value, computedMax, color, plotWidth]}\n width={plotWidth}\n />\n </Box>\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showValues ? (\n <Box flexShrink={0}>\n <Text>{String(datum.value)}</Text>\n </Box>\n ) : undefined}\n </Box>\n );\n })}\n </Box>\n );\n }\n\n const barWidth = 2;\n const gap = 1;\n const plotWidth = widthOverride ?? Math.max(data.length * (barWidth + gap), 1);\n\n return (\n <Box flexDirection=\"column\">\n <Canvas\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop -- canvas re-renders on `version` change, not draw identity\n draw={(context: CanvasContext) => {\n context.clear();\n\n data.forEach((datum, index) => {\n const ratio = computedMax === 0 ? 0 : datum.value / computedMax;\n const color = colorFor(index, palette, datum);\n const x = index * (barWidth + gap);\n\n for (let column = 0; column < barWidth; column += 1) {\n context.drawVBar(x + column, 0, height, ratio, { color });\n }\n });\n }}\n height={height}\n // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop -- version array is the canvas redraw key\n version={[data, computedMax, palette, barWidth, gap, height]}\n width={plotWidth}\n />\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showValues ? (\n <Box>\n {data.map((datum, index) => (\n // eslint-disable-next-line react-x/no-array-index-key -- chart column index is stable for the render\n <Box key={index} width={barWidth + gap}>\n <Text dimColor>{String(datum.value).padEnd(barWidth + gap, \" \")}</Text>\n </Box>\n ))}\n </Box>\n ) : undefined}\n {/* eslint-disable-next-line @stylistic/multiline-ternary -- prettier formats JSX ternaries on one line */}\n {showLabels ? (\n <Box>\n {data.map((datum, index) => (\n // eslint-disable-next-line react-x/no-array-index-key -- chart column index is stable for the render\n <Box key={index} width={barWidth + gap}>\n <Text dimColor wrap=\"truncate-end\">\n {(datum.label ?? \"\").padEnd(barWidth + gap, \" \")}\n </Text>\n </Box>\n ))}\n </Box>\n ) : undefined}\n </Box>\n );\n}\n\nexport { BarChart };\nexport type { Props as BarChartProps };\n",
19
+ "path": "registry/ui/bar-chart.tsx",
20
+ "target": "components/ui/bar-chart.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "big-text",
4
+ "type": "registry:ui",
5
+ "title": "Big Text",
6
+ "description": "Big Text component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/tui",
12
+ "cfonts"
13
+ ],
14
+ "registryDependencies": [],
15
+ "files": [
16
+ {
17
+ "content": "/* eslint-disable react/function-component-definition */\nimport Text from \"@visulima/tui/components/text\";\nimport CFonts from \"cfonts\";\nimport type { ReactElement } from \"react\";\nimport { useMemo } from \"react\";\n\n/**\n * Available CFonts font faces.\n */\ntype Font = \"3d\" | \"block\" | \"chrome\" | \"grid\" | \"huge\" | \"pallet\" | \"shade\" | \"simple\" | \"simple3d\" | \"simpleBlock\" | \"slick\" | \"tiny\";\n\n/**\n * Horizontal text alignment.\n */\ntype Align = \"center\" | \"left\" | \"right\";\n\n/**\n * Terminal background color names supported by CFonts.\n */\ntype BackgroundColor = \"black\" | \"blue\" | \"cyan\" | \"green\" | \"magenta\" | \"red\" | \"transparent\" | \"white\" | \"yellow\";\n\ntype Props = {\n /**\n * Horizontal alignment of the text.\n * @default \"left\"\n */\n readonly align?: Align;\n\n /**\n * Background color.\n * @default \"transparent\"\n */\n readonly backgroundColor?: BackgroundColor;\n\n /**\n * Colors to use for the font. Accepts color names, hex values, or `\"system\"` for terminal default.\n * @default [\"system\"]\n */\n readonly colors?: string[];\n\n /**\n * Font face to use.\n * @default \"block\"\n * @see https://github.com/dominikwilkowski/cfonts#supported-fonts\n */\n readonly font?: Font;\n\n /**\n * Letter spacing.\n * @default 1\n */\n readonly letterSpacing?: number;\n\n /**\n * Line height.\n * @default 1\n */\n readonly lineHeight?: number;\n\n /**\n * Maximum character length per line. `0` means no limit.\n * @default 0\n */\n readonly maxLength?: number;\n\n /**\n * Whether to add an empty line on top and at the bottom of the output.\n * @default true\n */\n readonly space?: boolean;\n\n /**\n * The text to render in a large font.\n */\n readonly text: string;\n};\n\nconst defaults = {\n align: \"left\" as const,\n backgroundColor: \"transparent\" as const,\n colors: [\"system\"],\n font: \"block\" as const,\n letterSpacing: 1,\n lineHeight: 1,\n maxLength: 0,\n space: true,\n};\n\n/**\n * Render large terminal text using CFonts.\n * @example\n * ```tsx\n * import { BigText } from \"@visulima/tui/ink\";\n *\n * <BigText text=\"Hello\" font=\"block\" />\n * ```\n */\nexport default function BigText({ align, backgroundColor, colors, font, letterSpacing, lineHeight, maxLength, space, text }: Props): ReactElement {\n const rendered = useMemo(\n () => CFonts.render(text, { ...defaults, align, backgroundColor, colors, font, letterSpacing, lineHeight, maxLength, space }),\n [text, align, backgroundColor, colors, font, letterSpacing, lineHeight, maxLength, space],\n );\n\n return <Text>{rendered && rendered.string}</Text>;\n}\n\nexport { BigText };\nexport type { Align, BackgroundColor, Font, Props };\nexport type { Align as BigTextAlign, BackgroundColor as BigTextBackgroundColor, Font as BigTextFont, Props as BigTextProps };\n",
18
+ "path": "registry/ui/big-text.tsx",
19
+ "target": "components/ui/big-text.tsx",
20
+ "type": "registry:ui"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "blink-dot",
4
+ "type": "registry:ui",
5
+ "title": "Blink Dot",
6
+ "description": "Blink Dot component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Text from \"@visulima/tui/components/text\";\nimport useAnimation from \"@visulima/tui/hooks/use-animation\";\nimport type { ReactElement } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type Props = {\n /**\n * Glyph rendered as the blinking dot.\n * @default \"●\" (filled bullet)\n */\n readonly character?: string;\n\n /**\n * Dot color.\n * @default \"red\"\n */\n readonly color?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Milliseconds per blink cycle. The dot is visible for half the cycle.\n * @default 800\n */\n readonly interval?: number;\n\n /**\n * When false, the dot stays solid (no animation).\n * @default true\n */\n readonly isActive?: boolean;\n};\n\n/**\n * Steady or blinking indicator dot. Pair with `StatusLine` or next to a\n * label to show a \"live\" / \"recording\" status.\n */\nexport default function BlinkDot({ character = \"●\", color = \"red\", interval = 800, isActive = true }: Props): ReactElement {\n const { frame } = useAnimation({ interval: Math.max(1, Math.floor(interval / 2)), isActive });\n // When the animation is paused, useAnimation freezes `frame` at 0 — the\n // dot stays solid. When running, even frames show the character and odd\n // frames show a space, producing the blink.\n const visible = !isActive || frame % 2 === 0;\n\n return <Text color={color}>{visible ? character : \" \"}</Text>;\n}\n\nexport { BlinkDot };\nexport type { Props as BlinkDotProps };\n",
19
+ "path": "registry/ui/blink-dot.tsx",
20
+ "target": "components/ui/blink-dot.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "breadcrumb",
4
+ "type": "registry:ui",
5
+ "title": "Breadcrumb",
6
+ "description": "Breadcrumb component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Fragment } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type BreadcrumbItem = {\n readonly icon?: ReactNode;\n readonly key?: string;\n readonly label: string;\n};\n\nexport type Props = {\n /**\n * Color applied to the trailing (current) item.\n * @default \"blue\"\n */\n readonly currentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Items to display from root to current page.\n */\n readonly items: ReadonlyArray<BreadcrumbItem>;\n\n /**\n * Glyph rendered between adjacent items.\n * @default \"›\" (right single angle quote)\n */\n readonly separator?: string;\n};\n\n/**\n * Render a breadcrumb trail (last item is highlighted).\n */\nexport default function Breadcrumb({ currentColor = \"blue\", items, separator = \"›\" }: Props): ReactElement {\n return (\n <Box>\n {items.map((item, index) => {\n const isLast = index === items.length - 1;\n\n return (\n <Fragment key={item.key ?? item.label}>\n {index > 0\n ? (\n<Text dimColor>\n{\" \"}\n{separator}\n{\" \"}\n</Text>\n )\n : undefined}\n <Text bold={isLast} color={isLast ? currentColor : undefined} dimColor={!isLast}>\n {item.icon === undefined\n ? undefined\n : (\n<>\n{item.icon}\n{\" \"}\n</>\n )}\n {item.label}\n </Text>\n </Fragment>\n );\n })}\n </Box>\n );\n}\n\nexport { Breadcrumb };\nexport type { Props as BreadcrumbProps };\n",
19
+ "path": "registry/ui/breadcrumb.tsx",
20
+ "target": "components/ui/breadcrumb.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "button",
4
+ "type": "registry:ui",
5
+ "title": "Button",
6
+ "description": "Button component for @visulima/tui.",
7
+ "categories": [
8
+ "ui"
9
+ ],
10
+ "dependencies": [
11
+ "@visulima/colorize",
12
+ "@visulima/tui",
13
+ "type-fest"
14
+ ],
15
+ "registryDependencies": [],
16
+ "files": [
17
+ {
18
+ "content": "/* eslint-disable react/function-component-definition */\nimport type { AnsiColors } from \"@visulima/colorize\";\nimport Box from \"@visulima/tui/components/box\";\nimport Text from \"@visulima/tui/components/text\";\nimport useFocus from \"@visulima/tui/hooks/use-focus\";\nimport useInput from \"@visulima/tui/hooks/use-input\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useCallback } from \"react\";\nimport type { LiteralUnion } from \"type-fest\";\n\nexport type ButtonVariant = \"ghost\" | \"outline\" | \"primary\" | \"secondary\";\n\nexport type Props = {\n /**\n * Accent color used when the button is focused.\n * @default \"blue\"\n */\n readonly accentColor?: LiteralUnion<AnsiColors, string>;\n\n /**\n * Focus this button when mounted if nothing else is focused.\n */\n readonly autoFocus?: boolean;\n\n /**\n * Button label.\n */\n readonly children: ReactNode;\n\n /**\n * When true, the button ignores input and is rendered dim.\n */\n readonly isDisabled?: boolean;\n\n /**\n * Called when the user presses Enter or Space while the button has focus.\n */\n readonly onPress?: () => void;\n\n /**\n * Visual style.\n * @default \"primary\"\n */\n readonly variant?: ButtonVariant;\n};\n\nconst BORDER_STYLE = \"round\" as const;\n\n/**\n * Pick the label color for a Button based on its variant, focus state, and\n * disabled state. Returns `undefined` when the default terminal color should\n * be used (disabled, or an unfocused non-primary button). The focused primary\n * variant uses `\"black\"` so the label is readable against the filled accent\n * background; every other focused variant just echoes `accentColor`.\n */\nconst resolveTextColor = (\n variant: ButtonVariant,\n isFocused: boolean,\n isDisabled: boolean,\n accentColor: LiteralUnion<AnsiColors, string>,\n // eslint-disable-next-line sonarjs/function-return-type -- legitimate union return based on variant/state\n): LiteralUnion<AnsiColors, string> | undefined => {\n if (isDisabled) {\n return undefined;\n }\n\n if (variant === \"primary\" && isFocused) {\n return \"black\";\n }\n\n if (isFocused) {\n return accentColor;\n }\n\n return undefined;\n};\n\n/**\n * Focusable button. Triggers `onPress` on Enter / Space.\n * @returns A `ReactElement` rendering the button (bordered or ghost variant).\n */\nexport default function Button({ accentColor = \"blue\", autoFocus = false, children, isDisabled = false, onPress, variant = \"primary\" }: Props): ReactElement {\n const { isFocused } = useFocus({ autoFocus, isActive: !isDisabled });\n\n useInput(\n useCallback(\n (input, key) => {\n if (!isFocused) {\n return;\n }\n\n if (key.return || input === \" \") {\n onPress?.();\n }\n },\n [isFocused, onPress],\n ),\n { isActive: !isDisabled && isFocused },\n );\n\n const textColor = resolveTextColor(variant, isFocused, isDisabled, accentColor);\n\n if (variant === \"ghost\") {\n return (\n <Box paddingX={1}>\n <Text bold={isFocused} color={textColor} dimColor={isDisabled}>\n {isFocused ? \"▸ \" : \" \"}\n {children}\n </Text>\n </Box>\n );\n }\n\n // Visual tokens per variant:\n //\n // primary → filled accent background + accent border (highlighted when focused)\n // secondary → transparent background + accent border always; bolder label on focus\n // outline → transparent background + neutral border, accent-colored when focused\n // eslint-disable-next-line sonarjs/function-return-type -- legitimate union return based on variant/focus\n const borderColor = (() => {\n if (variant === \"outline\") {\n return isFocused ? accentColor : \"gray\";\n }\n\n if (variant === \"secondary\") {\n // Secondary keeps the accent-colored border always; focus is\n // conveyed by the bold label alone.\n return accentColor;\n }\n\n // primary (and any future variants) — accent border when focused.\n return isFocused ? accentColor : undefined;\n })();\n const fillBackground = variant === \"primary\" && isFocused ? accentColor : undefined;\n\n return (\n <Box backgroundColor={fillBackground} borderColor={borderColor} borderStyle={BORDER_STYLE} paddingX={1}>\n <Text bold={isFocused} color={textColor} dimColor={isDisabled}>\n {children}\n </Text>\n </Box>\n );\n}\n\nexport { Button };\nexport type { Props as ButtonProps };\n",
19
+ "path": "registry/ui/button.tsx",
20
+ "target": "components/ui/button.tsx",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }