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,506 @@
1
+ /**
2
+ * Graph component for rendering line charts in the terminal using braille characters.
3
+ *
4
+ * Uses a custom opentui Renderable (GraphPlotRenderable) for the actual plot drawing,
5
+ * registered via extend() so it can be used as <graph-plot> in JSX. The plot renderable
6
+ * draws directly to OptimizedBuffer using setCell() with braille Unicode characters
7
+ * (U+2800-U+28FF), giving 2x horizontal and 4x vertical sub-pixel resolution per
8
+ * terminal character cell.
9
+ *
10
+ * The React <Graph> component is a thin wrapper that collects series data from
11
+ * <Graph.Line> children, computes axis ranges, and passes everything to the renderable.
12
+ *
13
+ * Braille dot layout per cell:
14
+ * col0 col1
15
+ * 1 8 row0
16
+ * 2 16 row1
17
+ * 4 32 row2
18
+ * 64 128 row3
19
+ *
20
+ * For a plot of W cols x H rows we get W*2 x H*4 virtual pixels.
21
+ */
22
+
23
+ import React, { ReactNode, useMemo } from 'react'
24
+ import { Renderable, RGBA } from '@opentui/core'
25
+ import type { RenderableOptions, RenderContext } from '@opentui/core'
26
+ import type { OptimizedBuffer } from '@opentui/core'
27
+ import { extend } from '@opentui/react'
28
+ import { useTheme, getThemePalette } from 'termcast/src/theme'
29
+ import { Color, resolveColor } from 'termcast/src/colors'
30
+
31
+ // ── Graph variant ────────────────────────────────────────────────────
32
+ // Three rendering modes for the plot area:
33
+ // - 'area': braille dots filling area under curve (2×4 sub-pixel resolution)
34
+ // - 'filled': solid block characters (2× vertical sub-row resolution)
35
+ // - 'striped': all columns filled, alternating between two colors
36
+ // (pass 'transparent' for one color to get gap-style bars)
37
+
38
+ export type GraphVariant = 'area' | 'filled' | 'striped'
39
+
40
+ // ── Block characters for Filled/Striped modes ───────────────────────
41
+ // We use ▀/▄ with fg+bg color encoding to eliminate the tiny gaps
42
+ // some terminals show between adjacent █ rows.
43
+ // ▀ = top half drawn with fg, bottom half shows bg
44
+ // ▄ = bottom half drawn with fg, top half shows bg
45
+ // ▁ = lower 1/8 block, used as a thin baseline for zero/minimum values
46
+ const UPPER_HALF = '▀' // U+2580
47
+ const LOWER_HALF = '▄' // U+2584
48
+ const LOWER_EIGHTH = '▁' // U+2581
49
+
50
+ // ── Braille bit map ──────────────────────────────────────────────────
51
+ // Maps (subCol, subRow) to the braille bit for that dot position.
52
+ // subCol is 0 or 1, subRow is 0..3.
53
+ const BRAILLE_BITS: number[][] = [
54
+ // col 0 col 1
55
+ [1, 8], // row 0
56
+ [2, 16], // row 1
57
+ [4, 32], // row 2
58
+ [64, 128], // row 3
59
+ ]
60
+
61
+ function brailleBit(subCol: number, subRow: number): number {
62
+ return BRAILLE_BITS[subRow]![subCol]!
63
+ }
64
+
65
+ // ── Series data passed to the renderable ─────────────────────────────
66
+
67
+ export interface SeriesData {
68
+ data: number[]
69
+ color: string // hex color
70
+ }
71
+
72
+ // ── GraphPlotRenderable ──────────────────────────────────────────────
73
+
74
+ export interface GraphPlotOptions extends RenderableOptions {
75
+ series?: SeriesData[]
76
+ xLabels?: string[]
77
+ yMin?: number
78
+ yMax?: number
79
+ yTicks?: number
80
+ yFormat?: (v: number) => string
81
+ axisColor?: string
82
+ variant?: GraphVariant
83
+ stripeColor1?: string // hex color for even columns in striped mode
84
+ stripeColor2?: string // hex color for odd columns in striped mode
85
+ }
86
+
87
+ export class GraphPlotRenderable extends Renderable {
88
+ private _series: SeriesData[] = []
89
+ private _xLabels: string[] = []
90
+ private _yMin = 0
91
+ private _yMax = 100
92
+ private _yTicks = 5
93
+ private _yFormat: (v: number) => string = (v) => {
94
+ return v >= 1000 ? v.toFixed(0) : v.toFixed(1)
95
+ }
96
+ private _axisColor: string = '#666666'
97
+ private _variant: GraphVariant = 'area'
98
+ private _stripeColor1: string = '#0080FF'
99
+ private _stripeColor2: string = '#FF8000'
100
+
101
+ constructor(ctx: RenderContext, options: GraphPlotOptions) {
102
+ super(ctx, options)
103
+ if (options.series) this._series = options.series
104
+ if (options.xLabels) this._xLabels = options.xLabels
105
+ if (options.yMin !== undefined) this._yMin = options.yMin
106
+ if (options.yMax !== undefined) this._yMax = options.yMax
107
+ if (options.yTicks !== undefined) this._yTicks = options.yTicks
108
+ if (options.yFormat) this._yFormat = options.yFormat
109
+ if (options.axisColor) this._axisColor = options.axisColor
110
+ if (options.variant) this._variant = options.variant
111
+ if (options.stripeColor1) this._stripeColor1 = options.stripeColor1
112
+ if (options.stripeColor2) this._stripeColor2 = options.stripeColor2
113
+ }
114
+
115
+ set series(value: SeriesData[]) { this._series = value; this.requestRender() }
116
+ set xLabels(value: string[]) { this._xLabels = value; this.requestRender() }
117
+ set yMin(value: number) { this._yMin = value; this.requestRender() }
118
+ set yMax(value: number) { this._yMax = value; this.requestRender() }
119
+ set yTicks(value: number) { this._yTicks = value; this.requestRender() }
120
+ set yFormat(value: (v: number) => string) { this._yFormat = value; this.requestRender() }
121
+ set axisColor(value: string) { this._axisColor = value; this.requestRender() }
122
+ set variant(value: GraphVariant) { this._variant = value; this.requestRender() }
123
+ set stripeColor1(value: string) { this._stripeColor1 = value; this.requestRender() }
124
+ set stripeColor2(value: string) { this._stripeColor2 = value; this.requestRender() }
125
+
126
+ // ── Shared: compute layout and draw axes ─────────────────────
127
+ private computeLayout(): {
128
+ plotX: number; plotY: number; plotW: number; plotH: number
129
+ yAxisWidth: number; yLabels: string[]
130
+ } | null {
131
+ const totalW = this.width
132
+ const totalH = this.height
133
+ if (totalW <= 0 || totalH <= 0) return null
134
+
135
+ const yLabels: string[] = []
136
+ for (let i = 0; i < this._yTicks; i++) {
137
+ const value = this._yMin + (this._yMax - this._yMin) * (1 - i / (this._yTicks - 1))
138
+ yLabels.push(this._yFormat(value))
139
+ }
140
+ const yAxisWidth = Math.max(...yLabels.map((l) => l.length))
141
+
142
+ const plotX = this.x + yAxisWidth + 1
143
+ const plotY = this.y
144
+ const plotH = totalH - 1
145
+ const plotW = totalW - yAxisWidth - 1
146
+ if (plotW <= 0 || plotH <= 0) return null
147
+
148
+ return { plotX, plotY, plotW, plotH, yAxisWidth, yLabels }
149
+ }
150
+
151
+ private drawAxes(buffer: OptimizedBuffer, layout: {
152
+ plotX: number; plotY: number; plotW: number; plotH: number
153
+ yAxisWidth: number; yLabels: string[]
154
+ }): void {
155
+ const { plotX, plotY, plotW, plotH, yAxisWidth, yLabels } = layout
156
+ const axisRgba = RGBA.fromHex(this._axisColor)
157
+
158
+ // Y-axis labels + separator
159
+ const labelRows = new Set<number>()
160
+ for (let i = 0; i < this._yTicks; i++) {
161
+ const row = Math.round(plotY + (i / (this._yTicks - 1)) * (plotH - 1))
162
+ labelRows.add(row)
163
+ const label = yLabels[i]!
164
+ buffer.drawText(label, this.x + yAxisWidth - label.length, row, axisRgba)
165
+ buffer.drawText('│', this.x + yAxisWidth, row, axisRgba)
166
+ }
167
+ for (let row = plotY; row < plotY + plotH; row++) {
168
+ if (!labelRows.has(row)) {
169
+ buffer.drawText('│', this.x + yAxisWidth, row, axisRgba)
170
+ }
171
+ }
172
+
173
+ // X-axis labels
174
+ if (this._xLabels.length > 0) {
175
+ const xAxisRow = plotY + plotH
176
+ const labelCount = this._xLabels.length
177
+ for (let i = 0; i < labelCount; i++) {
178
+ const label = this._xLabels[i]!
179
+ const labelX = plotX + Math.round((i / Math.max(1, labelCount - 1)) * (plotW - 1))
180
+ const centeredX = Math.max(plotX, Math.min(labelX - Math.floor(label.length / 2), plotX + plotW - label.length))
181
+ buffer.drawText(label, centeredX, xAxisRow, axisRgba)
182
+ }
183
+ }
184
+ }
185
+
186
+ // ── Shared: interpolate line Y per column ────────────────────
187
+ // Returns an array where lineY[col] = the topmost virtual-row Y
188
+ // of the series line at that column. virtualRows is the total
189
+ // number of virtual rows (pixW for braille, plotW for block modes).
190
+ private computeLineYPerColumn({ series, colCount, virtualH }: {
191
+ series: { data: number[]; color: RGBA }
192
+ colCount: number
193
+ virtualH: number
194
+ }): Int32Array {
195
+ const yRange = this._yMax - this._yMin
196
+ const dataLen = series.data.length
197
+ const lineY = new Int32Array(colCount).fill(virtualH)
198
+
199
+ const pixelYs: number[] = series.data.map((v) => {
200
+ const normalized = (v - this._yMin) / yRange
201
+ return Math.round((1 - normalized) * (virtualH - 1))
202
+ })
203
+
204
+ for (let i = 0; i < dataLen - 1; i++) {
205
+ const x0 = Math.round((i / (dataLen - 1)) * (colCount - 1))
206
+ const y0 = pixelYs[i]!
207
+ const x1 = Math.round(((i + 1) / (dataLen - 1)) * (colCount - 1))
208
+ const y1 = pixelYs[i + 1]!
209
+
210
+ let dx = Math.abs(x1 - x0)
211
+ let dy = Math.abs(y1 - y0)
212
+ const sx = x0 < x1 ? 1 : -1
213
+ const sy = y0 < y1 ? 1 : -1
214
+ let err = dx - dy
215
+ let cx = x0
216
+ let cy = y0
217
+
218
+ while (true) {
219
+ if (cx >= 0 && cx < colCount && cy >= 0 && cy < virtualH) {
220
+ if (cy < lineY[cx]!) lineY[cx] = cy
221
+ }
222
+ if (cx === x1 && cy === y1) break
223
+ const e2 = 2 * err
224
+ if (e2 > -dy) { err -= dy; cx += sx }
225
+ if (e2 < dx) { err += dx; cy += sy }
226
+ }
227
+ }
228
+
229
+ if (dataLen === 1) {
230
+ const px = Math.round(colCount / 2)
231
+ const py = pixelYs[0]!
232
+ if (px >= 0 && px < colCount && py >= 0 && py < virtualH) {
233
+ lineY[px] = py
234
+ }
235
+ }
236
+
237
+ return lineY
238
+ }
239
+
240
+ // ── Style: Area (braille) ────────────────────────────────────
241
+ private renderArea(buffer: OptimizedBuffer, plotX: number, plotY: number, plotW: number, plotH: number): void {
242
+ const transparent = RGBA.fromValues(0, 0, 0, 0)
243
+ const pixW = plotW * 2
244
+ const pixH = plotH * 4
245
+ const yRange = this._yMax - this._yMin
246
+ if (yRange === 0) return
247
+
248
+ const cellCount = plotW * plotH
249
+ const cellBits = new Uint8Array(cellCount)
250
+ const cellColors: RGBA[] = Array.from({ length: cellCount }, () => transparent)
251
+
252
+ for (const series of this._series) {
253
+ if (series.data.length === 0) continue
254
+ const seriesColor = RGBA.fromHex(series.color)
255
+
256
+ const lineY = this.computeLineYPerColumn({
257
+ series: { data: series.data, color: seriesColor },
258
+ colCount: pixW,
259
+ virtualH: pixH,
260
+ })
261
+
262
+ for (let px = 0; px < pixW; px++) {
263
+ const topY = lineY[px]!
264
+ if (topY >= pixH) continue
265
+ for (let py = topY; py < pixH; py++) {
266
+ const cellX = Math.floor(px / 2)
267
+ const cellY = Math.floor(py / 4)
268
+ const subCol = px % 2
269
+ const subRow = py % 4
270
+ const cellIdx = cellY * plotW + cellX
271
+ if (cellIdx >= 0 && cellIdx < cellCount) {
272
+ cellBits[cellIdx]! |= brailleBit(subCol, subRow)
273
+ cellColors[cellIdx] = seriesColor
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ for (let cy = 0; cy < plotH; cy++) {
280
+ for (let cx = 0; cx < plotW; cx++) {
281
+ const cellIdx = cy * plotW + cx
282
+ const bits = cellBits[cellIdx]!
283
+ if (bits === 0) continue
284
+ buffer.setCell(plotX + cx, plotY + cy, String.fromCharCode(0x2800 + bits), cellColors[cellIdx]!, transparent)
285
+ }
286
+ }
287
+ }
288
+
289
+ // ── Style: Filled / Striped (block characters) ───────────────
290
+ // Always uses ▀ (upper-half block) with fg=top color, bg=bottom color.
291
+ // This eliminates the tiny gaps some terminals show between adjacent █ rows.
292
+ // Filled: every column uses the series color.
293
+ // Striped: all columns filled, even cols = stripeColor1, odd = stripeColor2.
294
+ // Pass a transparent color to skip those columns (gap-style bars).
295
+ private renderBlock(buffer: OptimizedBuffer, plotX: number, plotY: number, plotW: number, plotH: number, striped: boolean): void {
296
+ const transparent = RGBA.fromValues(0, 0, 0, 0)
297
+ const virtualH = plotH * 2 // 2 sub-rows per terminal row
298
+ const yRange = this._yMax - this._yMin
299
+ if (yRange === 0) return
300
+
301
+ const stripe1 = RGBA.fromHex(this._stripeColor1)
302
+ const stripe2 = RGBA.fromHex(this._stripeColor2)
303
+
304
+ for (const series of this._series) {
305
+ if (series.data.length === 0) continue
306
+ const seriesColor = RGBA.fromHex(series.color)
307
+
308
+ const lineY = this.computeLineYPerColumn({
309
+ series: { data: series.data, color: seriesColor },
310
+ colCount: plotW,
311
+ virtualH,
312
+ })
313
+
314
+ for (let col = 0; col < plotW; col++) {
315
+ // Determine fill color for this column
316
+ const fillColor = striped
317
+ ? (col % 2 === 0 ? stripe1 : stripe2)
318
+ : seriesColor
319
+
320
+ // Skip transparent columns (allows gap-style bars in striped mode)
321
+ if (fillColor.a === 0) continue
322
+
323
+ const topVRow = lineY[col]!
324
+ if (topVRow >= virtualH) continue
325
+
326
+ // Fill from topVRow down to virtualH-1 using ▀/▄ with fg+bg encoding.
327
+ // ▀: fg paints top half, bg paints bottom half.
328
+ // ▄: fg paints bottom half, bg paints top half.
329
+ // We never set fg=transparent on a visible glyph part (would show as black).
330
+ for (let row = 0; row < plotH; row++) {
331
+ const vTop = row * 2 // virtual row for top half
332
+ const vBot = row * 2 + 1 // virtual row for bottom half
333
+ const topFilled = vTop >= topVRow
334
+ const botFilled = vBot >= topVRow
335
+
336
+ if (!topFilled && !botFilled) continue
337
+
338
+ if (topFilled && botFilled) {
339
+ // Both halves: ▀ with fg=color, bg=color → seamless full block
340
+ buffer.setCell(plotX + col, plotY + row, UPPER_HALF, fillColor, fillColor)
341
+ } else if (topFilled) {
342
+ // Top only: ▀ with fg=color, bg=transparent
343
+ buffer.setCell(plotX + col, plotY + row, UPPER_HALF, fillColor, transparent)
344
+ } else if (topVRow >= virtualH - 1) {
345
+ // Minimum fill: only the very last virtual row is filled (zero/min value).
346
+ // Use ▁ (lower 1/8 block) for a thin baseline indicator.
347
+ buffer.setCell(plotX + col, plotY + row, LOWER_EIGHTH, fillColor, transparent)
348
+ } else {
349
+ // Bottom only: ▄ with fg=color, bg=transparent
350
+ buffer.setCell(plotX + col, plotY + row, LOWER_HALF, fillColor, transparent)
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ // ── Main render ──────────────────────────────────────────────
358
+ protected renderSelf(buffer: OptimizedBuffer): void {
359
+ const layout = this.computeLayout()
360
+ if (!layout) return
361
+ const { plotX, plotY, plotW, plotH } = layout
362
+
363
+ this.drawAxes(buffer, layout)
364
+
365
+ const yRange = this._yMax - this._yMin
366
+ if (yRange === 0 || this._series.length === 0) return
367
+
368
+ switch (this._variant) {
369
+ case 'area': {
370
+ this.renderArea(buffer, plotX, plotY, plotW, plotH)
371
+ break
372
+ }
373
+ case 'filled': {
374
+ this.renderBlock(buffer, plotX, plotY, plotW, plotH, false)
375
+ break
376
+ }
377
+ case 'striped': {
378
+ this.renderBlock(buffer, plotX, plotY, plotW, plotH, true)
379
+ break
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ // ── Register the custom renderable ───────────────────────────────────
386
+
387
+ extend({ 'graph-plot': GraphPlotRenderable })
388
+
389
+ declare module '@opentui/react' {
390
+ interface OpenTUIComponents {
391
+ 'graph-plot': typeof GraphPlotRenderable
392
+ }
393
+ }
394
+
395
+ // ── Graph.Line (data-only child, renders null) ───────────────────────
396
+
397
+ export interface GraphLineProps {
398
+ /** Y-values for this series */
399
+ data: number[]
400
+ /** Line color */
401
+ color?: Color.ColorLike
402
+ /** Series label (for future legend) */
403
+ title?: string
404
+ }
405
+
406
+ const GraphLine = (_props: GraphLineProps): any => {
407
+ return null
408
+ }
409
+
410
+ // ── Graph React component ────────────────────────────────────────────
411
+
412
+ export interface GraphProps {
413
+ /** Height of the graph in terminal rows (default: 15) */
414
+ height?: number
415
+ /** X-axis labels */
416
+ xLabels?: string[]
417
+ /** Manual Y-axis range [min, max] (default: auto from data) */
418
+ yRange?: [number, number]
419
+ /** Number of Y-axis tick labels (default: 5) */
420
+ yTicks?: number
421
+ /** Custom Y-axis label formatter */
422
+ yFormat?: (v: number) => string
423
+ /** Rendering variant: 'area' (braille), 'filled' (blocks), 'striped' (alternating colors) */
424
+ variant?: GraphVariant
425
+ /** Two alternating colors for 'striped' variant [even, odd]. Defaults to [theme.primary, theme.accent].
426
+ * Pass 'transparent' for one to get gap-style bars. */
427
+ stripeColors?: [Color.ColorLike, Color.ColorLike]
428
+ /** Graph.Line children */
429
+ children: ReactNode
430
+ }
431
+
432
+ interface GraphType {
433
+ (props: GraphProps): any
434
+ Line: (props: GraphLineProps) => any
435
+ }
436
+
437
+ const Graph: GraphType = (props) => {
438
+ const theme = useTheme()
439
+ const { height = 15, xLabels = [], yRange, yTicks = 5, yFormat, variant = 'area', stripeColors, children } = props
440
+
441
+ const palette = getThemePalette(theme)
442
+
443
+ // Collect series data from Graph.Line children
444
+ const series = useMemo<SeriesData[]>(() => {
445
+ const result: SeriesData[] = []
446
+ let colorIndex = 0
447
+ React.Children.forEach(children, (child) => {
448
+ if (!React.isValidElement(child)) return
449
+ const childProps = child.props as GraphLineProps
450
+ if (!childProps.data) return
451
+
452
+ const color = resolveColor(childProps.color) || palette[colorIndex % palette.length]!
453
+ result.push({
454
+ data: childProps.data,
455
+ color,
456
+ })
457
+ colorIndex++
458
+ })
459
+ return result
460
+ }, [children, palette])
461
+
462
+ // Auto-compute Y range if not provided
463
+ const computedYRange = useMemo<[number, number]>(() => {
464
+ if (yRange) return yRange
465
+ let min = Infinity
466
+ let max = -Infinity
467
+ for (const s of series) {
468
+ for (const v of s.data) {
469
+ if (v < min) min = v
470
+ if (v > max) max = v
471
+ }
472
+ }
473
+ if (min === Infinity) return [0, 100]
474
+ // Add small padding so lines don't touch edges
475
+ const padding = (max - min) * 0.05
476
+ return [min - padding, max + padding]
477
+ }, [series, yRange])
478
+
479
+ // Resolve stripe colors (defaults to theme primary + accent)
480
+ const resolvedStripe1 = resolveColor(stripeColors?.[0]) || theme.primary
481
+ const resolvedStripe2 = resolveColor(stripeColors?.[1]) || theme.accent
482
+
483
+ // Total height = plot rows + 1 for x-axis labels
484
+ const totalHeight = height + (xLabels.length > 0 ? 1 : 0)
485
+
486
+ return (
487
+ <graph-plot
488
+ width="100%"
489
+ height={totalHeight}
490
+ series={series}
491
+ xLabels={xLabels}
492
+ yMin={computedYRange[0]}
493
+ yMax={computedYRange[1]}
494
+ yTicks={yTicks}
495
+ yFormat={yFormat}
496
+ axisColor={theme.textMuted}
497
+ variant={variant}
498
+ stripeColor1={resolvedStripe1}
499
+ stripeColor2={resolvedStripe2}
500
+ />
501
+ )
502
+ }
503
+
504
+ Graph.Line = GraphLine
505
+
506
+ export { Graph }
@@ -45,8 +45,8 @@ const ICON_MAP: Record<string, string> = {
45
45
  'at-symbol-16': '@',
46
46
  'band-aid-16': '╋',
47
47
  'bank-note-16': '¤',
48
- 'bar-chart-16': '▊',
49
- 'bar-code-16': '▐',
48
+ // 'bar-chart-16': '▊',
49
+ // 'bar-code-16': '▐',
50
50
  'bath-tub-16': '▬',
51
51
  'battery-16': '▮',
52
52
  'battery-charging-16': '▮',
@@ -188,7 +188,7 @@ const ICON_MAP: Record<string, string> = {
188
188
  'key-16': '⊢',
189
189
  'keyboard-16': '⌗',
190
190
  'layers-16': '◫',
191
- 'leaderboard-16': '▊',
191
+ // 'leaderboard-16': '▊',
192
192
  'leaf-16': '∿',
193
193
  'light-bulb-16': '※',
194
194
  'light-bulb-off-16': '※',
@@ -288,8 +288,8 @@ const ICON_MAP: Record<string, string> = {
288
288
  'shuffle-16': '⇝',
289
289
  'signal-0-16': '▁',
290
290
  'signal-1-16': '▂',
291
- 'signal-2-16': '▄',
292
- 'signal-3-16': '█',
291
+ // 'signal-2-16': '▄',
292
+ // 'signal-3-16': '█',
293
293
  'snippets-16': '⊠',
294
294
  'snowflake-16': '※',
295
295
  'soccer-ball-16': '◎',