termcast 1.3.48 → 1.3.50

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 (255) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +12 -0
  3. package/dist/build.js.map +1 -1
  4. package/dist/cli.js +5 -40
  5. package/dist/cli.js.map +1 -1
  6. package/dist/colors.d.ts +7 -7
  7. package/dist/colors.js +7 -7
  8. package/dist/compile.d.ts +6 -1
  9. package/dist/compile.d.ts.map +1 -1
  10. package/dist/compile.js +45 -26
  11. package/dist/compile.js.map +1 -1
  12. package/dist/components/actions.js +1 -1
  13. package/dist/components/actions.js.map +1 -1
  14. package/dist/components/bar-chart.d.ts +38 -0
  15. package/dist/components/bar-chart.d.ts.map +1 -0
  16. package/dist/components/bar-chart.js +158 -0
  17. package/dist/components/bar-chart.js.map +1 -0
  18. package/dist/components/bar-graph.d.ts +41 -0
  19. package/dist/components/bar-graph.d.ts.map +1 -0
  20. package/dist/components/bar-graph.js +95 -0
  21. package/dist/components/bar-graph.js.map +1 -0
  22. package/dist/components/detail.d.ts.map +1 -1
  23. package/dist/components/detail.js +5 -7
  24. package/dist/components/detail.js.map +1 -1
  25. package/dist/components/footer.d.ts.map +1 -1
  26. package/dist/components/footer.js +8 -9
  27. package/dist/components/footer.js.map +1 -1
  28. package/dist/components/form/date-picker.d.ts.map +1 -1
  29. package/dist/components/form/date-picker.js +7 -1
  30. package/dist/components/form/date-picker.js.map +1 -1
  31. package/dist/components/form/dropdown.d.ts.map +1 -1
  32. package/dist/components/form/dropdown.js +10 -2
  33. package/dist/components/form/dropdown.js.map +1 -1
  34. package/dist/components/form/index.d.ts.map +1 -1
  35. package/dist/components/form/index.js +4 -5
  36. package/dist/components/form/index.js.map +1 -1
  37. package/dist/components/form/use-form-navigation.d.ts.map +1 -1
  38. package/dist/components/form/use-form-navigation.js +6 -0
  39. package/dist/components/form/use-form-navigation.js.map +1 -1
  40. package/dist/components/graph.d.ts +111 -0
  41. package/dist/components/graph.d.ts.map +1 -0
  42. package/dist/components/graph.js +392 -0
  43. package/dist/components/graph.js.map +1 -0
  44. package/dist/components/icon.js +5 -5
  45. package/dist/components/icon.js.map +1 -1
  46. package/dist/components/list.d.ts +53 -5
  47. package/dist/components/list.d.ts.map +1 -1
  48. package/dist/components/list.js +125 -71
  49. package/dist/components/list.js.map +1 -1
  50. package/dist/components/loading-bar.js +3 -3
  51. package/dist/components/loading-bar.js.map +1 -1
  52. package/dist/components/loading-text.d.ts +1 -1
  53. package/dist/components/loading-text.d.ts.map +1 -1
  54. package/dist/components/loading-text.js +3 -1
  55. package/dist/components/loading-text.js.map +1 -1
  56. package/dist/components/metadata.js +2 -2
  57. package/dist/components/metadata.js.map +1 -1
  58. package/dist/components/row.d.ts +10 -0
  59. package/dist/components/row.d.ts.map +1 -0
  60. package/dist/components/row.js +12 -0
  61. package/dist/components/row.js.map +1 -0
  62. package/dist/components/table.d.ts +57 -0
  63. package/dist/components/table.d.ts.map +1 -0
  64. package/dist/components/table.js +365 -0
  65. package/dist/components/table.js.map +1 -0
  66. package/dist/descendants.js +13 -13
  67. package/dist/descendants.js.map +1 -1
  68. package/dist/examples/bar-graph-weekly.d.ts +2 -0
  69. package/dist/examples/bar-graph-weekly.d.ts.map +1 -0
  70. package/dist/examples/bar-graph-weekly.js +95 -0
  71. package/dist/examples/bar-graph-weekly.js.map +1 -0
  72. package/dist/examples/components-weird-places.d.ts +2 -0
  73. package/dist/examples/components-weird-places.d.ts.map +1 -0
  74. package/dist/examples/components-weird-places.js +46 -0
  75. package/dist/examples/components-weird-places.js.map +1 -0
  76. package/dist/examples/graph-bar-chart.d.ts +2 -0
  77. package/dist/examples/graph-bar-chart.d.ts.map +1 -0
  78. package/dist/examples/graph-bar-chart.js +270 -0
  79. package/dist/examples/graph-bar-chart.js.map +1 -0
  80. package/dist/examples/graph-multi-series.d.ts +2 -0
  81. package/dist/examples/graph-multi-series.d.ts.map +1 -0
  82. package/dist/examples/graph-multi-series.js +23 -0
  83. package/dist/examples/graph-multi-series.js.map +1 -0
  84. package/dist/examples/graph-polymarket.d.ts +2 -0
  85. package/dist/examples/graph-polymarket.d.ts.map +1 -0
  86. package/dist/examples/graph-polymarket.js +109 -0
  87. package/dist/examples/graph-polymarket.js.map +1 -0
  88. package/dist/examples/graph-row.d.ts +2 -0
  89. package/dist/examples/graph-row.d.ts.map +1 -0
  90. package/dist/examples/graph-row.js +226 -0
  91. package/dist/examples/graph-row.js.map +1 -0
  92. package/dist/examples/graph-styles.d.ts +2 -0
  93. package/dist/examples/graph-styles.d.ts.map +1 -0
  94. package/dist/examples/graph-styles.js +316 -0
  95. package/dist/examples/graph-styles.js.map +1 -0
  96. package/dist/examples/list-accessory-table.d.ts +2 -0
  97. package/dist/examples/list-accessory-table.d.ts.map +1 -0
  98. package/dist/examples/list-accessory-table.js +46 -0
  99. package/dist/examples/list-accessory-table.js.map +1 -0
  100. package/dist/examples/list-item-accessories.d.ts +2 -0
  101. package/dist/examples/list-item-accessories.d.ts.map +1 -0
  102. package/dist/examples/list-item-accessories.js +27 -0
  103. package/dist/examples/list-item-accessories.js.map +1 -0
  104. package/dist/examples/list-no-actions.d.ts +2 -0
  105. package/dist/examples/list-no-actions.d.ts.map +1 -0
  106. package/dist/examples/list-no-actions.js +7 -0
  107. package/dist/examples/list-no-actions.js.map +1 -0
  108. package/dist/examples/simple-detail-table.d.ts +2 -0
  109. package/dist/examples/simple-detail-table.d.ts.map +1 -0
  110. package/dist/examples/simple-detail-table.js +45 -0
  111. package/dist/examples/simple-detail-table.js.map +1 -0
  112. package/dist/examples/simple-graph.d.ts +2 -0
  113. package/dist/examples/simple-graph.d.ts.map +1 -0
  114. package/dist/examples/simple-graph.js +32 -0
  115. package/dist/examples/simple-graph.js.map +1 -0
  116. package/dist/examples/simple-table-wrap.d.ts +2 -0
  117. package/dist/examples/simple-table-wrap.d.ts.map +1 -0
  118. package/dist/examples/simple-table-wrap.js +37 -0
  119. package/dist/examples/simple-table-wrap.js.map +1 -0
  120. package/dist/examples/table-edge-cases.d.ts +2 -0
  121. package/dist/examples/table-edge-cases.d.ts.map +1 -0
  122. package/dist/examples/table-edge-cases.js +70 -0
  123. package/dist/examples/table-edge-cases.js.map +1 -0
  124. package/dist/examples/table-flex-grow.d.ts +2 -0
  125. package/dist/examples/table-flex-grow.d.ts.map +1 -0
  126. package/dist/examples/table-flex-grow.js +18 -0
  127. package/dist/examples/table-flex-grow.js.map +1 -0
  128. package/dist/extensions/dev.d.ts.map +1 -1
  129. package/dist/extensions/dev.js +5 -1
  130. package/dist/extensions/dev.js.map +1 -1
  131. package/dist/globals.d.ts +1 -0
  132. package/dist/globals.d.ts.map +1 -1
  133. package/dist/globals.js +2 -0
  134. package/dist/globals.js.map +1 -1
  135. package/dist/index.d.ts +10 -0
  136. package/dist/index.d.ts.map +1 -1
  137. package/dist/index.js +10 -0
  138. package/dist/index.js.map +1 -1
  139. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  140. package/dist/internal/date-picker-widget.js +4 -0
  141. package/dist/internal/date-picker-widget.js.map +1 -1
  142. package/dist/internal/providers.d.ts.map +1 -1
  143. package/dist/internal/providers.js +1 -3
  144. package/dist/internal/providers.js.map +1 -1
  145. package/dist/markdown-utils.d.ts +22 -1
  146. package/dist/markdown-utils.d.ts.map +1 -1
  147. package/dist/markdown-utils.js +66 -1
  148. package/dist/markdown-utils.js.map +1 -1
  149. package/dist/opentui.d.ts +4 -0
  150. package/dist/opentui.d.ts.map +1 -0
  151. package/dist/opentui.js +3 -0
  152. package/dist/opentui.js.map +1 -0
  153. package/dist/release.d.ts +2 -1
  154. package/dist/release.d.ts.map +1 -1
  155. package/dist/release.js +2 -1
  156. package/dist/release.js.map +1 -1
  157. package/dist/state.d.ts +1 -0
  158. package/dist/state.d.ts.map +1 -1
  159. package/dist/state.js +1 -1
  160. package/dist/state.js.map +1 -1
  161. package/dist/theme.d.ts +1 -0
  162. package/dist/theme.d.ts.map +1 -1
  163. package/dist/theme.js +13 -0
  164. package/dist/theme.js.map +1 -1
  165. package/dist/themes/nerv.json +227 -0
  166. package/dist/themes/termcast.json +72 -71
  167. package/dist/themes.d.ts +2 -1
  168. package/dist/themes.d.ts.map +1 -1
  169. package/dist/themes.js +7 -5
  170. package/dist/themes.js.map +1 -1
  171. package/dist/utils.d.ts.map +1 -1
  172. package/dist/utils.js +3 -0
  173. package/dist/utils.js.map +1 -1
  174. package/package.json +13 -5
  175. package/src/build.tsx +13 -0
  176. package/src/cli.tsx +5 -49
  177. package/src/colors.tsx +7 -7
  178. package/src/compile.tsx +52 -29
  179. package/src/components/actions.tsx +1 -1
  180. package/src/components/bar-chart.tsx +271 -0
  181. package/src/components/bar-graph.tsx +214 -0
  182. package/src/components/detail.tsx +7 -8
  183. package/src/components/footer.tsx +14 -15
  184. package/src/components/form/date-picker.tsx +9 -0
  185. package/src/components/form/dropdown.tsx +13 -3
  186. package/src/components/form/index.tsx +4 -6
  187. package/src/components/form/use-form-navigation.tsx +6 -0
  188. package/src/components/graph.tsx +506 -0
  189. package/src/components/icon.tsx +5 -5
  190. package/src/components/list.tsx +210 -102
  191. package/src/components/loading-bar.tsx +3 -3
  192. package/src/components/loading-text.tsx +4 -2
  193. package/src/components/metadata.tsx +2 -2
  194. package/src/components/row.tsx +31 -0
  195. package/src/components/table.tsx +511 -0
  196. package/src/descendants.tsx +13 -13
  197. package/src/examples/action-shortcut.vitest.tsx +1 -1
  198. package/src/examples/actions-context.vitest.tsx +1 -1
  199. package/src/examples/bar-graph-weekly.tsx +264 -0
  200. package/src/examples/bar-graph-weekly.vitest.tsx +275 -0
  201. package/src/examples/detail-metadata-showcase.vitest.tsx +8 -8
  202. package/src/examples/form-basic.vitest.tsx +239 -0
  203. package/src/examples/form-dropdown.vitest.tsx +29 -29
  204. package/src/examples/form-tagpicker.vitest.tsx +27 -27
  205. package/src/examples/github.vitest.tsx +4 -4
  206. package/src/examples/graph-bar-chart.tsx +408 -0
  207. package/src/examples/graph-bar-chart.vitest.tsx +283 -0
  208. package/src/examples/graph-multi-series.tsx +36 -0
  209. package/src/examples/graph-multi-series.vitest.tsx +89 -0
  210. package/src/examples/graph-polymarket.tsx +182 -0
  211. package/src/examples/graph-polymarket.vitest.tsx +130 -0
  212. package/src/examples/graph-row.tsx +347 -0
  213. package/src/examples/graph-row.vitest.tsx +295 -0
  214. package/src/examples/graph-styles.tsx +457 -0
  215. package/src/examples/graph-styles.vitest.tsx +322 -0
  216. package/src/examples/list-accessory-table.tsx +77 -0
  217. package/src/examples/list-detail-metadata.vitest.tsx +21 -21
  218. package/src/examples/list-dropdown-default.vitest.tsx +12 -12
  219. package/src/examples/list-item-accessories.tsx +106 -0
  220. package/src/examples/list-item-accessories.vitest.tsx +115 -0
  221. package/src/examples/list-no-actions.tsx +18 -0
  222. package/src/examples/list-no-actions.vitest.tsx +97 -0
  223. package/src/examples/list-spacing-mode.vitest.tsx +6 -6
  224. package/src/examples/list-with-detail.vitest.tsx +92 -92
  225. package/src/examples/list-with-dropdown.vitest.tsx +49 -6
  226. package/src/examples/list-with-sections.vitest.tsx +61 -56
  227. package/src/examples/simple-detail-markdown.vitest.tsx +21 -17
  228. package/src/examples/simple-detail-table.tsx +65 -0
  229. package/src/examples/simple-detail-table.vitest.tsx +200 -0
  230. package/src/examples/simple-graph.tsx +51 -0
  231. package/src/examples/simple-graph.vitest.tsx +124 -0
  232. package/src/examples/simple-grid.vitest.tsx +3 -3
  233. package/src/examples/simple-list-search.vitest.tsx +65 -0
  234. package/src/examples/simple-navigation.vitest.tsx +3 -3
  235. package/src/examples/simple-table-wrap.tsx +55 -0
  236. package/src/examples/simple-table-wrap.vitest.tsx +91 -0
  237. package/src/examples/store.vitest.tsx +1 -1
  238. package/src/examples/table-edge-cases.tsx +72 -0
  239. package/src/examples/table-edge-cases.vitest.tsx +307 -0
  240. package/src/examples/table-flex-grow.tsx +53 -0
  241. package/src/examples/table-flex-grow.vitest.tsx +124 -0
  242. package/src/extensions/dev.tsx +7 -1
  243. package/src/globals.ts +3 -0
  244. package/src/index.tsx +31 -0
  245. package/src/internal/date-picker-widget.tsx +4 -0
  246. package/src/internal/providers.tsx +1 -4
  247. package/src/markdown-utils.tsx +82 -1
  248. package/src/opentui.tsx +5 -0
  249. package/src/release.tsx +3 -0
  250. package/src/state.tsx +2 -1
  251. package/src/theme.tsx +14 -0
  252. package/src/themes/nerv.json +231 -0
  253. package/src/themes/termcast.json +75 -71
  254. package/src/themes.ts +8 -5
  255. package/src/utils.tsx +4 -0
@@ -0,0 +1,511 @@
1
+ // Standalone table renderable with borderless design, header background, and alternating row stripes.
2
+ // Header uses inverted markup.heading colors (fg becomes box bg, bg becomes text fg).
3
+ // Odd data rows use conceal.bg for stripe; even rows are transparent.
4
+ //
5
+ // Ported from opentui core. Imports now reference @opentui/core.
6
+ // Registered as <table-view> JSX element via extend().
7
+ // React wrapper <Table> at the bottom provides a declarative API.
8
+ import {
9
+ Renderable,
10
+ type RenderableOptions,
11
+ type RenderContext,
12
+ SyntaxStyle,
13
+ type StyleDefinition,
14
+ StyledText,
15
+ type TextChunk,
16
+ createTextAttributes,
17
+ TextRenderable,
18
+ BoxRenderable,
19
+ type OptimizedBuffer,
20
+ } from '@opentui/core'
21
+ import { extend } from '@opentui/react'
22
+ import React from 'react'
23
+ import { getMarkdownSyntaxStyle } from 'termcast/src/theme'
24
+ import { useStore } from 'termcast/src/state'
25
+ import { parseInlineMarkdown } from 'termcast/src/markdown-utils'
26
+
27
+ export type TableCellContent = string | StyledText
28
+
29
+ export interface TableRenderableOptions extends RenderableOptions<TableRenderable> {
30
+ headers?: TableCellContent[]
31
+ rows?: TableCellContent[][]
32
+ syntaxStyle?: SyntaxStyle
33
+ /** When true, cell text wraps instead of being truncated to one line. Default false. */
34
+ wrapText?: boolean
35
+ }
36
+
37
+ export class TableRenderable extends Renderable {
38
+ private _headers: TableCellContent[] = []
39
+ private _rows: TableCellContent[][] = []
40
+ private _syntaxStyle?: SyntaxStyle
41
+ private _wrapText: boolean = false
42
+ private _tableDirty = true
43
+ private _tableStructureDirty = true
44
+
45
+ constructor(ctx: RenderContext, options: TableRenderableOptions) {
46
+ super(ctx, {
47
+ ...options,
48
+ // Direction set in rebuild() based on wrapText:
49
+ // - wrapText=false: 'row' (column-based, content-sized columns)
50
+ // - wrapText=true: 'column' (row-based, synchronized row heights)
51
+ flexDirection: options.wrapText ? 'column' : 'row',
52
+ })
53
+
54
+ this._headers = options.headers ?? []
55
+ this._rows = options.rows ?? []
56
+ this._syntaxStyle = options.syntaxStyle
57
+ this._wrapText = options.wrapText ?? false
58
+
59
+ // Build eagerly on construction (safe since no existing children)
60
+ this._tableDirty = false
61
+ this._tableStructureDirty = false
62
+ this.rebuild()
63
+ }
64
+
65
+ get headers(): TableCellContent[] {
66
+ return this._headers
67
+ }
68
+
69
+ set headers(value: TableCellContent[]) {
70
+ const structureChanged = value.length !== this._headers.length
71
+ this._headers = value
72
+ this._tableDirty = true
73
+ if (structureChanged) this._tableStructureDirty = true
74
+ this.requestRender()
75
+ }
76
+
77
+ get rows(): TableCellContent[][] {
78
+ return this._rows
79
+ }
80
+
81
+ set rows(value: TableCellContent[][]) {
82
+ const structureChanged = value.length !== this._rows.length
83
+ this._rows = value
84
+ this._tableDirty = true
85
+ if (structureChanged) this._tableStructureDirty = true
86
+ this.requestRender()
87
+ }
88
+
89
+ get syntaxStyle(): SyntaxStyle | undefined {
90
+ return this._syntaxStyle
91
+ }
92
+
93
+ set syntaxStyle(value: SyntaxStyle | undefined) {
94
+ this._syntaxStyle = value
95
+ this._tableDirty = true
96
+ this.requestRender()
97
+ }
98
+
99
+ get wrapText(): boolean {
100
+ return this._wrapText
101
+ }
102
+
103
+ set wrapText(value: boolean) {
104
+ if (this._wrapText !== value) {
105
+ this._wrapText = value
106
+ this._tableDirty = true
107
+ this._tableStructureDirty = true
108
+ this.requestRender()
109
+ }
110
+ }
111
+
112
+ private getStyle(group: string): StyleDefinition | undefined {
113
+ if (!this._syntaxStyle) return undefined
114
+ let style = this._syntaxStyle.getStyle(group)
115
+ if (!style && group.includes('.')) {
116
+ const baseName = group.split('.')[0]
117
+ style = this._syntaxStyle.getStyle(baseName)
118
+ }
119
+ return style
120
+ }
121
+
122
+ private toStyledText(content: TableCellContent): StyledText {
123
+ if (content instanceof StyledText) return content
124
+ const defaultStyle = this.getStyle('default')
125
+ const chunk: TextChunk = {
126
+ __isChunk: true,
127
+ text: content,
128
+ fg: defaultStyle?.fg,
129
+ bg: defaultStyle?.bg,
130
+ attributes: defaultStyle
131
+ ? createTextAttributes({
132
+ bold: defaultStyle.bold,
133
+ italic: defaultStyle.italic,
134
+ underline: defaultStyle.underline,
135
+ dim: defaultStyle.dim,
136
+ })
137
+ : 0,
138
+ }
139
+ return new StyledText([chunk])
140
+ }
141
+
142
+ private styledHeaderChunks(content: StyledText, headingStyle: StyleDefinition | undefined, headerFg: StyleDefinition['fg']): StyledText {
143
+ if (!headerFg) return content
144
+ const styledChunks = content.chunks.map((chunk) => ({
145
+ ...chunk,
146
+ fg: headerFg,
147
+ bg: undefined,
148
+ attributes: headingStyle
149
+ ? createTextAttributes({
150
+ bold: headingStyle.bold,
151
+ italic: headingStyle.italic,
152
+ underline: headingStyle.underline,
153
+ dim: headingStyle.dim,
154
+ })
155
+ : chunk.attributes,
156
+ }))
157
+ return new StyledText(styledChunks)
158
+ }
159
+
160
+ private rebuild(): void {
161
+ // Remove all existing children (copy array since remove mutates it)
162
+ const children = [...(this as any)._childrenInLayoutOrder] as Renderable[]
163
+ for (const child of children) {
164
+ this.remove(child.id)
165
+ }
166
+
167
+ const colCount = this._headers.length
168
+ if (colCount === 0 || this._rows.length === 0) return
169
+
170
+ const headingStyle =
171
+ this.getStyle('markup.heading') || this.getStyle('default')
172
+ const concealStyle = this.getStyle('conceal')
173
+ const headerBg = headingStyle?.fg
174
+ const headerFg = headingStyle?.bg
175
+ const stripeBg = concealStyle?.bg
176
+
177
+ // Update flex direction based on wrapText mode
178
+ this.flexDirection = this._wrapText ? 'column' : 'row'
179
+
180
+ if (this._wrapText) {
181
+ this.rebuildRowBased(colCount, headingStyle, headerBg, headerFg, stripeBg)
182
+ } else {
183
+ this.rebuildColumnBased(colCount, headingStyle, headerBg, headerFg, stripeBg)
184
+ }
185
+ }
186
+
187
+ // Column-based: each column is a vertical stack. Content-sized, compact.
188
+ // Used when wrapText=false (all cells are height:1, no alignment issues).
189
+ private rebuildColumnBased(
190
+ colCount: number,
191
+ headingStyle: StyleDefinition | undefined,
192
+ headerBg: StyleDefinition['fg'],
193
+ headerFg: StyleDefinition['fg'],
194
+ stripeBg: StyleDefinition['fg'],
195
+ ): void {
196
+ for (let col = 0; col < colCount; col++) {
197
+ const columnBox = new BoxRenderable(this.ctx, {
198
+ id: `${this.id}-col-${col}`,
199
+ flexDirection: 'column',
200
+ })
201
+
202
+ const headerContent = this._headers[col] ?? ''
203
+ let headerStyledText = this.toStyledText(headerContent)
204
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
205
+
206
+ const headerBox = new BoxRenderable(this.ctx, {
207
+ id: `${this.id}-col-${col}-header-box`,
208
+ backgroundColor: headerBg,
209
+ })
210
+ headerBox.add(
211
+ new TextRenderable(this.ctx, {
212
+ id: `${this.id}-col-${col}-header`,
213
+ content: headerStyledText,
214
+ height: 1,
215
+ overflow: 'hidden',
216
+ paddingLeft: 1,
217
+ paddingRight: 1,
218
+ }),
219
+ )
220
+ columnBox.add(headerBox)
221
+
222
+ for (let row = 0; row < this._rows.length; row++) {
223
+ const cell = this._rows[row]?.[col] ?? ''
224
+ const cellContent = this.toStyledText(cell)
225
+ const isOddRow = row % 2 === 1
226
+
227
+ const cellBox = new BoxRenderable(this.ctx, {
228
+ id: `${this.id}-col-${col}-row-${row}-box`,
229
+ backgroundColor: isOddRow ? stripeBg : undefined,
230
+ })
231
+ cellBox.add(
232
+ new TextRenderable(this.ctx, {
233
+ id: `${this.id}-col-${col}-row-${row}`,
234
+ content: cellContent,
235
+ height: 1,
236
+ overflow: 'hidden',
237
+ paddingLeft: 1,
238
+ paddingRight: 1,
239
+ }),
240
+ )
241
+ columnBox.add(cellBox)
242
+ }
243
+
244
+ this.add(columnBox)
245
+ }
246
+ }
247
+
248
+ // Row-based: each row is a horizontal box. Equal-width columns but
249
+ // cells in the same row share height, so wrapped text aligns correctly.
250
+ private rebuildRowBased(
251
+ colCount: number,
252
+ headingStyle: StyleDefinition | undefined,
253
+ headerBg: StyleDefinition['fg'],
254
+ headerFg: StyleDefinition['fg'],
255
+ stripeBg: StyleDefinition['fg'],
256
+ ): void {
257
+ const headerRow = new BoxRenderable(this.ctx, {
258
+ id: `${this.id}-header-row`,
259
+ flexDirection: 'row',
260
+ backgroundColor: headerBg,
261
+ })
262
+ for (let col = 0; col < colCount; col++) {
263
+ const headerContent = this._headers[col] ?? ''
264
+ let headerStyledText = this.toStyledText(headerContent)
265
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
266
+
267
+ headerRow.add(
268
+ new TextRenderable(this.ctx, {
269
+ id: `${this.id}-header-${col}`,
270
+ content: headerStyledText,
271
+ flexGrow: 1,
272
+ flexBasis: 0,
273
+ paddingLeft: 1,
274
+ paddingRight: 1,
275
+ }),
276
+ )
277
+ }
278
+ this.add(headerRow)
279
+
280
+ for (let row = 0; row < this._rows.length; row++) {
281
+ const isOddRow = row % 2 === 1
282
+ const rowBox = new BoxRenderable(this.ctx, {
283
+ id: `${this.id}-row-${row}`,
284
+ flexDirection: 'row',
285
+ backgroundColor: isOddRow ? stripeBg : undefined,
286
+ })
287
+
288
+ for (let col = 0; col < colCount; col++) {
289
+ const cell = this._rows[row]?.[col] ?? ''
290
+ const cellContent = this.toStyledText(cell)
291
+
292
+ rowBox.add(
293
+ new TextRenderable(this.ctx, {
294
+ id: `${this.id}-row-${row}-col-${col}`,
295
+ content: cellContent,
296
+ flexGrow: 1,
297
+ flexBasis: 0,
298
+ paddingLeft: 1,
299
+ paddingRight: 1,
300
+ }),
301
+ )
302
+ }
303
+
304
+ this.add(rowBox)
305
+ }
306
+ }
307
+
308
+ private updateInPlace(): void {
309
+ if (this._wrapText) {
310
+ this.updateInPlaceRowBased()
311
+ } else {
312
+ this.updateInPlaceColumnBased()
313
+ }
314
+ }
315
+
316
+ private updateInPlaceColumnBased(): void {
317
+ const headingStyle =
318
+ this.getStyle('markup.heading') || this.getStyle('default')
319
+ const concealStyle = this.getStyle('conceal')
320
+ const headerBg = headingStyle?.fg
321
+ const headerFg = headingStyle?.bg
322
+ const stripeBg = concealStyle?.bg
323
+
324
+ const columns = (this as any)._childrenInLayoutOrder as Renderable[]
325
+ const colCount = this._headers.length
326
+
327
+ for (let col = 0; col < colCount; col++) {
328
+ const columnBox = columns[col]
329
+ if (!columnBox) continue
330
+
331
+ const columnChildren = (columnBox as any)._childrenInLayoutOrder as Renderable[]
332
+
333
+ // Update header
334
+ const headerBox = columnChildren[0]
335
+ if (headerBox instanceof BoxRenderable) {
336
+ headerBox.backgroundColor = headerBg ?? 'transparent'
337
+ const headerChildren = (headerBox as any)._childrenInLayoutOrder as Renderable[]
338
+ const headerText = headerChildren[0]
339
+ if (headerText instanceof TextRenderable) {
340
+ const headerContent = this._headers[col] ?? ''
341
+ let headerStyledText = this.toStyledText(headerContent)
342
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
343
+ headerText.content = headerStyledText
344
+ }
345
+ }
346
+
347
+ // Update data rows
348
+ for (let row = 0; row < this._rows.length; row++) {
349
+ const cellContainer = columnChildren[row + 1]
350
+ if (cellContainer instanceof BoxRenderable) {
351
+ const isOddRow = row % 2 === 1
352
+ cellContainer.backgroundColor = isOddRow && stripeBg ? stripeBg : 'transparent'
353
+ const cellChildren = (cellContainer as any)._childrenInLayoutOrder as Renderable[]
354
+ const cellText = cellChildren[0] as TextRenderable
355
+ if (cellText) {
356
+ const cell = this._rows[row]?.[col] ?? ''
357
+ cellText.content = this.toStyledText(cell)
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ private updateInPlaceRowBased(): void {
365
+ const headingStyle =
366
+ this.getStyle('markup.heading') || this.getStyle('default')
367
+ const concealStyle = this.getStyle('conceal')
368
+ const headerBg = headingStyle?.fg
369
+ const headerFg = headingStyle?.bg
370
+ const stripeBg = concealStyle?.bg
371
+
372
+ const allRows = (this as any)._childrenInLayoutOrder as Renderable[]
373
+ const colCount = this._headers.length
374
+
375
+ const headerRow = allRows[0]
376
+ if (headerRow instanceof BoxRenderable) {
377
+ headerRow.backgroundColor = headerBg ?? 'transparent'
378
+ const headerCells = (headerRow as any)._childrenInLayoutOrder as Renderable[]
379
+ for (let col = 0; col < colCount; col++) {
380
+ const headerText = headerCells[col]
381
+ if (headerText instanceof TextRenderable) {
382
+ const headerContent = this._headers[col] ?? ''
383
+ let headerStyledText = this.toStyledText(headerContent)
384
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
385
+ headerText.content = headerStyledText
386
+ }
387
+ }
388
+ }
389
+
390
+ for (let row = 0; row < this._rows.length; row++) {
391
+ const rowBox = allRows[row + 1]
392
+ if (!(rowBox instanceof BoxRenderable)) continue
393
+
394
+ const isOddRow = row % 2 === 1
395
+ rowBox.backgroundColor = isOddRow && stripeBg ? stripeBg : 'transparent'
396
+
397
+ const rowCells = (rowBox as any)._childrenInLayoutOrder as Renderable[]
398
+ for (let col = 0; col < colCount; col++) {
399
+ const cellText = rowCells[col]
400
+ if (cellText instanceof TextRenderable) {
401
+ const cell = this._rows[row]?.[col] ?? ''
402
+ cellText.content = this.toStyledText(cell)
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {
409
+ if (this._tableDirty) {
410
+ this._tableDirty = false
411
+ if (this._tableStructureDirty) {
412
+ this._tableStructureDirty = false
413
+ this.rebuild()
414
+ } else {
415
+ this.updateInPlace()
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ // ── Register as JSX element ──────────────────────────────────────────
422
+
423
+ extend({ 'table-view': TableRenderable })
424
+
425
+ declare module '@opentui/react' {
426
+ interface OpenTUIComponents {
427
+ 'table-view': typeof TableRenderable
428
+ }
429
+ }
430
+
431
+ // ── React wrapper ────────────────────────────────────────────────────
432
+
433
+ // Layout props picked from RenderableOptions that don't conflict with
434
+ // the table's internal styling (header bg, stripe bg, borderless design).
435
+ type TableLayoutProps = Partial<
436
+ Pick<
437
+ RenderableOptions<TableRenderable>,
438
+ | 'flexGrow'
439
+ | 'flexShrink'
440
+ | 'flexBasis'
441
+ | 'alignSelf'
442
+ | 'padding'
443
+ | 'paddingX'
444
+ | 'paddingY'
445
+ | 'paddingTop'
446
+ | 'paddingRight'
447
+ | 'paddingBottom'
448
+ | 'paddingLeft'
449
+ | 'margin'
450
+ | 'marginX'
451
+ | 'marginY'
452
+ | 'marginTop'
453
+ | 'marginRight'
454
+ | 'marginBottom'
455
+ | 'marginLeft'
456
+ | 'minWidth'
457
+ | 'minHeight'
458
+ | 'maxWidth'
459
+ | 'maxHeight'
460
+ >
461
+ >
462
+
463
+ export interface TableProps extends TableLayoutProps {
464
+ /** Column header labels */
465
+ headers: string[]
466
+ /** Row data – each inner array is one row of cell strings */
467
+ rows: string[][]
468
+ /** When true, cell text wraps instead of being truncated to one line. Default false. */
469
+ wrapText?: boolean
470
+ /** Width (default: 100%) */
471
+ width?: number | 'auto' | `${number}%`
472
+ /** Height (default: auto) */
473
+ height?: number | 'auto' | `${number}%`
474
+ }
475
+
476
+ export function Table(props: TableProps): any {
477
+ const { headers, rows, wrapText, width = '100%', height, ...layoutProps } = props
478
+
479
+ const themeName = useStore((state) => state.currentThemeName)
480
+ const syntaxStyle = React.useMemo(() => {
481
+ return getMarkdownSyntaxStyle()
482
+ }, [themeName])
483
+
484
+ // Parse inline markdown (bold, italic, code, links) in cell strings
485
+ // into StyledText so formatting is preserved in rendered cells.
486
+ const parsedHeaders = React.useMemo(() => {
487
+ return headers.map((h) => {
488
+ return parseInlineMarkdown(h)
489
+ })
490
+ }, [headers])
491
+
492
+ const parsedRows = React.useMemo(() => {
493
+ return rows.map((row) => {
494
+ return row.map((cell) => {
495
+ return parseInlineMarkdown(cell)
496
+ })
497
+ })
498
+ }, [rows])
499
+
500
+ return (
501
+ <table-view
502
+ headers={parsedHeaders}
503
+ rows={parsedRows}
504
+ wrapText={wrapText}
505
+ syntaxStyle={syntaxStyle}
506
+ width={width}
507
+ height={height}
508
+ {...layoutProps}
509
+ />
510
+ )
511
+ }
@@ -52,14 +52,15 @@ export function createDescendants<T = any>() {
52
52
  // For useSyncExternalStore - opt-in reactivity
53
53
  const listeners = React.useRef(new Set<() => void>())
54
54
  const snapshotRef = React.useRef('')
55
- const prevSnapshotRef = React.useRef('')
56
- // Committed map - stable copy of map.current updated only when snapshot changes
55
+ // Version counter ensures subscribers re-render on every commit, not just when
56
+ // items are added/removed. This is needed because committedMap props (like detail,
57
+ // visible) can change without structural changes to the descendant set.
58
+ const versionRef = React.useRef(0)
59
+ // Committed map - stable copy of map.current updated on every commit
57
60
  // This is what useDescendantsRerender returns, since map.current is cleared on every render
58
61
  const committedMapRef = React.useRef<DescendantMap<T>>({})
59
62
 
60
63
  const reset = () => {
61
- // Save previous snapshot before clearing
62
- prevSnapshotRef.current = snapshotRef.current
63
64
  indexCounter.current = 0
64
65
  map.current = {}
65
66
  }
@@ -85,15 +86,14 @@ export function createDescendants<T = any>() {
85
86
  // Must be stable for useSyncExternalStore
86
87
  const getSnapshot = React.useCallback(() => snapshotRef.current, [])
87
88
 
88
- // Called by provider after all children have registered
89
+ // Called by provider after all children have registered (in useLayoutEffect)
89
90
  const updateSnapshot = React.useCallback(() => {
90
- const newSnapshot = Object.keys(map.current).sort().join(',')
91
- snapshotRef.current = newSnapshot
92
- // Always update committed map so useDescendantsRerender returns fresh data
93
- // (map.current is cleared by reset() on every render)
91
+ // Bump version on every commit so useSyncExternalStore triggers a re-render
92
+ // for subscribers. This ensures they read fresh committedMap props (detail,
93
+ // visible, etc) even when no items were added/removed.
94
+ snapshotRef.current = String(++versionRef.current)
94
95
  committedMapRef.current = { ...map.current }
95
- // Only notify if there are listeners AND snapshot changed
96
- if (listeners.current.size > 0 && newSnapshot !== prevSnapshotRef.current) {
96
+ if (listeners.current.size > 0) {
97
97
  listeners.current.forEach((cb) => {
98
98
  cb()
99
99
  })
@@ -134,9 +134,9 @@ export function createDescendants<T = any>() {
134
134
  }
135
135
 
136
136
  /**
137
- * Opt-in to re-renders when the set of descendant IDs changes.
137
+ * Opt-in to re-renders when descendants change.
138
138
  * Returns the committed map of descendants, readable during render.
139
- * Only triggers re-render when descendants are added/removed, not on prop changes.
139
+ * Triggers a re-render on every commit so props (detail, visible, etc) are always fresh.
140
140
  */
141
141
  function useDescendantsRerender(): DescendantMap<T> {
142
142
  const context = React.useContext(DescendantContext)
@@ -97,7 +97,7 @@ test('action shortcut is displayed in action panel', async () => {
97
97
  │ │
98
98
  │ Settings │
99
99
  │ Change Theme... │
100
- See Console Logs
100
+ Toggle Console Logs
101
101
  │ │
102
102
  │ │
103
103
  │ │
@@ -49,7 +49,7 @@ test('actions preserve React context through portal', async () => {
49
49
  │ │
50
50
  │ Settings │
51
51
  │ Change Theme... │
52
- See Console Logs
52
+ Toggle Console Logs
53
53
  │ │
54
54
  │ │
55
55
  │ │