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.
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +12 -0
- package/dist/build.js.map +1 -1
- package/dist/cli.js +5 -40
- package/dist/cli.js.map +1 -1
- package/dist/colors.d.ts +7 -7
- package/dist/colors.js +7 -7
- package/dist/compile.d.ts +6 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +45 -26
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.js +1 -1
- package/dist/components/actions.js.map +1 -1
- package/dist/components/bar-chart.d.ts +38 -0
- package/dist/components/bar-chart.d.ts.map +1 -0
- package/dist/components/bar-chart.js +158 -0
- package/dist/components/bar-chart.js.map +1 -0
- package/dist/components/bar-graph.d.ts +41 -0
- package/dist/components/bar-graph.d.ts.map +1 -0
- package/dist/components/bar-graph.js +95 -0
- package/dist/components/bar-graph.js.map +1 -0
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +5 -7
- package/dist/components/detail.js.map +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +8 -9
- package/dist/components/footer.js.map +1 -1
- package/dist/components/form/date-picker.d.ts.map +1 -1
- package/dist/components/form/date-picker.js +7 -1
- package/dist/components/form/date-picker.js.map +1 -1
- package/dist/components/form/dropdown.d.ts.map +1 -1
- package/dist/components/form/dropdown.js +10 -2
- package/dist/components/form/dropdown.js.map +1 -1
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +4 -5
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/use-form-navigation.d.ts.map +1 -1
- package/dist/components/form/use-form-navigation.js +6 -0
- package/dist/components/form/use-form-navigation.js.map +1 -1
- package/dist/components/graph.d.ts +111 -0
- package/dist/components/graph.d.ts.map +1 -0
- package/dist/components/graph.js +392 -0
- package/dist/components/graph.js.map +1 -0
- package/dist/components/icon.js +5 -5
- package/dist/components/icon.js.map +1 -1
- package/dist/components/list.d.ts +53 -5
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +125 -71
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-bar.js +3 -3
- package/dist/components/loading-bar.js.map +1 -1
- package/dist/components/loading-text.d.ts +1 -1
- package/dist/components/loading-text.d.ts.map +1 -1
- package/dist/components/loading-text.js +3 -1
- package/dist/components/loading-text.js.map +1 -1
- package/dist/components/metadata.js +2 -2
- package/dist/components/metadata.js.map +1 -1
- package/dist/components/row.d.ts +10 -0
- package/dist/components/row.d.ts.map +1 -0
- package/dist/components/row.js +12 -0
- package/dist/components/row.js.map +1 -0
- package/dist/components/table.d.ts +57 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +365 -0
- package/dist/components/table.js.map +1 -0
- package/dist/descendants.js +13 -13
- package/dist/descendants.js.map +1 -1
- package/dist/examples/bar-graph-weekly.d.ts +2 -0
- package/dist/examples/bar-graph-weekly.d.ts.map +1 -0
- package/dist/examples/bar-graph-weekly.js +95 -0
- package/dist/examples/bar-graph-weekly.js.map +1 -0
- package/dist/examples/components-weird-places.d.ts +2 -0
- package/dist/examples/components-weird-places.d.ts.map +1 -0
- package/dist/examples/components-weird-places.js +46 -0
- package/dist/examples/components-weird-places.js.map +1 -0
- package/dist/examples/graph-bar-chart.d.ts +2 -0
- package/dist/examples/graph-bar-chart.d.ts.map +1 -0
- package/dist/examples/graph-bar-chart.js +270 -0
- package/dist/examples/graph-bar-chart.js.map +1 -0
- package/dist/examples/graph-multi-series.d.ts +2 -0
- package/dist/examples/graph-multi-series.d.ts.map +1 -0
- package/dist/examples/graph-multi-series.js +23 -0
- package/dist/examples/graph-multi-series.js.map +1 -0
- package/dist/examples/graph-polymarket.d.ts +2 -0
- package/dist/examples/graph-polymarket.d.ts.map +1 -0
- package/dist/examples/graph-polymarket.js +109 -0
- package/dist/examples/graph-polymarket.js.map +1 -0
- package/dist/examples/graph-row.d.ts +2 -0
- package/dist/examples/graph-row.d.ts.map +1 -0
- package/dist/examples/graph-row.js +226 -0
- package/dist/examples/graph-row.js.map +1 -0
- package/dist/examples/graph-styles.d.ts +2 -0
- package/dist/examples/graph-styles.d.ts.map +1 -0
- package/dist/examples/graph-styles.js +316 -0
- package/dist/examples/graph-styles.js.map +1 -0
- package/dist/examples/list-accessory-table.d.ts +2 -0
- package/dist/examples/list-accessory-table.d.ts.map +1 -0
- package/dist/examples/list-accessory-table.js +46 -0
- package/dist/examples/list-accessory-table.js.map +1 -0
- package/dist/examples/list-item-accessories.d.ts +2 -0
- package/dist/examples/list-item-accessories.d.ts.map +1 -0
- package/dist/examples/list-item-accessories.js +27 -0
- package/dist/examples/list-item-accessories.js.map +1 -0
- package/dist/examples/list-no-actions.d.ts +2 -0
- package/dist/examples/list-no-actions.d.ts.map +1 -0
- package/dist/examples/list-no-actions.js +7 -0
- package/dist/examples/list-no-actions.js.map +1 -0
- package/dist/examples/simple-detail-table.d.ts +2 -0
- package/dist/examples/simple-detail-table.d.ts.map +1 -0
- package/dist/examples/simple-detail-table.js +45 -0
- package/dist/examples/simple-detail-table.js.map +1 -0
- package/dist/examples/simple-graph.d.ts +2 -0
- package/dist/examples/simple-graph.d.ts.map +1 -0
- package/dist/examples/simple-graph.js +32 -0
- package/dist/examples/simple-graph.js.map +1 -0
- package/dist/examples/simple-table-wrap.d.ts +2 -0
- package/dist/examples/simple-table-wrap.d.ts.map +1 -0
- package/dist/examples/simple-table-wrap.js +37 -0
- package/dist/examples/simple-table-wrap.js.map +1 -0
- package/dist/examples/table-edge-cases.d.ts +2 -0
- package/dist/examples/table-edge-cases.d.ts.map +1 -0
- package/dist/examples/table-edge-cases.js +70 -0
- package/dist/examples/table-edge-cases.js.map +1 -0
- package/dist/examples/table-flex-grow.d.ts +2 -0
- package/dist/examples/table-flex-grow.d.ts.map +1 -0
- package/dist/examples/table-flex-grow.js +18 -0
- package/dist/examples/table-flex-grow.js.map +1 -0
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +5 -1
- package/dist/extensions/dev.js.map +1 -1
- package/dist/globals.d.ts +1 -0
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +2 -0
- package/dist/globals.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/date-picker-widget.d.ts.map +1 -1
- package/dist/internal/date-picker-widget.js +4 -0
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +1 -3
- package/dist/internal/providers.js.map +1 -1
- package/dist/markdown-utils.d.ts +22 -1
- package/dist/markdown-utils.d.ts.map +1 -1
- package/dist/markdown-utils.js +66 -1
- package/dist/markdown-utils.js.map +1 -1
- package/dist/opentui.d.ts +4 -0
- package/dist/opentui.d.ts.map +1 -0
- package/dist/opentui.js +3 -0
- package/dist/opentui.js.map +1 -0
- package/dist/release.d.ts +2 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +2 -1
- package/dist/release.js.map +1 -1
- package/dist/state.d.ts +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +13 -0
- package/dist/theme.js.map +1 -1
- package/dist/themes/nerv.json +227 -0
- package/dist/themes/termcast.json +72 -71
- package/dist/themes.d.ts +2 -1
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +7 -5
- package/dist/themes.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -0
- package/dist/utils.js.map +1 -1
- package/package.json +13 -5
- package/src/build.tsx +13 -0
- package/src/cli.tsx +5 -49
- package/src/colors.tsx +7 -7
- package/src/compile.tsx +52 -29
- package/src/components/actions.tsx +1 -1
- package/src/components/bar-chart.tsx +271 -0
- package/src/components/bar-graph.tsx +214 -0
- package/src/components/detail.tsx +7 -8
- package/src/components/footer.tsx +14 -15
- package/src/components/form/date-picker.tsx +9 -0
- package/src/components/form/dropdown.tsx +13 -3
- package/src/components/form/index.tsx +4 -6
- package/src/components/form/use-form-navigation.tsx +6 -0
- package/src/components/graph.tsx +506 -0
- package/src/components/icon.tsx +5 -5
- package/src/components/list.tsx +210 -102
- package/src/components/loading-bar.tsx +3 -3
- package/src/components/loading-text.tsx +4 -2
- package/src/components/metadata.tsx +2 -2
- package/src/components/row.tsx +31 -0
- package/src/components/table.tsx +511 -0
- package/src/descendants.tsx +13 -13
- package/src/examples/action-shortcut.vitest.tsx +1 -1
- package/src/examples/actions-context.vitest.tsx +1 -1
- package/src/examples/bar-graph-weekly.tsx +264 -0
- package/src/examples/bar-graph-weekly.vitest.tsx +275 -0
- package/src/examples/detail-metadata-showcase.vitest.tsx +8 -8
- package/src/examples/form-basic.vitest.tsx +239 -0
- package/src/examples/form-dropdown.vitest.tsx +29 -29
- package/src/examples/form-tagpicker.vitest.tsx +27 -27
- package/src/examples/github.vitest.tsx +4 -4
- package/src/examples/graph-bar-chart.tsx +408 -0
- package/src/examples/graph-bar-chart.vitest.tsx +283 -0
- package/src/examples/graph-multi-series.tsx +36 -0
- package/src/examples/graph-multi-series.vitest.tsx +89 -0
- package/src/examples/graph-polymarket.tsx +182 -0
- package/src/examples/graph-polymarket.vitest.tsx +130 -0
- package/src/examples/graph-row.tsx +347 -0
- package/src/examples/graph-row.vitest.tsx +295 -0
- package/src/examples/graph-styles.tsx +457 -0
- package/src/examples/graph-styles.vitest.tsx +322 -0
- package/src/examples/list-accessory-table.tsx +77 -0
- package/src/examples/list-detail-metadata.vitest.tsx +21 -21
- package/src/examples/list-dropdown-default.vitest.tsx +12 -12
- package/src/examples/list-item-accessories.tsx +106 -0
- package/src/examples/list-item-accessories.vitest.tsx +115 -0
- package/src/examples/list-no-actions.tsx +18 -0
- package/src/examples/list-no-actions.vitest.tsx +97 -0
- package/src/examples/list-spacing-mode.vitest.tsx +6 -6
- package/src/examples/list-with-detail.vitest.tsx +92 -92
- package/src/examples/list-with-dropdown.vitest.tsx +49 -6
- package/src/examples/list-with-sections.vitest.tsx +61 -56
- package/src/examples/simple-detail-markdown.vitest.tsx +21 -17
- package/src/examples/simple-detail-table.tsx +65 -0
- package/src/examples/simple-detail-table.vitest.tsx +200 -0
- package/src/examples/simple-graph.tsx +51 -0
- package/src/examples/simple-graph.vitest.tsx +124 -0
- package/src/examples/simple-grid.vitest.tsx +3 -3
- package/src/examples/simple-list-search.vitest.tsx +65 -0
- package/src/examples/simple-navigation.vitest.tsx +3 -3
- package/src/examples/simple-table-wrap.tsx +55 -0
- package/src/examples/simple-table-wrap.vitest.tsx +91 -0
- package/src/examples/store.vitest.tsx +1 -1
- package/src/examples/table-edge-cases.tsx +72 -0
- package/src/examples/table-edge-cases.vitest.tsx +307 -0
- package/src/examples/table-flex-grow.tsx +53 -0
- package/src/examples/table-flex-grow.vitest.tsx +124 -0
- package/src/extensions/dev.tsx +7 -1
- package/src/globals.ts +3 -0
- package/src/index.tsx +31 -0
- package/src/internal/date-picker-widget.tsx +4 -0
- package/src/internal/providers.tsx +1 -4
- package/src/markdown-utils.tsx +82 -1
- package/src/opentui.tsx +5 -0
- package/src/release.tsx +3 -0
- package/src/state.tsx +2 -1
- package/src/theme.tsx +14 -0
- package/src/themes/nerv.json +231 -0
- package/src/themes/termcast.json +75 -71
- package/src/themes.ts +8 -5
- 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
|
+
}
|
package/src/descendants.tsx
CHANGED
|
@@ -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
|
-
|
|
56
|
-
//
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
137
|
+
* Opt-in to re-renders when descendants change.
|
|
138
138
|
* Returns the committed map of descendants, readable during render.
|
|
139
|
-
*
|
|
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)
|