termcast 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +8 -7
- package/dist/build.js.map +1 -1
- package/dist/cli.js +0 -40
- package/dist/cli.js.map +1 -1
- package/dist/components/bar-graph.d.ts +23 -8
- package/dist/components/bar-graph.d.ts.map +1 -1
- package/dist/components/bar-graph.js +84 -40
- package/dist/components/bar-graph.js.map +1 -1
- package/dist/components/dotted-line-graph.d.ts +86 -0
- package/dist/components/dotted-line-graph.d.ts.map +1 -0
- package/dist/components/dotted-line-graph.js +260 -0
- package/dist/components/dotted-line-graph.js.map +1 -0
- package/dist/components/extension-preferences.d.ts.map +1 -1
- package/dist/components/extension-preferences.js +1 -10
- package/dist/components/extension-preferences.js.map +1 -1
- package/dist/components/graph.d.ts.map +1 -1
- package/dist/components/graph.js +7 -1
- package/dist/components/graph.js.map +1 -1
- package/dist/components/histogram.d.ts +42 -0
- package/dist/components/histogram.d.ts.map +1 -0
- package/dist/components/histogram.js +115 -0
- package/dist/components/histogram.js.map +1 -0
- package/dist/components/horizontal-bar-graph.d.ts +47 -0
- package/dist/components/horizontal-bar-graph.d.ts.map +1 -0
- package/dist/components/horizontal-bar-graph.js +137 -0
- package/dist/components/horizontal-bar-graph.js.map +1 -0
- package/dist/components/list.d.ts +2 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +10 -10
- package/dist/components/list.js.map +1 -1
- package/dist/examples/bar-graph-weekly.js +2 -2
- package/dist/examples/bar-graph-weekly.js.map +1 -1
- package/dist/examples/charts-showcase-barchart.d.ts +2 -0
- package/dist/examples/charts-showcase-barchart.d.ts.map +1 -0
- package/dist/examples/charts-showcase-barchart.js +10 -0
- package/dist/examples/charts-showcase-barchart.js.map +1 -0
- package/dist/examples/charts-showcase-bargraph.d.ts +2 -0
- package/dist/examples/charts-showcase-bargraph.d.ts.map +1 -0
- package/dist/examples/charts-showcase-bargraph.js +60 -0
- package/dist/examples/charts-showcase-bargraph.js.map +1 -0
- package/dist/examples/charts-showcase-candle.d.ts +2 -0
- package/dist/examples/charts-showcase-candle.d.ts.map +1 -0
- package/dist/examples/charts-showcase-candle.js +30 -0
- package/dist/examples/charts-showcase-candle.js.map +1 -0
- package/dist/examples/charts-showcase-graph.d.ts +2 -0
- package/dist/examples/charts-showcase-graph.d.ts.map +1 -0
- package/dist/examples/charts-showcase-graph.js +33 -0
- package/dist/examples/charts-showcase-graph.js.map +1 -0
- package/dist/examples/charts-showcase-heatmap.d.ts +2 -0
- package/dist/examples/charts-showcase-heatmap.d.ts.map +1 -0
- package/dist/examples/charts-showcase-heatmap.js +36 -0
- package/dist/examples/charts-showcase-heatmap.js.map +1 -0
- package/dist/examples/charts-showcase-mixed.d.ts +2 -0
- package/dist/examples/charts-showcase-mixed.d.ts.map +1 -0
- package/dist/examples/charts-showcase-mixed.js +30 -0
- package/dist/examples/charts-showcase-mixed.js.map +1 -0
- package/dist/examples/charts-showcase-progress.d.ts +2 -0
- package/dist/examples/charts-showcase-progress.d.ts.map +1 -0
- package/dist/examples/charts-showcase-progress.js +10 -0
- package/dist/examples/charts-showcase-progress.js.map +1 -0
- package/dist/examples/graph-multi-series.js +1 -1
- package/dist/examples/graph-multi-series.js.map +1 -1
- package/dist/examples/horizontal-bar-graph-weekly.d.ts +2 -0
- package/dist/examples/horizontal-bar-graph-weekly.d.ts.map +1 -0
- package/dist/examples/horizontal-bar-graph-weekly.js +67 -0
- package/dist/examples/horizontal-bar-graph-weekly.js.map +1 -0
- package/dist/examples/simple-dotted-line-graph.d.ts +2 -0
- package/dist/examples/simple-dotted-line-graph.d.ts.map +1 -0
- package/dist/examples/simple-dotted-line-graph.js +39 -0
- package/dist/examples/simple-dotted-line-graph.js.map +1 -0
- package/dist/examples/simple-histogram.d.ts +2 -0
- package/dist/examples/simple-histogram.d.ts.map +1 -0
- package/dist/examples/simple-histogram.js +47 -0
- package/dist/examples/simple-histogram.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +15 -6
- package/dist/logger.js.map +1 -1
- package/dist/platform/node/sqlite.d.ts +6 -5
- package/dist/platform/node/sqlite.d.ts.map +1 -1
- package/dist/platform/node/sqlite.js +30 -14
- package/dist/platform/node/sqlite.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +11 -9
- package/dist/theme.js.map +1 -1
- package/dist/utils/run-command.d.ts.map +1 -1
- package/dist/utils/run-command.js +8 -19
- package/dist/utils/run-command.js.map +1 -1
- package/dist/utils.d.ts +1 -19
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -100
- package/dist/utils.js.map +1 -1
- package/package.json +14 -16
- package/src/build.tsx +11 -10
- package/src/cli.tsx +3 -40
- package/src/compile.vitest.tsx +3 -3
- package/src/components/bar-graph.tsx +217 -111
- package/src/components/dotted-line-graph.tsx +407 -0
- package/src/components/extension-preferences.tsx +2 -12
- package/src/components/graph.tsx +5 -1
- package/src/components/histogram.tsx +228 -0
- package/src/components/horizontal-bar-graph.tsx +279 -0
- package/src/components/list.tsx +20 -15
- package/src/examples/action-shortcut.vitest.tsx +17 -17
- package/src/examples/bar-graph-weekly.tsx +2 -2
- package/src/examples/bar-graph-weekly.vitest.tsx +63 -62
- package/src/examples/charts-showcase-bargraph.tsx +103 -0
- package/src/examples/detail-metadata-showcase.vitest.tsx +13 -18
- package/src/examples/form-basic.vitest.tsx +35 -35
- package/src/examples/form-dropdown.vitest.tsx +11 -11
- package/src/examples/form-scroll.vitest.tsx +1 -1
- package/src/examples/form-tagpicker.vitest.tsx +11 -11
- package/src/examples/github.vitest.tsx +22 -22
- package/src/examples/graph-bar-chart.vitest.tsx +8 -8
- package/src/examples/graph-multi-series.tsx +1 -1
- package/src/examples/graph-row.vitest.tsx +14 -14
- package/src/examples/graph-styles.vitest.tsx +77 -77
- package/src/examples/horizontal-bar-graph-weekly.tsx +138 -0
- package/src/examples/horizontal-bar-graph-weekly.vitest.tsx +164 -0
- package/src/examples/list-detail-metadata.vitest.tsx +4 -4
- package/src/examples/list-with-detail.vitest.tsx +46 -46
- package/src/examples/simple-candle-chart.vitest.tsx +8 -8
- package/src/examples/simple-dotted-line-graph.tsx +53 -0
- package/src/examples/simple-dotted-line-graph.vitest.tsx +62 -0
- package/src/examples/simple-grid.vitest.tsx +4 -4
- package/src/examples/simple-histogram.tsx +90 -0
- package/src/examples/simple-navigation.vitest.tsx +4 -4
- package/src/examples/swift-extension.vitest.tsx +3 -3
- package/src/examples/toast-variations.vitest.tsx +5 -5
- package/src/extensions/dev.vitest.tsx +8 -8
- package/src/index.tsx +21 -0
- package/src/logger.tsx +16 -6
- package/src/platform/node/sqlite.ts +29 -13
- package/src/theme.tsx +11 -10
- package/src/utils/run-command.tsx +10 -19
- package/src/utils.tsx +0 -163
- package/src/examples/store.tsx +0 -4
- package/src/examples/store.vitest.tsx +0 -78
- package/src/extensions/home.tsx +0 -227
- package/src/extensions/store.tsx +0 -375
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HorizontalBarGraph renders multi-series horizontal stacked bars.
|
|
3
|
+
*
|
|
4
|
+
* Each row is one category, usually a time bucket. The left chart area grows to
|
|
5
|
+
* fill available space while the right legend uses only the width needed for
|
|
6
|
+
* colored series rows and percentages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { ReactNode, useMemo } from 'react'
|
|
10
|
+
import { BoxProps } from '@opentui/react'
|
|
11
|
+
import { Color, resolveColor } from 'termcast/src/colors'
|
|
12
|
+
import { getThemePalette, useTheme } from 'termcast/src/theme'
|
|
13
|
+
|
|
14
|
+
export interface HorizontalBarGraphSeriesProps {
|
|
15
|
+
/** One value per row/category position. */
|
|
16
|
+
data: number[]
|
|
17
|
+
/** Series label shown in the legend. */
|
|
18
|
+
title?: string
|
|
19
|
+
/** Override the auto-assigned color. */
|
|
20
|
+
color?: Color.ColorLike
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HorizontalBarGraphProps extends BoxProps {
|
|
24
|
+
/** Row labels, one per bar position. Usually time buckets. */
|
|
25
|
+
labels?: string[]
|
|
26
|
+
/** Maximum chart rows to render. Defaults to all rows. */
|
|
27
|
+
height?: number
|
|
28
|
+
/** Character used for bar cells. Matches Histogram by default. */
|
|
29
|
+
barCharacter?: string
|
|
30
|
+
/** Show header row. Default true. */
|
|
31
|
+
showHeader?: boolean
|
|
32
|
+
/** Show right-side legend. Default true when any series has a title. */
|
|
33
|
+
showLegend?: boolean
|
|
34
|
+
/** Max display width for labels; longer labels are truncated. Default 16. */
|
|
35
|
+
maxLabelWidth?: number
|
|
36
|
+
/** Header text for the category column. Default "category". */
|
|
37
|
+
categoryTitle?: string
|
|
38
|
+
/** Header text for the bar column. Default "distribution". */
|
|
39
|
+
distributionTitle?: string
|
|
40
|
+
/** Header text for the legend column. Default "legend". */
|
|
41
|
+
legendTitle?: string
|
|
42
|
+
/** HorizontalBarGraph.Series children. */
|
|
43
|
+
children: ReactNode
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface HorizontalBarGraphType {
|
|
47
|
+
(props: HorizontalBarGraphProps): any
|
|
48
|
+
Series: (props: HorizontalBarGraphSeriesProps) => any
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function truncateLabel(label: string, maxLabelWidth: number): string {
|
|
52
|
+
if (label.length <= maxLabelWidth) {
|
|
53
|
+
return label
|
|
54
|
+
}
|
|
55
|
+
return label.slice(0, maxLabelWidth - 1) + '…'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function padRight(value: string, width: number): string {
|
|
59
|
+
if (value.length >= width) {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
return value + ' '.repeat(width - value.length)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function padLeft(value: string, width: number): string {
|
|
66
|
+
if (value.length >= width) {
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
69
|
+
return ' '.repeat(width - value.length) + value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatPercentage(value: number, total: number): string {
|
|
73
|
+
if (total <= 0) {
|
|
74
|
+
return '0%'
|
|
75
|
+
}
|
|
76
|
+
return `${Math.round((value / total) * 100)}%`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const HorizontalBarGraphSeries = (_props: HorizontalBarGraphSeriesProps): any => {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const HorizontalBarGraph: HorizontalBarGraphType = (props) => {
|
|
84
|
+
const theme = useTheme()
|
|
85
|
+
const {
|
|
86
|
+
labels = [],
|
|
87
|
+
height,
|
|
88
|
+
barCharacter = '╻',
|
|
89
|
+
showHeader = true,
|
|
90
|
+
showLegend,
|
|
91
|
+
maxLabelWidth = 16,
|
|
92
|
+
categoryTitle = 'category',
|
|
93
|
+
distributionTitle = 'distribution',
|
|
94
|
+
legendTitle = 'legend',
|
|
95
|
+
children,
|
|
96
|
+
...rest
|
|
97
|
+
} = props
|
|
98
|
+
|
|
99
|
+
const palette = getThemePalette(theme)
|
|
100
|
+
|
|
101
|
+
const seriesList = useMemo<Array<{ data: number[]; color: string; title?: string }>>(() => {
|
|
102
|
+
const childArray = React.Children.toArray(children)
|
|
103
|
+
return childArray
|
|
104
|
+
.filter(React.isValidElement)
|
|
105
|
+
.map((child, index) => {
|
|
106
|
+
const childProps = child.props as HorizontalBarGraphSeriesProps
|
|
107
|
+
return {
|
|
108
|
+
data: childProps.data,
|
|
109
|
+
title: childProps.title,
|
|
110
|
+
color: resolveColor(childProps.color) || palette[index % palette.length]!,
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.filter((series) => {
|
|
114
|
+
return Array.isArray(series.data)
|
|
115
|
+
})
|
|
116
|
+
}, [children, palette])
|
|
117
|
+
|
|
118
|
+
const rowCount = useMemo(() => {
|
|
119
|
+
return Math.max(labels.length, ...seriesList.map((series) => series.data.length), 0)
|
|
120
|
+
}, [labels, seriesList])
|
|
121
|
+
|
|
122
|
+
const rows = useMemo<Array<{ label: string; total: number; values: number[] }>>(() => {
|
|
123
|
+
return Array.from({ length: rowCount }, (_, rowIndex) => {
|
|
124
|
+
const values = seriesList.map((series) => {
|
|
125
|
+
return series.data[rowIndex] || 0
|
|
126
|
+
})
|
|
127
|
+
const total = values.reduce((sum, value) => {
|
|
128
|
+
return sum + value
|
|
129
|
+
}, 0)
|
|
130
|
+
return {
|
|
131
|
+
label: labels[rowIndex] ?? String(rowIndex + 1),
|
|
132
|
+
total,
|
|
133
|
+
values,
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}, [labels, rowCount, seriesList])
|
|
137
|
+
|
|
138
|
+
const visibleRows = useMemo(() => {
|
|
139
|
+
return rows.slice(0, height ?? rows.length)
|
|
140
|
+
}, [height, rows])
|
|
141
|
+
|
|
142
|
+
const maxTotal = useMemo(() => {
|
|
143
|
+
return Math.max(0, ...rows.map((row) => row.total))
|
|
144
|
+
}, [rows])
|
|
145
|
+
|
|
146
|
+
const legendRows = useMemo<Array<{ title: string; color: string; percentage: string; total: number }>>(() => {
|
|
147
|
+
const grandTotal = rows.reduce((sum, row) => {
|
|
148
|
+
return sum + row.total
|
|
149
|
+
}, 0)
|
|
150
|
+
return seriesList
|
|
151
|
+
.map((series, seriesIndex) => {
|
|
152
|
+
const seriesTotal = series.data.reduce((sum, value) => {
|
|
153
|
+
return sum + value
|
|
154
|
+
}, 0)
|
|
155
|
+
return {
|
|
156
|
+
title: series.title ?? `Series ${seriesIndex + 1}`,
|
|
157
|
+
color: series.color,
|
|
158
|
+
percentage: formatPercentage(seriesTotal, grandTotal),
|
|
159
|
+
total: seriesTotal,
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
.filter((row) => {
|
|
163
|
+
return row.title.length > 0
|
|
164
|
+
})
|
|
165
|
+
.sort((a, b) => {
|
|
166
|
+
return b.total - a.total
|
|
167
|
+
})
|
|
168
|
+
}, [rows, seriesList])
|
|
169
|
+
|
|
170
|
+
const legendVisible = showLegend ?? seriesList.some((series) => {
|
|
171
|
+
return Boolean(series.title)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
if (seriesList.length === 0 || visibleRows.length === 0 || maxTotal === 0) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const displayLabels = visibleRows.map((row) => {
|
|
179
|
+
return truncateLabel(row.label, maxLabelWidth)
|
|
180
|
+
})
|
|
181
|
+
const displayCategoryTitle = truncateLabel(categoryTitle, maxLabelWidth)
|
|
182
|
+
const labelWidth = Math.max(5, displayCategoryTitle.length, ...displayLabels.map((label) => label.length))
|
|
183
|
+
|
|
184
|
+
const legendGap = 2
|
|
185
|
+
const legendTitleWidth = Math.max(6, ...legendRows.map((row) => row.title.length))
|
|
186
|
+
const legendPercentageWidth = Math.max(3, ...legendRows.map((row) => row.percentage.length))
|
|
187
|
+
const legendWidth = legendVisible ? legendGap + 2 + legendTitleWidth + 2 + legendPercentageWidth : 0
|
|
188
|
+
const headerHeight = showHeader ? 2 : 0
|
|
189
|
+
const chartHeight = headerHeight + visibleRows.length
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<box flexDirection="column" width="100%" flexShrink={0} {...rest}>
|
|
193
|
+
{showHeader && (
|
|
194
|
+
<>
|
|
195
|
+
<box flexDirection="row" height={1} flexShrink={0}>
|
|
196
|
+
<box width={labelWidth + 2} flexShrink={0} overflow="hidden">
|
|
197
|
+
<text fg={theme.textMuted} wrapMode="none">{padRight(displayCategoryTitle, labelWidth)} </text>
|
|
198
|
+
</box>
|
|
199
|
+
<box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
200
|
+
<text fg={theme.textMuted} wrapMode="none">{distributionTitle}</text>
|
|
201
|
+
</box>
|
|
202
|
+
{legendVisible && (
|
|
203
|
+
<box width={legendWidth} flexShrink={0} overflow="hidden">
|
|
204
|
+
<text fg={theme.textMuted} wrapMode="none">{' '.repeat(legendGap)}{legendTitle}</text>
|
|
205
|
+
</box>
|
|
206
|
+
)}
|
|
207
|
+
</box>
|
|
208
|
+
<box flexDirection="row" height={1} flexShrink={0}>
|
|
209
|
+
<box width={labelWidth + 2} flexShrink={0} overflow="hidden">
|
|
210
|
+
<text fg={theme.borderSubtle} wrapMode="none">{'─'.repeat(labelWidth)} </text>
|
|
211
|
+
</box>
|
|
212
|
+
<box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
213
|
+
<text fg={theme.borderSubtle} wrapMode="none">{'─'.repeat(200)}</text>
|
|
214
|
+
</box>
|
|
215
|
+
{legendVisible && (
|
|
216
|
+
<box width={legendWidth} flexShrink={0} overflow="hidden">
|
|
217
|
+
<text fg={theme.borderSubtle} wrapMode="none">{' '.repeat(legendGap)}{'─'.repeat(legendWidth - legendGap)}</text>
|
|
218
|
+
</box>
|
|
219
|
+
)}
|
|
220
|
+
</box>
|
|
221
|
+
</>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
<box flexDirection="row" height={chartHeight - headerHeight} flexShrink={0}>
|
|
225
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1} overflow="hidden">
|
|
226
|
+
{visibleRows.map((row, rowIndex) => {
|
|
227
|
+
return (
|
|
228
|
+
<box key={row.label} flexDirection="row" height={1} flexShrink={0} overflow="hidden">
|
|
229
|
+
<box width={labelWidth + 2} flexShrink={0} overflow="hidden">
|
|
230
|
+
<text fg={theme.text} wrapMode="none">
|
|
231
|
+
{padRight(displayLabels[rowIndex]!, labelWidth)}
|
|
232
|
+
</text>
|
|
233
|
+
</box>
|
|
234
|
+
<box flexDirection="row" flexGrow={1} flexShrink={1} overflow="hidden">
|
|
235
|
+
{row.values.map((value, seriesIndex) => {
|
|
236
|
+
if (value <= 0) {
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
const series = seriesList[seriesIndex]!
|
|
240
|
+
return (
|
|
241
|
+
<box key={seriesIndex} flexGrow={value} flexBasis={0} flexShrink={1} overflow="hidden">
|
|
242
|
+
<box position="absolute" width="100%" height="100%" overflow="hidden">
|
|
243
|
+
<text fg={series.color} wrapMode="none">{barCharacter.repeat(200)}</text>
|
|
244
|
+
</box>
|
|
245
|
+
</box>
|
|
246
|
+
)
|
|
247
|
+
})}
|
|
248
|
+
{maxTotal > row.total && (
|
|
249
|
+
<box flexGrow={maxTotal - row.total} flexBasis={0} flexShrink={1} />
|
|
250
|
+
)}
|
|
251
|
+
</box>
|
|
252
|
+
</box>
|
|
253
|
+
)
|
|
254
|
+
})}
|
|
255
|
+
</box>
|
|
256
|
+
|
|
257
|
+
{legendVisible && (
|
|
258
|
+
<box flexDirection="column" width={legendWidth} flexShrink={0} overflow="hidden">
|
|
259
|
+
{legendRows.map((row) => {
|
|
260
|
+
return (
|
|
261
|
+
<box key={row.title} height={1} flexShrink={0} flexDirection="row" overflow="hidden">
|
|
262
|
+
<box width={legendGap} flexShrink={0} />
|
|
263
|
+
<text fg={row.color} wrapMode="none">● </text>
|
|
264
|
+
<text fg={theme.textMuted} wrapMode="none">
|
|
265
|
+
{padRight(row.title, legendTitleWidth)} {padLeft(row.percentage, legendPercentageWidth)}
|
|
266
|
+
</text>
|
|
267
|
+
</box>
|
|
268
|
+
)
|
|
269
|
+
})}
|
|
270
|
+
</box>
|
|
271
|
+
)}
|
|
272
|
+
</box>
|
|
273
|
+
</box>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
HorizontalBarGraph.Series = HorizontalBarGraphSeries
|
|
278
|
+
|
|
279
|
+
export { HorizontalBarGraph }
|
package/src/components/list.tsx
CHANGED
|
@@ -374,6 +374,8 @@ export interface DropdownProps extends SearchBarInterface, CommonProps {
|
|
|
374
374
|
storeValue?: boolean
|
|
375
375
|
value?: string
|
|
376
376
|
defaultValue?: string
|
|
377
|
+
/** Override the text shown in the search bar for the active selection. When set, this is displayed instead of the selected item's title. Useful for managed state where the display label doesn't match any dropdown item. */
|
|
378
|
+
displayValue?: string
|
|
377
379
|
onChange?: (newValue: string) => void
|
|
378
380
|
children?: ReactNode
|
|
379
381
|
}
|
|
@@ -946,9 +948,9 @@ function ListItemRow(props: {
|
|
|
946
948
|
<text flexShrink={0} fg={active ? theme.background : theme.text} attributes={active ? TextAttributes.BOLD : undefined} selectable={false} wrapMode="none">{active ? '›' : ' '}</text>
|
|
947
949
|
{icon && <text flexShrink={0} fg={active ? theme.background : iconColor || theme.text} selectable={false} wrapMode="none">{getIconEmoji(icon)} </text>}
|
|
948
950
|
<text
|
|
949
|
-
flexShrink={
|
|
951
|
+
flexShrink={1}
|
|
952
|
+
truncate
|
|
950
953
|
fg={active ? theme.background : theme.text}
|
|
951
|
-
|
|
952
954
|
attributes={TextAttributes.BOLD}
|
|
953
955
|
selectable={false}
|
|
954
956
|
wrapMode="none"
|
|
@@ -969,9 +971,10 @@ function ListItemRow(props: {
|
|
|
969
971
|
</box>
|
|
970
972
|
{/* Line 2: subtitle indented to align with title */}
|
|
971
973
|
{subtitle && (
|
|
972
|
-
<box style={{ paddingLeft: subtitleIndent }}>
|
|
974
|
+
<box style={{ paddingLeft: subtitleIndent, overflow: 'hidden' }}>
|
|
973
975
|
<text
|
|
974
|
-
flexShrink={
|
|
976
|
+
flexShrink={1}
|
|
977
|
+
truncate
|
|
975
978
|
fg={active ? theme.background : theme.text}
|
|
976
979
|
selectable={false}
|
|
977
980
|
wrapMode="none"
|
|
@@ -1006,11 +1009,12 @@ function ListItemRow(props: {
|
|
|
1006
1009
|
onMouseDown={handleMouseDown}
|
|
1007
1010
|
>
|
|
1008
1011
|
<box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden', gap: 1 }}>
|
|
1009
|
-
<box style={{ flexDirection: 'row', flexShrink:
|
|
1012
|
+
<box style={{ flexDirection: 'row', flexShrink: 1, overflow: 'hidden' }}>
|
|
1010
1013
|
<text flexShrink={0} fg={active ? theme.background : theme.text} attributes={active ? TextAttributes.BOLD : undefined} selectable={false} wrapMode="none">{active ? '›' : ' '}</text>
|
|
1011
1014
|
{icon && <text flexShrink={0} fg={active ? theme.background : iconColor || theme.text} selectable={false} wrapMode="none">{getIconEmoji(icon)} </text>}
|
|
1012
1015
|
<text
|
|
1013
|
-
flexShrink={
|
|
1016
|
+
flexShrink={1}
|
|
1017
|
+
truncate
|
|
1014
1018
|
fg={active ? theme.background : theme.text}
|
|
1015
1019
|
attributes={active ? TextAttributes.BOLD : undefined}
|
|
1016
1020
|
selectable={false}
|
|
@@ -1021,7 +1025,8 @@ function ListItemRow(props: {
|
|
|
1021
1025
|
</box>
|
|
1022
1026
|
{subtitle && (
|
|
1023
1027
|
<text
|
|
1024
|
-
flexShrink={
|
|
1028
|
+
flexShrink={3}
|
|
1029
|
+
truncate
|
|
1025
1030
|
fg={active ? theme.background : theme.textMuted}
|
|
1026
1031
|
selectable={false}
|
|
1027
1032
|
wrapMode="none"
|
|
@@ -2127,9 +2132,9 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
2127
2132
|
|
|
2128
2133
|
const { isDropdownOpen, setIsDropdownOpen } = listContext
|
|
2129
2134
|
|
|
2130
|
-
const setDropdownSelection = (
|
|
2131
|
-
setDropdownState({ value:
|
|
2132
|
-
useStore.setState({ dropdownFooterLabel: props.title || 'dropdown' })
|
|
2135
|
+
const setDropdownSelection = (selectionProps: { value: string; title: string }) => {
|
|
2136
|
+
setDropdownState({ value: selectionProps.value, title: selectionProps.title })
|
|
2137
|
+
useStore.setState({ dropdownFooterLabel: props.displayValue ?? (selectionProps.title || 'dropdown') })
|
|
2133
2138
|
}
|
|
2134
2139
|
// Store both value and title together
|
|
2135
2140
|
const [dropdownState, setDropdownState] = useState<{
|
|
@@ -2168,7 +2173,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
2168
2173
|
|
|
2169
2174
|
if (!valueToUse) {
|
|
2170
2175
|
useStore.setState({
|
|
2171
|
-
dropdownFooterLabel: dropdownState.title || 'dropdown',
|
|
2176
|
+
dropdownFooterLabel: props.displayValue ?? (dropdownState.title || 'dropdown'),
|
|
2172
2177
|
})
|
|
2173
2178
|
return
|
|
2174
2179
|
}
|
|
@@ -2189,8 +2194,8 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
2189
2194
|
return
|
|
2190
2195
|
}
|
|
2191
2196
|
|
|
2192
|
-
useStore.setState({ dropdownFooterLabel: title || 'dropdown' })
|
|
2193
|
-
}, [props.value]) // Run when props.value changes and on mount
|
|
2197
|
+
useStore.setState({ dropdownFooterLabel: props.displayValue ?? (title || 'dropdown') })
|
|
2198
|
+
}, [props.value, props.displayValue]) // Run when props.value or displayValue changes and on mount
|
|
2194
2199
|
|
|
2195
2200
|
const dropdownContextValue = useMemo<DropdownContextValue>(
|
|
2196
2201
|
() => ({
|
|
@@ -2242,8 +2247,8 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
2242
2247
|
}
|
|
2243
2248
|
}, [isDropdownOpen, props.children])
|
|
2244
2249
|
|
|
2245
|
-
// Display the title from our state
|
|
2246
|
-
const displayValue = dropdownState.title || 'All'
|
|
2250
|
+
// Display the title from our state, or use the caller's override
|
|
2251
|
+
const displayValue = props.displayValue ?? (dropdownState.title || 'All')
|
|
2247
2252
|
const openDropdownIfClosed = () => {
|
|
2248
2253
|
if (!isDropdownOpen) {
|
|
2249
2254
|
listContext.openDropdown()
|
|
@@ -27,18 +27,18 @@ afterEach(() => {
|
|
|
27
27
|
test('ctrl+r shortcut should trigger Refresh action directly', async () => {
|
|
28
28
|
// Wait for list to render
|
|
29
29
|
await session.text({
|
|
30
|
-
waitFor: (text) => /
|
|
30
|
+
waitFor: (text) => /nt: 0/.test(text),
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
const initial = await session.text()
|
|
34
|
-
expect(initial).toContain('
|
|
34
|
+
expect(initial).toContain('nt: 0')
|
|
35
35
|
|
|
36
36
|
// Press ctrl+r directly to trigger Refresh action
|
|
37
37
|
await session.press(['ctrl', 'r'])
|
|
38
38
|
|
|
39
39
|
// Wait for the refresh to take effect
|
|
40
40
|
const afterCtrlR = await session.text({
|
|
41
|
-
waitFor: (text) => /
|
|
41
|
+
waitFor: (text) => /nt: 1/.test(text),
|
|
42
42
|
timeout: 5000,
|
|
43
43
|
})
|
|
44
44
|
expect(afterCtrlR).toMatchInlineSnapshot(`
|
|
@@ -49,7 +49,7 @@ test('ctrl+r shortcut should trigger Refresh action directly', async () => {
|
|
|
49
49
|
|
|
50
50
|
> Search...
|
|
51
51
|
|
|
52
|
-
›
|
|
52
|
+
›Refre...nt: 1Press ctrl+r to refresh ...nter then select Refresh
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
|
|
@@ -69,7 +69,7 @@ test('ctrl+r shortcut should trigger Refresh action directly', async () => {
|
|
|
69
69
|
test('action shortcut is displayed in action panel', async () => {
|
|
70
70
|
// Wait for list to render
|
|
71
71
|
await session.text({
|
|
72
|
-
waitFor: (text) => /
|
|
72
|
+
waitFor: (text) => /nt: 0/.test(text),
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
// Open action panel with ctrl+k
|
|
@@ -110,30 +110,30 @@ test('action shortcut is displayed in action panel', async () => {
|
|
|
110
110
|
test('action works via Enter (auto-execute first action)', async () => {
|
|
111
111
|
// Wait for list to render
|
|
112
112
|
await session.text({
|
|
113
|
-
waitFor: (text) => /
|
|
113
|
+
waitFor: (text) => /nt: 0/.test(text),
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
// Press Enter to auto-execute first action (Refresh)
|
|
117
117
|
await session.press('return')
|
|
118
118
|
|
|
119
119
|
const afterEnter = await session.text({
|
|
120
|
-
waitFor: (text) => /
|
|
120
|
+
waitFor: (text) => /nt: 1/.test(text),
|
|
121
121
|
timeout: 5000,
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
-
expect(afterEnter).toContain('
|
|
124
|
+
expect(afterEnter).toContain('nt: 1')
|
|
125
125
|
}, 30000)
|
|
126
126
|
|
|
127
127
|
test('ctrl+x shortcut should trigger Reset action directly', async () => {
|
|
128
128
|
// Wait for list to render and increment once via Enter
|
|
129
129
|
await session.text({
|
|
130
|
-
waitFor: (text) => /
|
|
130
|
+
waitFor: (text) => /nt: 0/.test(text),
|
|
131
131
|
})
|
|
132
132
|
|
|
133
133
|
// Increment via Enter first
|
|
134
134
|
await session.press('return')
|
|
135
135
|
await session.text({
|
|
136
|
-
waitFor: (text) => /
|
|
136
|
+
waitFor: (text) => /nt: 1/.test(text),
|
|
137
137
|
timeout: 5000,
|
|
138
138
|
})
|
|
139
139
|
|
|
@@ -142,27 +142,27 @@ test('ctrl+x shortcut should trigger Reset action directly', async () => {
|
|
|
142
142
|
|
|
143
143
|
// Wait for the reset to take effect
|
|
144
144
|
const afterCtrlX = await session.text({
|
|
145
|
-
waitFor: (text) => /
|
|
145
|
+
waitFor: (text) => /nt: 0/.test(text),
|
|
146
146
|
timeout: 5000,
|
|
147
147
|
})
|
|
148
|
-
expect(afterCtrlX).toContain('
|
|
148
|
+
expect(afterCtrlX).toContain('nt: 0')
|
|
149
149
|
}, 30000)
|
|
150
150
|
|
|
151
151
|
test('alt+d shortcut should trigger Double action directly', async () => {
|
|
152
152
|
// Wait for list to render
|
|
153
153
|
await session.text({
|
|
154
|
-
waitFor: (text) => /
|
|
154
|
+
waitFor: (text) => /nt: 0/.test(text),
|
|
155
155
|
})
|
|
156
156
|
|
|
157
157
|
// Increment twice via Enter to get count=2
|
|
158
158
|
await session.press('return')
|
|
159
159
|
await session.text({
|
|
160
|
-
waitFor: (text) => /
|
|
160
|
+
waitFor: (text) => /nt: 1/.test(text),
|
|
161
161
|
timeout: 5000,
|
|
162
162
|
})
|
|
163
163
|
await session.press('return')
|
|
164
164
|
await session.text({
|
|
165
|
-
waitFor: (text) => /
|
|
165
|
+
waitFor: (text) => /nt: 2/.test(text),
|
|
166
166
|
timeout: 5000,
|
|
167
167
|
})
|
|
168
168
|
|
|
@@ -171,8 +171,8 @@ test('alt+d shortcut should trigger Double action directly', async () => {
|
|
|
171
171
|
|
|
172
172
|
// Wait for the double to take effect
|
|
173
173
|
const afterAltD = await session.text({
|
|
174
|
-
waitFor: (text) => /
|
|
174
|
+
waitFor: (text) => /nt: 4/.test(text),
|
|
175
175
|
timeout: 5000,
|
|
176
176
|
})
|
|
177
|
-
expect(afterAltD).toContain('
|
|
177
|
+
expect(afterAltD).toContain('nt: 4')
|
|
178
178
|
}, 30000)
|
|
@@ -151,7 +151,7 @@ function BarGraphWeeklyExample() {
|
|
|
151
151
|
<List.Item.Detail
|
|
152
152
|
metadata={
|
|
153
153
|
<List.Item.Detail.Metadata>
|
|
154
|
-
<BarGraph height={10} labels={manyColsLabels}>
|
|
154
|
+
<BarGraph height={10} labels={manyColsLabels} barWidth={1}>
|
|
155
155
|
{manyColsSeries.map((s, i) => {
|
|
156
156
|
return <BarGraph.Series key={i} data={s.data} title={s.title} />
|
|
157
157
|
})}
|
|
@@ -261,4 +261,4 @@ function BarGraphWeeklyExample() {
|
|
|
261
261
|
)
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
renderWithProviders(<BarGraphWeeklyExample />)
|
|
264
|
+
void renderWithProviders(<BarGraphWeeklyExample />)
|