ratatui_ruby 0.4.0 → 0.6.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 (441) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +98 -176
  7. data/CHANGELOG.md +80 -6
  8. data/README.md +19 -7
  9. data/REUSE.toml +15 -0
  10. data/doc/application_architecture.md +179 -45
  11. data/doc/application_testing.md +80 -32
  12. data/doc/contributors/design/ruby_frontend.md +48 -8
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +191 -48
  15. data/doc/contributors/documentation_style.md +7 -0
  16. data/doc/contributors/examples_audit/p1_high.md +21 -0
  17. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  18. data/doc/contributors/examples_audit.md +41 -0
  19. data/doc/contributors/index.md +2 -0
  20. data/doc/event_handling.md +21 -7
  21. data/doc/images/app_all_events.png +0 -0
  22. data/doc/images/app_color_picker.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_stateful_interaction.png +0 -0
  25. data/doc/images/verify_quickstart_dsl.png +0 -0
  26. data/doc/images/verify_quickstart_layout.png +0 -0
  27. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  28. data/doc/images/verify_readme_usage.png +0 -0
  29. data/doc/images/widget_barchart_demo.png +0 -0
  30. data/doc/images/widget_block_demo.png +0 -0
  31. data/doc/images/widget_box_demo.png +0 -0
  32. data/doc/images/widget_calendar_demo.png +0 -0
  33. data/doc/images/widget_canvas_demo.png +0 -0
  34. data/doc/images/widget_cell_demo.png +0 -0
  35. data/doc/images/widget_center_demo.png +0 -0
  36. data/doc/images/widget_chart_demo.png +0 -0
  37. data/doc/images/widget_gauge_demo.png +0 -0
  38. data/doc/images/widget_layout_split.png +0 -0
  39. data/doc/images/widget_line_gauge_demo.png +0 -0
  40. data/doc/images/widget_list_demo.png +0 -0
  41. data/doc/images/widget_overlay_demo.png +0 -0
  42. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  43. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  44. data/doc/images/widget_render.png +0 -0
  45. data/doc/images/widget_rich_text.png +0 -0
  46. data/doc/images/widget_scroll_text.png +0 -0
  47. data/doc/images/widget_scrollbar_demo.png +0 -0
  48. data/doc/images/widget_sparkline_demo.png +0 -0
  49. data/doc/images/widget_style_colors.png +0 -0
  50. data/doc/images/widget_table_demo.png +0 -0
  51. data/doc/images/widget_table_flex.png +0 -0
  52. data/doc/images/widget_tabs_demo.png +0 -0
  53. data/doc/images/widget_text_width.png +0 -0
  54. data/doc/interactive_design.md +25 -30
  55. data/doc/quickstart.md +150 -130
  56. data/doc/terminal_limitations.md +92 -0
  57. data/examples/app_all_events/README.md +99 -0
  58. data/examples/app_all_events/app.rb +96 -0
  59. data/examples/app_all_events/model/app_model.rb +157 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +92 -0
  62. data/examples/app_all_events/model/msg.rb +37 -0
  63. data/examples/app_all_events/model/timestamp.rb +54 -0
  64. data/examples/app_all_events/update.rb +73 -0
  65. data/examples/app_all_events/view/app_view.rb +78 -0
  66. data/examples/app_all_events/view/controls_view.rb +52 -0
  67. data/examples/app_all_events/view/counts_view.rb +59 -0
  68. data/examples/app_all_events/view/live_view.rb +70 -0
  69. data/examples/app_all_events/view/log_view.rb +55 -0
  70. data/examples/app_all_events/view.rb +7 -0
  71. data/examples/app_color_picker/README.md +134 -0
  72. data/examples/app_color_picker/app.rb +74 -0
  73. data/examples/app_color_picker/clipboard.rb +84 -0
  74. data/examples/app_color_picker/color.rb +191 -0
  75. data/examples/app_color_picker/controls.rb +90 -0
  76. data/examples/app_color_picker/copy_dialog.rb +166 -0
  77. data/examples/app_color_picker/export_pane.rb +126 -0
  78. data/examples/app_color_picker/harmony.rb +56 -0
  79. data/examples/app_color_picker/input.rb +174 -0
  80. data/examples/app_color_picker/main_container.rb +178 -0
  81. data/examples/app_color_picker/palette.rb +109 -0
  82. data/examples/app_login_form/README.md +47 -0
  83. data/examples/{login_form → app_login_form}/app.rb +38 -42
  84. data/examples/app_stateful_interaction/README.md +31 -0
  85. data/examples/app_stateful_interaction/app.rb +272 -0
  86. data/examples/timeout_demo.rb +43 -0
  87. data/examples/verify_quickstart_dsl/README.md +48 -0
  88. data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +17 -6
  89. data/examples/verify_quickstart_layout/README.md +71 -0
  90. data/examples/verify_quickstart_layout/app.rb +71 -0
  91. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  92. data/examples/verify_quickstart_lifecycle/app.rb +54 -0
  93. data/examples/verify_readme_usage/README.md +43 -0
  94. data/examples/verify_readme_usage/app.rb +40 -0
  95. data/examples/widget_barchart_demo/README.md +49 -0
  96. data/examples/widget_barchart_demo/app.rb +238 -0
  97. data/examples/widget_block_demo/README.md +34 -0
  98. data/examples/widget_block_demo/app.rb +256 -0
  99. data/examples/widget_box_demo/README.md +45 -0
  100. data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
  101. data/examples/widget_calendar_demo/README.md +39 -0
  102. data/examples/widget_calendar_demo/app.rb +109 -0
  103. data/examples/widget_canvas_demo/README.md +27 -0
  104. data/examples/widget_canvas_demo/app.rb +123 -0
  105. data/examples/widget_cell_demo/README.md +36 -0
  106. data/examples/widget_cell_demo/app.rb +111 -0
  107. data/examples/widget_center_demo/README.md +29 -0
  108. data/examples/widget_center_demo/app.rb +116 -0
  109. data/examples/widget_chart_demo/README.md +41 -0
  110. data/examples/widget_chart_demo/app.rb +218 -0
  111. data/examples/widget_gauge_demo/README.md +41 -0
  112. data/examples/widget_gauge_demo/app.rb +212 -0
  113. data/examples/widget_layout_split/README.md +44 -0
  114. data/examples/widget_layout_split/app.rb +246 -0
  115. data/examples/widget_line_gauge_demo/README.md +41 -0
  116. data/examples/widget_line_gauge_demo/app.rb +217 -0
  117. data/examples/widget_list_demo/README.md +49 -0
  118. data/examples/widget_list_demo/app.rb +366 -0
  119. data/examples/widget_map_demo/README.md +39 -0
  120. data/examples/{map_demo → widget_map_demo}/app.rb +24 -21
  121. data/examples/widget_overlay_demo/app.rb +248 -0
  122. data/examples/widget_popup_demo/README.md +36 -0
  123. data/examples/widget_popup_demo/app.rb +104 -0
  124. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  125. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  126. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  127. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  128. data/examples/widget_rect/README.md +38 -0
  129. data/examples/widget_rect/app.rb +205 -0
  130. data/examples/widget_render/README.md +37 -0
  131. data/examples/widget_render/app.rb +184 -0
  132. data/examples/widget_rich_text/README.md +35 -0
  133. data/examples/widget_rich_text/app.rb +166 -0
  134. data/examples/widget_scroll_text/README.md +37 -0
  135. data/examples/widget_scroll_text/app.rb +107 -0
  136. data/examples/widget_scrollbar_demo/README.md +37 -0
  137. data/examples/widget_scrollbar_demo/app.rb +153 -0
  138. data/examples/widget_sparkline_demo/README.md +42 -0
  139. data/examples/widget_sparkline_demo/app.rb +275 -0
  140. data/examples/widget_style_colors/README.md +34 -0
  141. data/examples/widget_style_colors/app.rb +19 -21
  142. data/examples/widget_table_demo/README.md +48 -0
  143. data/examples/widget_table_demo/app.rb +239 -0
  144. data/examples/widget_tabs_demo/README.md +41 -0
  145. data/examples/widget_tabs_demo/app.rb +181 -0
  146. data/examples/widget_text_width/README.md +35 -0
  147. data/examples/widget_text_width/app.rb +106 -0
  148. data/ext/ratatui_ruby/Cargo.lock +11 -4
  149. data/ext/ratatui_ruby/Cargo.toml +2 -1
  150. data/ext/ratatui_ruby/src/events.rs +359 -62
  151. data/ext/ratatui_ruby/src/frame.rs +227 -0
  152. data/ext/ratatui_ruby/src/lib.rs +110 -27
  153. data/ext/ratatui_ruby/src/rendering.rs +8 -4
  154. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  155. data/ext/ratatui_ruby/src/style.rs +138 -57
  156. data/ext/ratatui_ruby/src/terminal.rs +42 -22
  157. data/ext/ratatui_ruby/src/text.rs +14 -7
  158. data/ext/ratatui_ruby/src/widgets/barchart.rs +74 -54
  159. data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
  160. data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
  161. data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
  162. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  163. data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
  164. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  165. data/ext/ratatui_ruby/src/widgets/list.rs +211 -12
  166. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  167. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  168. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  169. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  170. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
  171. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
  172. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +97 -3
  173. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  174. data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
  175. data/ext/ratatui_ruby/src/widgets/table.rs +121 -5
  176. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  177. data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
  178. data/lib/ratatui_ruby/cell.rb +7 -7
  179. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  180. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  181. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  182. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  183. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  184. data/lib/ratatui_ruby/event/key.rb +112 -52
  185. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  186. data/lib/ratatui_ruby/event/none.rb +43 -0
  187. data/lib/ratatui_ruby/event/paste.rb +1 -1
  188. data/lib/ratatui_ruby/event.rb +56 -4
  189. data/lib/ratatui_ruby/frame.rb +183 -0
  190. data/lib/ratatui_ruby/list_state.rb +88 -0
  191. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +13 -13
  192. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
  193. data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
  194. data/lib/ratatui_ruby/schema/block.rb +163 -168
  195. data/lib/ratatui_ruby/schema/calendar.rb +66 -67
  196. data/lib/ratatui_ruby/schema/canvas.rb +63 -63
  197. data/lib/ratatui_ruby/schema/center.rb +46 -46
  198. data/lib/ratatui_ruby/schema/chart.rb +135 -143
  199. data/lib/ratatui_ruby/schema/clear.rb +42 -42
  200. data/lib/ratatui_ruby/schema/constraint.rb +76 -76
  201. data/lib/ratatui_ruby/schema/cursor.rb +30 -25
  202. data/lib/ratatui_ruby/schema/gauge.rb +54 -52
  203. data/lib/ratatui_ruby/schema/layout.rb +87 -87
  204. data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
  205. data/lib/ratatui_ruby/schema/list.rb +103 -80
  206. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  207. data/lib/ratatui_ruby/schema/overlay.rb +31 -31
  208. data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
  209. data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
  210. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
  211. data/lib/ratatui_ruby/schema/rect.rb +99 -56
  212. data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
  213. data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
  214. data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
  215. data/lib/ratatui_ruby/schema/style.rb +66 -46
  216. data/lib/ratatui_ruby/schema/table.rb +126 -115
  217. data/lib/ratatui_ruby/schema/tabs.rb +66 -67
  218. data/lib/ratatui_ruby/schema/text.rb +69 -1
  219. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  220. data/lib/ratatui_ruby/session/autodoc.rb +482 -0
  221. data/lib/ratatui_ruby/session.rb +55 -23
  222. data/lib/ratatui_ruby/table_state.rb +90 -0
  223. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  224. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  225. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  226. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  227. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  228. data/lib/ratatui_ruby/test_helper.rb +66 -193
  229. data/lib/ratatui_ruby/version.rb +1 -1
  230. data/lib/ratatui_ruby.rb +100 -51
  231. data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
  232. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  233. data/sig/examples/app_all_events/model/events.rbs +15 -0
  234. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  235. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  236. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  237. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  238. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  239. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  240. data/sig/examples/app_all_events/view.rbs +8 -0
  241. data/sig/examples/app_all_events/view_state.rbs +15 -0
  242. data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
  243. data/sig/examples/app_login_form/app.rbs +11 -0
  244. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  245. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  246. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  247. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  248. data/sig/examples/widget_block_demo/app.rbs +32 -0
  249. data/sig/examples/widget_box_demo/app.rbs +11 -0
  250. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  251. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  252. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  253. data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
  254. data/sig/examples/widget_layout_split/app.rbs +10 -0
  255. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  256. data/sig/examples/widget_list_demo/app.rbs +12 -0
  257. data/sig/examples/widget_map_demo/app.rbs +11 -0
  258. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  259. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  260. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  261. data/sig/examples/widget_rect/app.rbs +12 -0
  262. data/sig/examples/widget_render/app.rbs +10 -0
  263. data/sig/examples/widget_rich_text/app.rbs +11 -0
  264. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  265. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  266. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  267. data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
  268. data/sig/examples/widget_table_demo/app.rbs +11 -0
  269. data/sig/examples/widget_text_width/app.rbs +10 -0
  270. data/sig/ratatui_ruby/event.rbs +11 -1
  271. data/sig/ratatui_ruby/frame.rbs +11 -0
  272. data/sig/ratatui_ruby/list_state.rbs +13 -0
  273. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -4
  274. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  275. data/sig/ratatui_ruby/schema/draw.rbs +4 -0
  276. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  277. data/sig/ratatui_ruby/schema/layout.rbs +1 -1
  278. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  279. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  280. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  281. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  282. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  283. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  284. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  285. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  286. data/sig/ratatui_ruby/session.rbs +107 -0
  287. data/sig/ratatui_ruby/table_state.rbs +15 -0
  288. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  289. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  290. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  291. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  292. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  293. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  294. data/tasks/autodoc/examples.rb +79 -0
  295. data/tasks/autodoc/inventory.rb +63 -0
  296. data/tasks/autodoc/member.rb +56 -0
  297. data/tasks/autodoc/name.rb +19 -0
  298. data/tasks/autodoc/notice.rb +26 -0
  299. data/tasks/autodoc/rbs.rb +38 -0
  300. data/tasks/autodoc/rdoc.rb +45 -0
  301. data/tasks/autodoc.rake +53 -0
  302. data/tasks/bump/changelog.rb +3 -3
  303. data/tasks/bump/history.rb +2 -2
  304. data/tasks/bump/links.rb +67 -0
  305. data/tasks/doc.rake +600 -6
  306. data/tasks/example_viewer.html.erb +172 -0
  307. data/tasks/lint.rake +8 -4
  308. data/tasks/resources/index.html.erb +6 -0
  309. data/tasks/sourcehut.rake +70 -30
  310. data/tasks/terminal_preview/app_screenshot.rb +14 -6
  311. data/tasks/terminal_preview/crash_report.rb +7 -9
  312. data/tasks/terminal_preview/launcher_script.rb +4 -6
  313. data/tasks/terminal_preview/preview_collection.rb +4 -6
  314. data/tasks/terminal_preview/safety_confirmation.rb +3 -5
  315. data/tasks/terminal_preview/saved_screenshot.rb +10 -11
  316. data/tasks/terminal_preview/terminal_window.rb +7 -9
  317. data/tasks/test.rake +1 -1
  318. data/tasks/website/index_page.rb +3 -3
  319. data/tasks/website/version.rb +10 -10
  320. data/tasks/website/version_menu.rb +10 -12
  321. data/tasks/website/versioned_documentation.rb +49 -17
  322. data/tasks/website/website.rb +6 -8
  323. data/tasks/website.rake +4 -4
  324. metadata +232 -127
  325. data/LICENSES/BSD-2-Clause.txt +0 -9
  326. data/doc/contributors/better_dx.md +0 -543
  327. data/doc/contributors/example_analysis.md +0 -82
  328. data/doc/images/all_events.png +0 -0
  329. data/doc/images/block_padding.png +0 -0
  330. data/doc/images/block_titles.png +0 -0
  331. data/doc/images/box_demo.png +0 -0
  332. data/doc/images/calendar_demo.png +0 -0
  333. data/doc/images/cell_demo.png +0 -0
  334. data/doc/images/chart_demo.png +0 -0
  335. data/doc/images/flex_layout.png +0 -0
  336. data/doc/images/gauge_demo.png +0 -0
  337. data/doc/images/line_gauge_demo.png +0 -0
  338. data/doc/images/list_demo.png +0 -0
  339. data/doc/images/list_styles.png +0 -0
  340. data/doc/images/login_form.png +0 -0
  341. data/doc/images/quickstart_dsl.png +0 -0
  342. data/doc/images/quickstart_lifecycle.png +0 -0
  343. data/doc/images/readme_usage.png +0 -0
  344. data/doc/images/rich_text.png +0 -0
  345. data/doc/images/scroll_text.png +0 -0
  346. data/doc/images/scrollbar_demo.png +0 -0
  347. data/doc/images/sparkline_demo.png +0 -0
  348. data/doc/images/table_flex.png +0 -0
  349. data/doc/images/table_select.png +0 -0
  350. data/examples/all_events/app.rb +0 -169
  351. data/examples/all_events/app.rbs +0 -7
  352. data/examples/all_events/test_app.rb +0 -139
  353. data/examples/analytics/app.rb +0 -258
  354. data/examples/analytics/app.rbs +0 -7
  355. data/examples/analytics/test_app.rb +0 -132
  356. data/examples/block_padding/app.rb +0 -63
  357. data/examples/block_padding/app.rbs +0 -7
  358. data/examples/block_padding/test_app.rb +0 -31
  359. data/examples/block_titles/app.rb +0 -61
  360. data/examples/block_titles/app.rbs +0 -7
  361. data/examples/block_titles/test_app.rb +0 -34
  362. data/examples/box_demo/app.rbs +0 -7
  363. data/examples/box_demo/test_app.rb +0 -88
  364. data/examples/calendar_demo/app.rb +0 -101
  365. data/examples/calendar_demo/app.rbs +0 -7
  366. data/examples/calendar_demo/test_app.rb +0 -108
  367. data/examples/cell_demo/app.rb +0 -108
  368. data/examples/cell_demo/app.rbs +0 -7
  369. data/examples/cell_demo/test_app.rb +0 -36
  370. data/examples/chart_demo/app.rb +0 -203
  371. data/examples/chart_demo/app.rbs +0 -7
  372. data/examples/chart_demo/test_app.rb +0 -102
  373. data/examples/custom_widget/app.rb +0 -51
  374. data/examples/custom_widget/app.rbs +0 -7
  375. data/examples/custom_widget/test_app.rb +0 -30
  376. data/examples/flex_layout/app.rb +0 -156
  377. data/examples/flex_layout/app.rbs +0 -7
  378. data/examples/flex_layout/test_app.rb +0 -65
  379. data/examples/gauge_demo/app.rb +0 -182
  380. data/examples/gauge_demo/test_app.rb +0 -120
  381. data/examples/hit_test/app.rb +0 -175
  382. data/examples/hit_test/app.rbs +0 -7
  383. data/examples/hit_test/test_app.rb +0 -102
  384. data/examples/line_gauge_demo/app.rb +0 -190
  385. data/examples/line_gauge_demo/app.rbs +0 -7
  386. data/examples/line_gauge_demo/test_app.rb +0 -129
  387. data/examples/list_demo/app.rb +0 -253
  388. data/examples/list_demo/test_app.rb +0 -237
  389. data/examples/list_styles/app.rb +0 -140
  390. data/examples/list_styles/app.rbs +0 -7
  391. data/examples/list_styles/test_app.rb +0 -157
  392. data/examples/login_form/app.rbs +0 -7
  393. data/examples/login_form/test_app.rb +0 -51
  394. data/examples/map_demo/app.rbs +0 -7
  395. data/examples/map_demo/test_app.rb +0 -149
  396. data/examples/mouse_events/app.rb +0 -97
  397. data/examples/mouse_events/app.rbs +0 -7
  398. data/examples/mouse_events/test_app.rb +0 -53
  399. data/examples/popup_demo/app.rb +0 -103
  400. data/examples/popup_demo/app.rbs +0 -7
  401. data/examples/popup_demo/test_app.rb +0 -54
  402. data/examples/quickstart_dsl/app.rbs +0 -7
  403. data/examples/quickstart_dsl/test_app.rb +0 -29
  404. data/examples/quickstart_lifecycle/app.rb +0 -39
  405. data/examples/quickstart_lifecycle/app.rbs +0 -7
  406. data/examples/quickstart_lifecycle/test_app.rb +0 -29
  407. data/examples/ratatui_logo_demo/app.rb +0 -79
  408. data/examples/ratatui_logo_demo/app.rbs +0 -7
  409. data/examples/ratatui_logo_demo/test_app.rb +0 -51
  410. data/examples/ratatui_mascot_demo/app.rb +0 -84
  411. data/examples/ratatui_mascot_demo/app.rbs +0 -7
  412. data/examples/ratatui_mascot_demo/test_app.rb +0 -47
  413. data/examples/readme_usage/app.rb +0 -29
  414. data/examples/readme_usage/app.rbs +0 -7
  415. data/examples/readme_usage/test_app.rb +0 -29
  416. data/examples/rich_text/app.rb +0 -141
  417. data/examples/rich_text/app.rbs +0 -7
  418. data/examples/rich_text/test_app.rb +0 -166
  419. data/examples/scroll_text/app.rb +0 -103
  420. data/examples/scroll_text/app.rbs +0 -7
  421. data/examples/scroll_text/test_app.rb +0 -110
  422. data/examples/scrollbar_demo/app.rb +0 -143
  423. data/examples/scrollbar_demo/app.rbs +0 -7
  424. data/examples/scrollbar_demo/test_app.rb +0 -77
  425. data/examples/sparkline_demo/app.rb +0 -240
  426. data/examples/sparkline_demo/test_app.rb +0 -107
  427. data/examples/table_flex/app.rb +0 -65
  428. data/examples/table_flex/app.rbs +0 -7
  429. data/examples/table_flex/test_app.rb +0 -36
  430. data/examples/table_select/app.rb +0 -198
  431. data/examples/table_select/app.rbs +0 -7
  432. data/examples/table_select/test_app.rb +0 -180
  433. data/examples/widget_style_colors/test_app.rb +0 -48
  434. data/tasks/bump/comparison_links.rb +0 -41
  435. /data/doc/images/{analytics.png → app_analytics.png} +0 -0
  436. /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
  437. /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
  438. /data/doc/images/{map_demo.png → widget_map_demo.png} +0 -0
  439. /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
  440. /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
  441. /data/{doc/images/ratatui_logo_demo.png → exe/.gitkeep} +0 -0
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "chroma"
7
+ require "wcag_color_contrast"
8
+ require_relative "harmony"
9
+
10
+ # Represents a single color with format conversion and harmony generation.
11
+ #
12
+ # Colors are central to visual design. Users need to work with colors in multiple
13
+ # formats: hex, RGB, HSL. They also need to generate color schemes: shades, tints,
14
+ # and complementary colors. Managing these conversions and relationships manually
15
+ # is tedious and error-prone.
16
+ #
17
+ # This object wraps a Chroma color. It exposes format conversions. It generates
18
+ # color harmonies. It calculates contrast ratios to choose readable text colors.
19
+ #
20
+ # Use it to parse user input, transform colors, and build color palettes.
21
+ #
22
+ # === Example
23
+ #
24
+ # color = Color.parse("#FF0000")
25
+ # puts color.hex # => "#FF0000"
26
+ # puts color.rgb # => "rgb(255, 0, 0)"
27
+ # puts color.hsl_string # => "hsl(0, 100%, 50%)"
28
+ #
29
+ # # Generate harmonies
30
+ # harmonies = color.harmonies # => [main, shade, tint, complement, ...]
31
+ #
32
+ # # Transform colors
33
+ # lighter = color.tint(5)
34
+ # darker = color.shade(3)
35
+ # rotated = color.spin(180)
36
+ class Color
37
+ def initialize(chroma_color)
38
+ @chroma = chroma_color
39
+ end
40
+
41
+ # Parses a color string and returns a Color, or nil if the string is invalid.
42
+ #
43
+ # Accepts hex, RGB, HSL, and named colors. Trims whitespace and handles
44
+ # empty strings gracefully.
45
+ #
46
+ # [input_str] String in any format Chroma supports (e.g., <tt>"#FF0000"</tt>, <tt>"red"</tt>, <tt>"rgb(255,0,0)"</tt>)
47
+ #
48
+ # === Example
49
+ #
50
+ # Color.parse("#FF0000") # => Color
51
+ # Color.parse("red") # => Color
52
+ # Color.parse("invalid") # => nil
53
+ # Color.parse("") # => nil
54
+ def self.parse(input_str)
55
+ input_str = input_str.to_s.strip
56
+ return nil if input_str.empty?
57
+
58
+ new(Chroma.paint(input_str.dup))
59
+ rescue
60
+ nil
61
+ end
62
+
63
+ # Hex color code (uppercase).
64
+ #
65
+ # === Example
66
+ #
67
+ # color = Color.parse("red")
68
+ # color.hex # => "#FF0000"
69
+ def hex
70
+ @chroma.to_hex.upcase
71
+ end
72
+
73
+ # RGB color code.
74
+ #
75
+ # === Example
76
+ #
77
+ # color = Color.parse("red")
78
+ # color.rgb # => "rgb(255, 0, 0)"
79
+ def rgb
80
+ @chroma.to_rgb
81
+ end
82
+
83
+ # HSL color string with percentage formatting.
84
+ #
85
+ # === Example
86
+ #
87
+ # color = Color.parse("red")
88
+ # color.hsl_string # => "hsl(0, 100%, 50%)"
89
+ def hsl_string
90
+ hsl_obj = @chroma.hsl
91
+ h = hsl_obj.h
92
+ s = hsl_obj.s
93
+ l = hsl_obj.l
94
+ format("hsl(%.0f, %.1f%%, %.1f%%)", h, s * 100, l * 100)
95
+ end
96
+
97
+ # Darkens the color. Returns a new Color.
98
+ #
99
+ # [amount] Integer amount to darken (default: 3)
100
+ #
101
+ # === Example
102
+ #
103
+ # color = Color.parse("red")
104
+ # color.shade(5).hex # => darker red
105
+ def shade(amount = 3)
106
+ Color.new(@chroma.darken(amount))
107
+ end
108
+
109
+ # Lightens the color. Returns a new Color.
110
+ #
111
+ # [amount] Integer amount to lighten (default: 3)
112
+ #
113
+ # === Example
114
+ #
115
+ # color = Color.parse("red")
116
+ # color.tint(5).hex # => lighter red
117
+ def tint(amount = 3)
118
+ Color.new(@chroma.lighten(amount))
119
+ end
120
+
121
+ # Rotates the hue. Returns a new Color.
122
+ #
123
+ # [degrees] Integer degrees to rotate (0-360)
124
+ #
125
+ # === Example
126
+ #
127
+ # color = Color.parse("red")
128
+ # color.spin(180).hex # => cyan
129
+ def spin(degrees)
130
+ Color.new(@chroma.spin(degrees))
131
+ end
132
+
133
+ # Determines optimal text color (:white or :black) for maximum contrast.
134
+ #
135
+ # Uses WCAG contrast ratio calculation. Returns <tt>:white</tt> if white has
136
+ # higher contrast; <tt>:black</tt> otherwise.
137
+ #
138
+ # === Example
139
+ #
140
+ # Color.parse("yellow").contrasting_text_color # => :black
141
+ # Color.parse("navy").contrasting_text_color # => :white
142
+ def contrasting_text_color
143
+ white_contrast = WCAGColorContrast.ratio(hex.sub(/^#/, ""), "ffffff")
144
+ black_contrast = WCAGColorContrast.ratio(hex.sub(/^#/, ""), "000000")
145
+ (white_contrast > black_contrast) ? :white : :black
146
+ end
147
+
148
+ # Background color for rendering this color as a swatch.
149
+ #
150
+ # Returns <tt>"#000000"</tt> if text should be white; <tt>"#ffffff"</tt> if black.
151
+ # Used to frame color swatches with contrasting borders.
152
+ #
153
+ # === Example
154
+ #
155
+ # Color.parse("yellow").frame_color # => "#000000"
156
+ def frame_color
157
+ (contrasting_text_color == :white) ? "#000000" : "#ffffff"
158
+ end
159
+
160
+ # Seven-color harmony: main, shade, tint, complement, split 1, split 2, split-complement.
161
+ #
162
+ # Generates a complete color scheme for UI design. Each harmony is a Harmony
163
+ # value object with label, hex, and styling information.
164
+ #
165
+ # === Example
166
+ #
167
+ # color = Color.parse("red")
168
+ # harmonies = color.harmonies
169
+ # harmonies.first.label # => "Main"
170
+ # harmonies.size # => 7
171
+ def harmonies
172
+ [
173
+ harmony_with_label("Main"),
174
+ shade.harmony_with_label("Shade"),
175
+ tint.harmony_with_label("Tint"),
176
+ spin(180).harmony_with_label("Comp"),
177
+ spin(150).harmony_with_label("Split 1"),
178
+ spin(210).harmony_with_label("Split 2"),
179
+ spin(30).harmony_with_label("S.Comp"),
180
+ ]
181
+ end
182
+
183
+ def harmony_with_label(label)
184
+ Harmony.new(
185
+ label:,
186
+ hex:,
187
+ text_color: contrasting_text_color,
188
+ frame_color:,
189
+ )
190
+ end
191
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # A display-only component showing keyboard shortcuts and clipboard feedback.
7
+ #
8
+ # Users need to know what keys are available. They also need feedback when
9
+ # they copy a color. This component renders the controls section.
10
+ #
11
+ # === Component Contract
12
+ #
13
+ # - `render(tui, frame, area, clipboard:)`: Draws the controls; stores `area`
14
+ # - `handle_event(event) -> nil`: Display-only, always returns nil
15
+ # - `tick`: Delegates to clipboard for time-based feedback updates
16
+ #
17
+ # === Example
18
+ #
19
+ # controls = Controls.new
20
+ # controls.render(tui, frame, area, clipboard: clipboard)
21
+ # controls.tick(clipboard)
22
+ class Controls
23
+ def initialize
24
+ @area = nil
25
+ @hotkey_style = nil
26
+ end
27
+
28
+ # The cached render area.
29
+ attr_reader :area
30
+
31
+ # Renders the controls section into the given area.
32
+ #
33
+ # Shows keyboard shortcuts and clipboard feedback message if one is active.
34
+ #
35
+ # [tui] Session or TUI factory object
36
+ # [frame] Frame object from RatatuiRuby.draw block
37
+ # [area] Rect area to draw into
38
+ # [clipboard] Clipboard object for feedback message
39
+ #
40
+ # === Example
41
+ #
42
+ # controls.render(tui, frame, control_area, clipboard: clipboard)
43
+ def render(tui, frame, area, clipboard:)
44
+ @area = area
45
+ @hotkey_style ||= tui.style(modifiers: [:bold, :underlined])
46
+ widget = build_widget(tui, clipboard)
47
+ frame.render_widget(widget, area)
48
+ end
49
+
50
+ # Display-only component; always returns nil.
51
+ def handle_event(_event)
52
+ nil
53
+ end
54
+
55
+ # Delegates tick to the clipboard for time-based updates.
56
+ #
57
+ # [clipboard] Clipboard object to tick
58
+ def tick(clipboard)
59
+ clipboard.tick
60
+ end
61
+
62
+ private def build_widget(tui, clipboard)
63
+ control_lines = [
64
+ tui.text_line(spans: [
65
+ tui.text_span(content: "a-z/0-9", style: @hotkey_style),
66
+ tui.text_span(content: ": Type "),
67
+ tui.text_span(content: "enter", style: @hotkey_style),
68
+ tui.text_span(content: ": Parse "),
69
+ tui.text_span(content: "bksp", style: @hotkey_style),
70
+ tui.text_span(content: ": Erase "),
71
+ tui.text_span(content: "esc", style: @hotkey_style),
72
+ tui.text_span(content: ": Quit"),
73
+ ]),
74
+ ]
75
+
76
+ unless clipboard.message.empty?
77
+ control_lines << tui.text_line(spans: [
78
+ tui.text_span(content: clipboard.message, style: tui.style(fg: :green, modifiers: [:bold])),
79
+ ])
80
+ end
81
+
82
+ tui.block(
83
+ title: "Controls",
84
+ borders: [:all],
85
+ children: [
86
+ tui.paragraph(text: control_lines),
87
+ ]
88
+ )
89
+ end
90
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "clipboard"
7
+
8
+ # A self-contained modal dialog component for copying text to the clipboard.
9
+ #
10
+ # Users click on content they want to copy. The app needs to confirm: "Are you
11
+ # sure?" This component owns dialog state, renders itself, and handles keyboard
12
+ # input.
13
+ #
14
+ # === Component Contract
15
+ #
16
+ # - `render(tui, frame, area)`: Draws the dialog; stores `area`
17
+ # - `handle_event(event) -> Symbol | nil`: Returns `:consumed` when handled
18
+ # - `open(text)`: Opens the dialog with the text to copy
19
+ # - `close`: Closes the dialog
20
+ # - `active?`: True if the dialog is visible
21
+ #
22
+ # === Example
23
+ #
24
+ # dialog = CopyDialog.new(clipboard)
25
+ # dialog.open("#FF0000")
26
+ #
27
+ # result = dialog.handle_event(event)
28
+ # # result == :consumed when dialog handled the event
29
+ #
30
+ # dialog.render(tui, frame, center_area)
31
+ class CopyDialog
32
+ def initialize(clipboard)
33
+ @clipboard = clipboard
34
+ @text = ""
35
+ @selected = :yes
36
+ @active = false
37
+ @area = nil
38
+ end
39
+
40
+ # The cached render area.
41
+ attr_reader :area
42
+
43
+ # Opens the dialog with text to copy.
44
+ #
45
+ # Initializes selection to <tt>:yes</tt> and sets active to true.
46
+ #
47
+ # [text] String text to show and copy
48
+ #
49
+ # === Example
50
+ #
51
+ # dialog.open("#FF0000")
52
+ # dialog.active? # => true
53
+ def open(text)
54
+ @text = text
55
+ @selected = :yes
56
+ @active = true
57
+ end
58
+
59
+ # Closes the dialog and deactivates it.
60
+ def close
61
+ @active = false
62
+ end
63
+
64
+ # True if the dialog is currently open and visible.
65
+ def active?
66
+ @active
67
+ end
68
+
69
+ # Renders the dialog into the given area.
70
+ #
71
+ # Shows the text to copy, Yes/No buttons with current selection highlighted,
72
+ # and keyboard instructions.
73
+ #
74
+ # [tui] Session or TUI factory object
75
+ # [frame] Frame object from RatatuiRuby.draw block
76
+ # [area] Rect area to draw into
77
+ #
78
+ # === Example
79
+ #
80
+ # dialog.render(tui, frame, center_area)
81
+ def render(tui, frame, area)
82
+ @area = area
83
+ widget = build_widget(tui)
84
+ frame.render_widget(widget, area)
85
+ end
86
+
87
+ # Processes a keyboard event and updates selection or closes the dialog.
88
+ #
89
+ # Returns:
90
+ # - `:consumed` when the event was handled
91
+ # - `nil` when the event was ignored or dialog is inactive
92
+ #
93
+ # [event] Event from RatatuiRuby.poll_event
94
+ #
95
+ # === Example
96
+ #
97
+ # result = dialog.handle_event(event)
98
+ def handle_event(event)
99
+ return nil unless @active
100
+
101
+ case event
102
+ in { type: :key, code: "left" } | { type: :key, code: "h" }
103
+ @selected = :yes
104
+ :consumed
105
+ in { type: :key, code: "right" } | { type: :key, code: "l" }
106
+ @selected = :no
107
+ :consumed
108
+ in { type: :key, code: "enter" }
109
+ if @selected == :yes
110
+ @clipboard.copy(@text)
111
+ end
112
+ @active = false
113
+ :consumed
114
+ in { type: :key, code: "y" }
115
+ @clipboard.copy(@text)
116
+ @active = false
117
+ :consumed
118
+ in { type: :key, code: "n" }
119
+ @active = false
120
+ :consumed
121
+ else
122
+ nil
123
+ end
124
+ end
125
+
126
+ private def build_widget(tui)
127
+ yes_style = if @selected == :yes
128
+ tui.style(bg: :cyan, fg: :black, modifiers: [:bold])
129
+ else
130
+ tui.style(fg: :gray)
131
+ end
132
+
133
+ no_style = if @selected == :no
134
+ tui.style(bg: :cyan, fg: :black, modifiers: [:bold])
135
+ else
136
+ tui.style(fg: :gray)
137
+ end
138
+
139
+ tui.block(
140
+ title: "Copy to Clipboard",
141
+ borders: [:all],
142
+ border_type: :rounded,
143
+ style: tui.style(bg: :black, fg: :white),
144
+ children: [
145
+ tui.paragraph(
146
+ text: [
147
+ tui.text_line(spans: [
148
+ tui.text_span(content: "Copy #{@text}?", style: tui.style(fg: :white)),
149
+ ]),
150
+ tui.text_line(spans: []),
151
+ tui.text_line(spans: [
152
+ tui.text_span(content: "[", style: tui.style(fg: :white)),
153
+ tui.text_span(content: "Yes", style: yes_style),
154
+ tui.text_span(content: "] [", style: tui.style(fg: :white)),
155
+ tui.text_span(content: "No", style: no_style),
156
+ tui.text_span(content: "]", style: tui.style(fg: :white)),
157
+ ]),
158
+ tui.text_line(spans: [
159
+ tui.text_span(content: "Use ←/→ or h/l to select, Enter to confirm", style: tui.style(fg: :gray, modifiers: [:italic])),
160
+ ]),
161
+ ]
162
+ ),
163
+ ]
164
+ )
165
+ end
166
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # A self-contained component displaying export formats for a color.
7
+ #
8
+ # Users need to copy color values in different formats (HEX, RGB, HSL).
9
+ # This component renders the export section and detects clicks on itself.
10
+ #
11
+ # === Component Contract
12
+ #
13
+ # - `render(tui, frame, area, palette:)`: Draws the export formats; stores `area` for hit testing
14
+ # - `handle_event(event) -> Symbol | nil`: Returns `:copy_requested` when clicked
15
+ #
16
+ # === Example
17
+ #
18
+ # export_pane = ExportPane.new
19
+ # export_pane.render(tui, frame, area, palette: palette)
20
+ #
21
+ # result = export_pane.handle_event(event)
22
+ # if result == :copy_requested && palette.main
23
+ # dialog.open(palette.main.hex)
24
+ # end
25
+ class ExportPane
26
+ def initialize
27
+ @area = nil
28
+ end
29
+
30
+ # The cached render area, for hit testing.
31
+ attr_reader :area
32
+
33
+ # Renders the export formats section into the given area.
34
+ #
35
+ # Shows HEX, RGB, and HSL values for the current color. If no color is set,
36
+ # displays a placeholder message.
37
+ #
38
+ # [tui] Session or TUI factory object
39
+ # [frame] Frame object from RatatuiRuby.draw block
40
+ # [area] Rect area to draw into
41
+ # [palette] Palette object containing the color to display
42
+ #
43
+ # === Example
44
+ #
45
+ # export_pane.render(tui, frame, export_area, palette: palette)
46
+ def render(tui, frame, area, palette:)
47
+ @area = area
48
+ widget = build_widget(tui, palette)
49
+ frame.render_widget(widget, area)
50
+ end
51
+
52
+ # Processes a mouse event and returns a signal if clicked.
53
+ #
54
+ # Returns:
55
+ # - `:copy_requested` when the pane is clicked (caller should open copy dialog)
56
+ # - `nil` when the event was ignored or outside the area
57
+ #
58
+ # [event] Event from RatatuiRuby.poll_event
59
+ #
60
+ # === Example
61
+ #
62
+ # result = export_pane.handle_event(event)
63
+ # if result == :copy_requested
64
+ # dialog.open(palette.main.hex)
65
+ # end
66
+ def handle_event(event)
67
+ case event
68
+ in { type: :mouse, kind: "down", button: "left", x:, y: }
69
+ if @area&.contains?(x, y)
70
+ :copy_requested
71
+ end
72
+ else
73
+ nil
74
+ end
75
+ end
76
+
77
+ private def build_widget(tui, palette)
78
+ if palette.main.nil?
79
+ tui.block(
80
+ title: "Export Formats",
81
+ borders: [:all],
82
+ children: [
83
+ tui.paragraph(
84
+ text: tui.text_line(spans: [
85
+ tui.text_span(content: "Enter a color to see formats"),
86
+ ])
87
+ ),
88
+ ]
89
+ )
90
+ else
91
+ build_color_widget(tui, palette.main)
92
+ end
93
+ end
94
+
95
+ private def build_color_widget(tui, color)
96
+ hex = color.hex
97
+ rgb = color.rgb
98
+ hsl = color.hsl_string
99
+ text_color = color.contrasting_text_color
100
+ bg_style = tui.style(bg: hex, fg: text_color)
101
+
102
+ tui.block(
103
+ title: "Export Formats",
104
+ borders: [:all],
105
+ style: bg_style,
106
+ children: [
107
+ tui.paragraph(
108
+ text: [
109
+ tui.text_line(spans: [
110
+ tui.text_span(content: "HEX: ", style: bg_style),
111
+ tui.text_span(content: hex, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
112
+ ]),
113
+ tui.text_line(spans: [
114
+ tui.text_span(content: "RGB: ", style: bg_style),
115
+ tui.text_span(content: rgb, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
116
+ ]),
117
+ tui.text_line(spans: [
118
+ tui.text_span(content: "HSL: ", style: bg_style),
119
+ tui.text_span(content: hsl, style: tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
120
+ ]),
121
+ ]
122
+ ),
123
+ ]
124
+ )
125
+ end
126
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # A single color variant with label and styling information.
7
+ #
8
+ # Color palettes need to show individual colors with labels (Main, Shade, Tint,
9
+ # Complement). Bundling a color's hex code, text color, and frame color together
10
+ # is natural—they're always used as a set.
11
+ #
12
+ # This value object pairs a color with its metadata and rendering styles.
13
+ #
14
+ # Use it to represent colors in a palette or harmony.
15
+ #
16
+ # === Attributes
17
+ #
18
+ # [label] String label for this color variant
19
+ # [hex] String hex color code
20
+ # [text_color] Symbol (:white or :black) for readable text
21
+ # [frame_color] String background color for the swatch frame
22
+ #
23
+ # === Example
24
+ #
25
+ # harmony = Harmony.new(
26
+ # label: "Main",
27
+ # hex: "#FF0000",
28
+ # text_color: :white,
29
+ # frame_color: "#000000"
30
+ # )
31
+ Harmony = Data.define(:label, :hex, :text_color, :frame_color) do
32
+ # Renders a 4-line color swatch for display in a TUI Block.
33
+ #
34
+ # Produces a visual representation: a 7-character-wide box with the color
35
+ # centered and the hex code below.
36
+ #
37
+ # [tui] Session or TUI factory object
38
+ #
39
+ # === Example
40
+ #
41
+ # harmony = Harmony.new(...)
42
+ # lines = harmony.color_swatch_lines(tui)
43
+ # # => [TextLine, TextLine, TextLine, TextLine]
44
+ def color_swatch_lines(tui)
45
+ [
46
+ tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
47
+ tui.text_line(spans: [
48
+ *Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
49
+ tui.text_span(content: " ", style: tui.style(bg: hex, fg: text_color)),
50
+ *Array.new(3) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) },
51
+ ]),
52
+ tui.text_line(spans: Array.new(7) { tui.text_span(content: " ", style: tui.style(bg: frame_color)) }),
53
+ tui.text_line(spans: [tui.text_span(content: hex, style: tui.style(fg: :white))]),
54
+ ]
55
+ end
56
+ end