termcast 1.3.47 → 1.3.49

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 (278) hide show
  1. package/dist/apis/cache.d.ts.map +1 -1
  2. package/dist/apis/cache.js +1 -2
  3. package/dist/apis/cache.js.map +1 -1
  4. package/dist/apis/localstorage.d.ts.map +1 -1
  5. package/dist/apis/localstorage.js +1 -2
  6. package/dist/apis/localstorage.js.map +1 -1
  7. package/dist/apis/sqlite.d.ts +7 -0
  8. package/dist/apis/sqlite.d.ts.map +1 -0
  9. package/dist/apis/sqlite.js +13 -0
  10. package/dist/apis/sqlite.js.map +1 -0
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/build.js +14 -5
  13. package/dist/build.js.map +1 -1
  14. package/dist/cli.js +5 -40
  15. package/dist/cli.js.map +1 -1
  16. package/dist/colors.d.ts +7 -7
  17. package/dist/colors.js +7 -7
  18. package/dist/compile.d.ts +6 -1
  19. package/dist/compile.d.ts.map +1 -1
  20. package/dist/compile.js +46 -27
  21. package/dist/compile.js.map +1 -1
  22. package/dist/components/actions.js +1 -1
  23. package/dist/components/actions.js.map +1 -1
  24. package/dist/components/bar-chart.d.ts +38 -0
  25. package/dist/components/bar-chart.d.ts.map +1 -0
  26. package/dist/components/bar-chart.js +158 -0
  27. package/dist/components/bar-chart.js.map +1 -0
  28. package/dist/components/bar-graph.d.ts +41 -0
  29. package/dist/components/bar-graph.d.ts.map +1 -0
  30. package/dist/components/bar-graph.js +95 -0
  31. package/dist/components/bar-graph.js.map +1 -0
  32. package/dist/components/detail.d.ts.map +1 -1
  33. package/dist/components/detail.js +5 -7
  34. package/dist/components/detail.js.map +1 -1
  35. package/dist/components/footer.d.ts.map +1 -1
  36. package/dist/components/footer.js +8 -9
  37. package/dist/components/footer.js.map +1 -1
  38. package/dist/components/form/date-picker.d.ts.map +1 -1
  39. package/dist/components/form/date-picker.js +7 -1
  40. package/dist/components/form/date-picker.js.map +1 -1
  41. package/dist/components/form/dropdown.d.ts.map +1 -1
  42. package/dist/components/form/dropdown.js +10 -2
  43. package/dist/components/form/dropdown.js.map +1 -1
  44. package/dist/components/form/index.d.ts.map +1 -1
  45. package/dist/components/form/index.js +4 -5
  46. package/dist/components/form/index.js.map +1 -1
  47. package/dist/components/form/use-form-navigation.d.ts.map +1 -1
  48. package/dist/components/form/use-form-navigation.js +6 -0
  49. package/dist/components/form/use-form-navigation.js.map +1 -1
  50. package/dist/components/graph.d.ts +111 -0
  51. package/dist/components/graph.d.ts.map +1 -0
  52. package/dist/components/graph.js +392 -0
  53. package/dist/components/graph.js.map +1 -0
  54. package/dist/components/icon.js +5 -5
  55. package/dist/components/icon.js.map +1 -1
  56. package/dist/components/list.d.ts +53 -5
  57. package/dist/components/list.d.ts.map +1 -1
  58. package/dist/components/list.js +125 -71
  59. package/dist/components/list.js.map +1 -1
  60. package/dist/components/loading-bar.js +3 -3
  61. package/dist/components/loading-bar.js.map +1 -1
  62. package/dist/components/loading-text.d.ts +1 -1
  63. package/dist/components/loading-text.d.ts.map +1 -1
  64. package/dist/components/loading-text.js +3 -1
  65. package/dist/components/loading-text.js.map +1 -1
  66. package/dist/components/metadata.js +2 -2
  67. package/dist/components/metadata.js.map +1 -1
  68. package/dist/components/row.d.ts +10 -0
  69. package/dist/components/row.d.ts.map +1 -0
  70. package/dist/components/row.js +12 -0
  71. package/dist/components/row.js.map +1 -0
  72. package/dist/components/table.d.ts +57 -0
  73. package/dist/components/table.d.ts.map +1 -0
  74. package/dist/components/table.js +365 -0
  75. package/dist/components/table.js.map +1 -0
  76. package/dist/descendants.js +13 -13
  77. package/dist/descendants.js.map +1 -1
  78. package/dist/examples/bar-graph-weekly.d.ts +2 -0
  79. package/dist/examples/bar-graph-weekly.d.ts.map +1 -0
  80. package/dist/examples/bar-graph-weekly.js +95 -0
  81. package/dist/examples/bar-graph-weekly.js.map +1 -0
  82. package/dist/examples/components-weird-places.d.ts +2 -0
  83. package/dist/examples/components-weird-places.d.ts.map +1 -0
  84. package/dist/examples/components-weird-places.js +46 -0
  85. package/dist/examples/components-weird-places.js.map +1 -0
  86. package/dist/examples/graph-bar-chart.d.ts +2 -0
  87. package/dist/examples/graph-bar-chart.d.ts.map +1 -0
  88. package/dist/examples/graph-bar-chart.js +270 -0
  89. package/dist/examples/graph-bar-chart.js.map +1 -0
  90. package/dist/examples/graph-multi-series.d.ts +2 -0
  91. package/dist/examples/graph-multi-series.d.ts.map +1 -0
  92. package/dist/examples/graph-multi-series.js +23 -0
  93. package/dist/examples/graph-multi-series.js.map +1 -0
  94. package/dist/examples/graph-polymarket.d.ts +2 -0
  95. package/dist/examples/graph-polymarket.d.ts.map +1 -0
  96. package/dist/examples/graph-polymarket.js +109 -0
  97. package/dist/examples/graph-polymarket.js.map +1 -0
  98. package/dist/examples/graph-row.d.ts +2 -0
  99. package/dist/examples/graph-row.d.ts.map +1 -0
  100. package/dist/examples/graph-row.js +226 -0
  101. package/dist/examples/graph-row.js.map +1 -0
  102. package/dist/examples/graph-styles.d.ts +2 -0
  103. package/dist/examples/graph-styles.d.ts.map +1 -0
  104. package/dist/examples/graph-styles.js +316 -0
  105. package/dist/examples/graph-styles.js.map +1 -0
  106. package/dist/examples/list-accessory-table.d.ts +2 -0
  107. package/dist/examples/list-accessory-table.d.ts.map +1 -0
  108. package/dist/examples/list-accessory-table.js +46 -0
  109. package/dist/examples/list-accessory-table.js.map +1 -0
  110. package/dist/examples/list-item-accessories.d.ts +2 -0
  111. package/dist/examples/list-item-accessories.d.ts.map +1 -0
  112. package/dist/examples/list-item-accessories.js +27 -0
  113. package/dist/examples/list-item-accessories.js.map +1 -0
  114. package/dist/examples/list-no-actions.d.ts +2 -0
  115. package/dist/examples/list-no-actions.d.ts.map +1 -0
  116. package/dist/examples/list-no-actions.js +7 -0
  117. package/dist/examples/list-no-actions.js.map +1 -0
  118. package/dist/examples/simple-detail-table.d.ts +2 -0
  119. package/dist/examples/simple-detail-table.d.ts.map +1 -0
  120. package/dist/examples/simple-detail-table.js +45 -0
  121. package/dist/examples/simple-detail-table.js.map +1 -0
  122. package/dist/examples/simple-graph.d.ts +2 -0
  123. package/dist/examples/simple-graph.d.ts.map +1 -0
  124. package/dist/examples/simple-graph.js +32 -0
  125. package/dist/examples/simple-graph.js.map +1 -0
  126. package/dist/examples/simple-table-wrap.d.ts +2 -0
  127. package/dist/examples/simple-table-wrap.d.ts.map +1 -0
  128. package/dist/examples/simple-table-wrap.js +37 -0
  129. package/dist/examples/simple-table-wrap.js.map +1 -0
  130. package/dist/examples/table-edge-cases.d.ts +2 -0
  131. package/dist/examples/table-edge-cases.d.ts.map +1 -0
  132. package/dist/examples/table-edge-cases.js +70 -0
  133. package/dist/examples/table-edge-cases.js.map +1 -0
  134. package/dist/examples/table-flex-grow.d.ts +2 -0
  135. package/dist/examples/table-flex-grow.d.ts.map +1 -0
  136. package/dist/examples/table-flex-grow.js +18 -0
  137. package/dist/examples/table-flex-grow.js.map +1 -0
  138. package/dist/extensions/dev.d.ts.map +1 -1
  139. package/dist/extensions/dev.js +5 -1
  140. package/dist/extensions/dev.js.map +1 -1
  141. package/dist/globals.d.ts +1 -0
  142. package/dist/globals.d.ts.map +1 -1
  143. package/dist/globals.js +2 -0
  144. package/dist/globals.js.map +1 -1
  145. package/dist/index.d.ts +10 -0
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +10 -0
  148. package/dist/index.js.map +1 -1
  149. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  150. package/dist/internal/date-picker-widget.js +4 -0
  151. package/dist/internal/date-picker-widget.js.map +1 -1
  152. package/dist/internal/providers.d.ts.map +1 -1
  153. package/dist/internal/providers.js +1 -3
  154. package/dist/internal/providers.js.map +1 -1
  155. package/dist/logger.d.ts.map +1 -1
  156. package/dist/logger.js +2 -1
  157. package/dist/logger.js.map +1 -1
  158. package/dist/markdown-utils.d.ts +22 -1
  159. package/dist/markdown-utils.d.ts.map +1 -1
  160. package/dist/markdown-utils.js +66 -1
  161. package/dist/markdown-utils.js.map +1 -1
  162. package/dist/opentui.d.ts +4 -0
  163. package/dist/opentui.d.ts.map +1 -0
  164. package/dist/opentui.js +3 -0
  165. package/dist/opentui.js.map +1 -0
  166. package/dist/release.d.ts +2 -1
  167. package/dist/release.d.ts.map +1 -1
  168. package/dist/release.js +2 -1
  169. package/dist/release.js.map +1 -1
  170. package/dist/state.d.ts +1 -0
  171. package/dist/state.d.ts.map +1 -1
  172. package/dist/state.js +1 -1
  173. package/dist/state.js.map +1 -1
  174. package/dist/swift-runtime.d.ts.map +1 -1
  175. package/dist/swift-runtime.js +20 -5
  176. package/dist/swift-runtime.js.map +1 -1
  177. package/dist/theme.d.ts +1 -0
  178. package/dist/theme.d.ts.map +1 -1
  179. package/dist/theme.js +13 -0
  180. package/dist/theme.js.map +1 -1
  181. package/dist/themes/nerv.json +227 -0
  182. package/dist/themes/termcast.json +72 -71
  183. package/dist/themes.d.ts +2 -1
  184. package/dist/themes.d.ts.map +1 -1
  185. package/dist/themes.js +7 -5
  186. package/dist/themes.js.map +1 -1
  187. package/dist/utils.d.ts.map +1 -1
  188. package/dist/utils.js +4 -1
  189. package/dist/utils.js.map +1 -1
  190. package/package.json +12 -4
  191. package/src/apis/cache.test.ts +1 -1
  192. package/src/apis/cache.tsx +1 -2
  193. package/src/apis/localstorage.tsx +1 -2
  194. package/src/apis/sqlite.ts +14 -0
  195. package/src/build.tsx +15 -5
  196. package/src/cli.tsx +5 -49
  197. package/src/colors.tsx +7 -7
  198. package/src/compile.tsx +53 -30
  199. package/src/components/actions.tsx +1 -1
  200. package/src/components/bar-chart.tsx +271 -0
  201. package/src/components/bar-graph.tsx +214 -0
  202. package/src/components/detail.tsx +7 -8
  203. package/src/components/footer.tsx +14 -15
  204. package/src/components/form/date-picker.tsx +9 -0
  205. package/src/components/form/dropdown.tsx +13 -3
  206. package/src/components/form/index.tsx +4 -6
  207. package/src/components/form/use-form-navigation.tsx +6 -0
  208. package/src/components/graph.tsx +506 -0
  209. package/src/components/icon.tsx +5 -5
  210. package/src/components/list.tsx +210 -102
  211. package/src/components/loading-bar.tsx +3 -3
  212. package/src/components/loading-text.tsx +4 -2
  213. package/src/components/metadata.tsx +2 -2
  214. package/src/components/row.tsx +31 -0
  215. package/src/components/table.tsx +511 -0
  216. package/src/descendants.tsx +13 -13
  217. package/src/examples/action-shortcut.vitest.tsx +1 -1
  218. package/src/examples/actions-context.vitest.tsx +1 -1
  219. package/src/examples/bar-graph-weekly.tsx +264 -0
  220. package/src/examples/bar-graph-weekly.vitest.tsx +275 -0
  221. package/src/examples/detail-metadata-showcase.vitest.tsx +22 -22
  222. package/src/examples/form-basic.vitest.tsx +239 -0
  223. package/src/examples/form-dropdown.vitest.tsx +29 -29
  224. package/src/examples/form-tagpicker.vitest.tsx +27 -27
  225. package/src/examples/github.vitest.tsx +4 -4
  226. package/src/examples/graph-bar-chart.tsx +408 -0
  227. package/src/examples/graph-bar-chart.vitest.tsx +283 -0
  228. package/src/examples/graph-multi-series.tsx +36 -0
  229. package/src/examples/graph-multi-series.vitest.tsx +89 -0
  230. package/src/examples/graph-polymarket.tsx +182 -0
  231. package/src/examples/graph-polymarket.vitest.tsx +130 -0
  232. package/src/examples/graph-row.tsx +347 -0
  233. package/src/examples/graph-row.vitest.tsx +295 -0
  234. package/src/examples/graph-styles.tsx +457 -0
  235. package/src/examples/graph-styles.vitest.tsx +322 -0
  236. package/src/examples/list-accessory-table.tsx +77 -0
  237. package/src/examples/list-detail-metadata.vitest.tsx +21 -21
  238. package/src/examples/list-dropdown-default.vitest.tsx +12 -12
  239. package/src/examples/list-item-accessories.tsx +106 -0
  240. package/src/examples/list-item-accessories.vitest.tsx +115 -0
  241. package/src/examples/list-no-actions.tsx +18 -0
  242. package/src/examples/list-no-actions.vitest.tsx +97 -0
  243. package/src/examples/list-spacing-mode.vitest.tsx +6 -6
  244. package/src/examples/list-with-detail.vitest.tsx +73 -73
  245. package/src/examples/list-with-dropdown.vitest.tsx +49 -6
  246. package/src/examples/list-with-sections.vitest.tsx +61 -56
  247. package/src/examples/simple-detail-markdown.vitest.tsx +21 -18
  248. package/src/examples/simple-detail-table.tsx +65 -0
  249. package/src/examples/simple-detail-table.vitest.tsx +200 -0
  250. package/src/examples/simple-graph.tsx +51 -0
  251. package/src/examples/simple-graph.vitest.tsx +124 -0
  252. package/src/examples/simple-grid.vitest.tsx +3 -3
  253. package/src/examples/simple-list-search.vitest.tsx +65 -0
  254. package/src/examples/simple-navigation.vitest.tsx +3 -3
  255. package/src/examples/simple-table-wrap.tsx +55 -0
  256. package/src/examples/simple-table-wrap.vitest.tsx +91 -0
  257. package/src/examples/store.vitest.tsx +1 -1
  258. package/src/examples/table-edge-cases.tsx +72 -0
  259. package/src/examples/table-edge-cases.vitest.tsx +307 -0
  260. package/src/examples/table-flex-grow.tsx +53 -0
  261. package/src/examples/table-flex-grow.vitest.tsx +124 -0
  262. package/src/extensions/dev.tsx +7 -1
  263. package/src/globals.ts +3 -0
  264. package/src/index.tsx +31 -0
  265. package/src/internal/date-picker-widget.tsx +4 -0
  266. package/src/internal/providers.tsx +1 -4
  267. package/src/logger.tsx +2 -1
  268. package/src/markdown-utils.tsx +82 -1
  269. package/src/opentui.tsx +5 -0
  270. package/src/release.tsx +3 -0
  271. package/src/state.tsx +2 -1
  272. package/src/swift-runtime.tsx +19 -5
  273. package/src/theme.tsx +14 -0
  274. package/src/themes/nerv.json +231 -0
  275. package/src/themes/termcast.json +75 -71
  276. package/src/themes.ts +8 -5
  277. package/src/utils.test.tsx +1 -1
  278. package/src/utils.tsx +5 -1
@@ -132,23 +132,62 @@ function CurrentItemActionsOffscreen(props: {
132
132
  const descendantsMap = useListDescendantsRerender()
133
133
 
134
134
  // Get current item's actions
135
- const items = Object.values(descendantsMap)
136
- .filter((item) => item.index !== -1)
137
- .sort((a, b) => a.index - b.index)
138
-
139
- const currentItem = items.find((item) => item.index === props.selectedIndex)
135
+ const currentItem = Object.values(descendantsMap)
136
+ .find((item) => item.index === props.selectedIndex)
140
137
  const actions = currentItem?.props?.actions ?? props.fallbackActions ?? null
141
138
 
142
- // Clear first action title when there are no actions
139
+ const hasExtensionActions = !!actions
140
+
141
+ // Clear firstActionTitle when no extension actions exist, so the footer
142
+ // doesn't show "↵ change theme..." for built-in actions — Enter should only
143
+ // auto-execute extension-provided actions, not built-in ones like Change Theme.
144
+ // This runs after ActionPanel's own layoutEffect which would set it to the
145
+ // first built-in action title.
143
146
  useLayoutEffect(() => {
144
- if (!actions) {
147
+ if (!hasExtensionActions) {
145
148
  useStore.setState({ firstActionTitle: '' })
146
149
  }
147
- }, [actions])
150
+ })
148
151
 
149
- if (!actions) return null
152
+ // Always mount ActionPanel offscreen so built-in actions (Change Theme, etc.)
153
+ // are available via ctrl+k even when extension provides no actions
154
+ return <Offscreen>{actions || <ActionPanel />}</Offscreen>
155
+ }
156
+
157
+ /**
158
+ * Reads the selected item's detail directly from committed descendants map.
159
+ * useDescendantsRerender now notifies on every commit (not just structural changes),
160
+ * so committedMap always has fresh props including detail.
161
+ */
162
+ function CurrentItemDetail(props: {
163
+ selectedIndex: number
164
+ isShowingDetail?: boolean
165
+ }): any {
166
+ const theme = useTheme()
167
+ const descendantsMap = useListDescendantsRerender()
150
168
 
151
- return <Offscreen>{actions}</Offscreen>
169
+ if (!props.isShowingDetail) return null
170
+
171
+ const currentItem = Object.values(descendantsMap)
172
+ .find((item) => item.index === props.selectedIndex)
173
+ const detail = currentItem?.props?.detail ?? null
174
+
175
+ if (!detail) return null
176
+
177
+ return (
178
+ <box
179
+ style={{
180
+ width: '50%',
181
+ paddingLeft: 1,
182
+ paddingRight: 1,
183
+ }}
184
+ border={['left']}
185
+ borderStyle='single'
186
+ borderColor={theme.border}
187
+ >
188
+ {detail}
189
+ </box>
190
+ )
152
191
  }
153
192
 
154
193
  interface NavigationChildInterface {
@@ -208,9 +247,11 @@ export type ItemAccessory =
208
247
  | {
209
248
  tag?:
210
249
  | string
250
+ | null
211
251
  | {
212
252
  value: string
213
253
  color?: Color.ColorLike
254
+ key?: string
214
255
  }
215
256
  }
216
257
  | {
@@ -302,11 +343,58 @@ export interface ListProps
302
343
  selectedItemId?: string
303
344
  isShowingDetail?: boolean
304
345
  /**
305
- * Controls the vertical spacing of list items.
306
- * - 'default': Single-line items with title and subtitle on same row
307
- * - 'relaxed': Two-line items with title on first row, subtitle below, and padding between items
308
- */
346
+ * Controls the vertical spacing of list items.
347
+ * - 'default': Single-line items with title and subtitle on same row
348
+ * - 'relaxed': Two-line items with title on first row, subtitle below, and padding between items
349
+ */
309
350
  spacingMode?: ListSpacingMode
351
+ /**
352
+ * Fixed column widths (in terminal characters) for tag accessories,
353
+ * enabling table-like alignment across all list items.
354
+ *
355
+ * Each number in the array defines the display width for the Nth tag
356
+ * in each item's accessories array. Tags are left-aligned within their
357
+ * column using `padEnd`. Non-tag accessories (`text`, `date`) are not
358
+ * affected and render with their natural width.
359
+ *
360
+ * **Requirements:**
361
+ * - All items should have the same number of tag accessories in the
362
+ * same order. Use `{ tag: null }` as a placeholder for missing tags
363
+ * to preserve column alignment.
364
+ * - Each width should be at least the length of the longest tag value
365
+ * at that position. Tags render as plain text (no brackets added).
366
+ * - The array length should match the number of tag positions.
367
+ *
368
+ * **Example:**
369
+ * ```tsx
370
+ * // Widths: comments max "12 comments"=11, status max "In Progress"=11, priority max "P3"=2
371
+ * <List accessoryTagsLayout={[11, 11, 2]}>
372
+ * <List.Item
373
+ * title="Fix login bug"
374
+ * accessories={[
375
+ * { tag: { value: '3 comments', key: 'comments' } },
376
+ * { tag: { value: 'Open', color: Color.Green, key: 'status' } },
377
+ * { tag: { value: 'P1', color: Color.Red, key: 'priority' } },
378
+ * { date: new Date() },
379
+ * ]}
380
+ * />
381
+ * <List.Item
382
+ * title="Refactor auth"
383
+ * accessories={[
384
+ * { tag: { value: '7 comments', key: 'comments' } },
385
+ * { tag: { value: 'Closed', color: Color.Purple, key: 'status' } },
386
+ * { tag: null }, // placeholder for missing priority column
387
+ * { date: new Date() },
388
+ * ]}
389
+ * />
390
+ * </List>
391
+ *
392
+ * // Renders as:
393
+ * // Fix login bug 3 comments Open P1 1d
394
+ * // Refactor auth 7 comments Closed 2w
395
+ * ```
396
+ */
397
+ accessoryTagsLayout?: number[]
310
398
  }
311
399
 
312
400
  interface ListType {
@@ -370,12 +458,12 @@ interface ListContextValue {
370
458
  setSelectedIndex?: (index: number) => void
371
459
  searchText: string
372
460
  isFiltering: boolean
373
- setCurrentDetail?: (detail: ReactNode) => void
374
461
  isShowingDetail?: boolean
375
462
  customEmptyViewRef: React.MutableRefObject<boolean>
376
463
  isLoading?: boolean
377
464
  hasDropdown?: boolean
378
465
  spacingMode: ListSpacingMode
466
+ accessoryTagWidths?: number[]
379
467
  }
380
468
 
381
469
  const ListContext = createContext<ListContextValue | undefined>(undefined)
@@ -405,6 +493,16 @@ function shouldItemBeVisible(
405
493
  return searchableText.includes(needle)
406
494
  }
407
495
 
496
+ // Get the foreground color for a tag accessory (used in both auto and table modes)
497
+ function getTagColor(tag: ItemAccessory, theme: ReturnType<typeof useTheme>, active: boolean): string | undefined {
498
+ if (active) return theme.background
499
+ if ('tag' in tag && tag.tag) {
500
+ const tagColor = typeof tag.tag === 'object' ? tag.tag?.color : undefined
501
+ return resolveColor(tagColor) || theme.warning
502
+ }
503
+ return undefined
504
+ }
505
+
408
506
  // Create descendants for List items
409
507
  interface ListItemDescendant {
410
508
  id?: string
@@ -651,12 +749,14 @@ function ListItemRow(props: {
651
749
  const theme = useTheme()
652
750
  const listCtx = useContext(ListContext)
653
751
  const spacingMode = listCtx?.spacingMode ?? 'default'
752
+ const accessoryTagWidths = listCtx?.accessoryTagWidths
654
753
  const isRelaxed = spacingMode === 'relaxed'
655
754
  const { title, subtitle, icon, iconColor, accessories, active, ref } = props
656
755
  const [isHovered, setIsHovered] = useState(false)
657
756
 
658
757
  const accessoryElements: ReactNode[] = []
659
758
  if (accessories) {
759
+ let tagIndex = 0
660
760
  accessories.forEach((accessory) => {
661
761
  if ('text' in accessory && accessory.text) {
662
762
  const textValue =
@@ -678,25 +778,35 @@ function ListItemRow(props: {
678
778
  )
679
779
  }
680
780
  }
681
- if ('tag' in accessory && accessory.tag) {
781
+ if ('tag' in accessory) {
782
+ const colWidth = accessoryTagWidths?.[tagIndex]
682
783
  const tagValue =
683
784
  typeof accessory.tag === 'string'
684
785
  ? accessory.tag
685
786
  : accessory.tag?.value
686
- const tagColor =
687
- typeof accessory.tag === 'object' ? accessory.tag?.color : undefined
688
787
  if (tagValue) {
788
+ const tagColor = getTagColor(accessory, theme, !!active)
789
+ const displayText = tagValue
790
+ const padded = colWidth ? displayText.padEnd(colWidth) : displayText
689
791
  accessoryElements.push(
690
792
  <text
691
- key={`tag-${tagValue}`}
793
+ key={`tag-${tagIndex}`}
692
794
  flexShrink={0}
693
- fg={active ? theme.background : resolveColor(tagColor) || theme.warning}
795
+ fg={tagColor}
694
796
  wrapMode="none"
695
797
  >
696
- [{tagValue}]
798
+ {padded}
799
+ </text>,
800
+ )
801
+ } else if (colWidth) {
802
+ // Null/empty tag → empty space placeholder to preserve column alignment
803
+ accessoryElements.push(
804
+ <text key={`tag-empty-${tagIndex}`} flexShrink={0} wrapMode="none">
805
+ {' '.repeat(colWidth)}
697
806
  </text>,
698
807
  )
699
808
  }
809
+ tagIndex++
700
810
  }
701
811
  if ('date' in accessory && accessory.date) {
702
812
  const dateValue =
@@ -722,6 +832,7 @@ function ListItemRow(props: {
722
832
  }
723
833
  }
724
834
  })
835
+
725
836
  }
726
837
 
727
838
  // Calculate subtitle indentation to align with title start
@@ -877,6 +988,8 @@ export const List: ListType = (props) => {
877
988
  selectedItemId,
878
989
  searchBarAccessory,
879
990
  spacingMode = 'default',
991
+ accessoryTagsLayout,
992
+ throttle,
880
993
  ...otherProps
881
994
  } = props
882
995
 
@@ -886,15 +999,29 @@ export const List: ListType = (props) => {
886
999
  const currentItem = stack[stack.length - 1]
887
1000
  return currentItem?.selectedListIndex
888
1001
  })
889
- const [internalSearchText, setInternalSearchText] = useState('')
1002
+ const currentStackSearchText = useStore((state) => {
1003
+ const stack = state.navigationStack
1004
+ const currentItem = stack[stack.length - 1]
1005
+ return currentItem?.searchText
1006
+ })
1007
+ const [internalSearchText, setInternalSearchText] = useState(
1008
+ () => currentStackSearchText ?? '',
1009
+ )
890
1010
  const [selectedIndex, setSelectedIndex] = useState<number>(() => {
891
1011
  return currentStackSelectedListIndex ?? 0
892
1012
  })
893
1013
  const [isDropdownOpen, setIsDropdownOpen] = useState(false)
894
- const [currentDetail, setCurrentDetail] = useState<ReactNode>(null)
895
1014
 
896
1015
  const inputRef = useRef<TextareaRenderable>(null)
897
1016
  const customEmptyViewRef = useRef(false)
1017
+ const throttleTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
1018
+ useEffect(() => {
1019
+ return () => {
1020
+ if (throttleTimeoutRef.current) {
1021
+ clearTimeout(throttleTimeoutRef.current)
1022
+ }
1023
+ }
1024
+ }, [])
898
1025
 
899
1026
  // Ref callback that registers the textarea in global state for ESC handling
900
1027
  const setInputRef = useCallback((node: TextareaRenderable | null) => {
@@ -984,6 +1111,28 @@ export const List: ListType = (props) => {
984
1111
  })
985
1112
  }
986
1113
 
1114
+ // Persist search text in the navigation stack item so it survives push/pop.
1115
+ // Same pattern as persistSelectedIndexInCurrentNavigationItem.
1116
+ const persistSearchTextInCurrentNavigationItem = (text: string) => {
1117
+ useStore.setState((state) => {
1118
+ const stack = state.navigationStack
1119
+ const currentIndex = stack.length - 1
1120
+ const currentItem = stack[currentIndex]
1121
+ if (!currentItem) {
1122
+ return {}
1123
+ }
1124
+ if (currentItem.searchText === text) {
1125
+ return {}
1126
+ }
1127
+ const nextStack = [...stack]
1128
+ nextStack[currentIndex] = {
1129
+ ...currentItem,
1130
+ searchText: text,
1131
+ }
1132
+ return { navigationStack: nextStack }
1133
+ })
1134
+ }
1135
+
987
1136
  const setSelectedIndexWithPersistence = (index: number) => {
988
1137
  setSelectedIndex(index)
989
1138
  persistSelectedIndexInCurrentNavigationItem(index)
@@ -1037,23 +1186,16 @@ export const List: ListType = (props) => {
1037
1186
  setSelectedIndex: setSelectedIndexWithPersistence,
1038
1187
  searchText,
1039
1188
  isFiltering: isFilteringEnabled,
1040
- setCurrentDetail,
1041
1189
  isShowingDetail,
1042
1190
  customEmptyViewRef,
1043
1191
  isLoading,
1044
1192
  hasDropdown: !!searchBarAccessory,
1045
1193
  spacingMode,
1194
+ accessoryTagWidths: accessoryTagsLayout,
1046
1195
  }),
1047
- [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail, isLoading, searchBarAccessory, spacingMode],
1196
+ [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail, isLoading, searchBarAccessory, spacingMode, accessoryTagsLayout],
1048
1197
  )
1049
1198
 
1050
- // Clear detail when detail view is hidden (before paint to avoid flash)
1051
- useLayoutEffect(() => {
1052
- if (!isShowingDetail) {
1053
- setCurrentDetail(null)
1054
- }
1055
- }, [isShowingDetail])
1056
-
1057
1199
  // Handle selectedItemId prop changes (before paint to avoid flash)
1058
1200
  useLayoutEffect(() => {
1059
1201
  // Only update selection if selectedItemId is explicitly provided
@@ -1220,11 +1362,9 @@ export const List: ListType = (props) => {
1220
1362
  const currentItem = items.find((item) => item.index === selectedIndex)
1221
1363
 
1222
1364
  // Handle Ctrl+K to show actions dialog via portal
1365
+ // Always open — built-in actions (Change Theme, etc.) are always available
1223
1366
  if (evt.name === 'k' && evt.ctrl) {
1224
- const hasActions = currentItem?.props?.actions || props.actions
1225
- if (hasActions) {
1226
- useStore.setState({ showActionsDialog: true })
1227
- }
1367
+ useStore.setState({ showActionsDialog: true })
1228
1368
  return
1229
1369
  }
1230
1370
 
@@ -1243,14 +1383,25 @@ export const List: ListType = (props) => {
1243
1383
  const handleSearchChange = (newValue: string) => {
1244
1384
  if (!inFocus) return
1245
1385
 
1246
- // Always call onSearchTextChange if provided
1247
- if (onSearchTextChange) {
1248
- onSearchTextChange(newValue)
1249
- }
1250
-
1386
+ // Always update internal state immediately so the textarea and filtering
1387
+ // stay responsive even when throttle delays the parent callback
1251
1388
  if (controlledSearchText === undefined) {
1252
1389
  setInternalSearchText(newValue)
1253
1390
  }
1391
+ persistSearchTextInCurrentNavigationItem(newValue)
1392
+
1393
+ if (onSearchTextChange) {
1394
+ if (throttle) {
1395
+ if (throttleTimeoutRef.current) {
1396
+ clearTimeout(throttleTimeoutRef.current)
1397
+ }
1398
+ throttleTimeoutRef.current = setTimeout(() => {
1399
+ onSearchTextChange(newValue)
1400
+ }, 300)
1401
+ } else {
1402
+ onSearchTextChange(newValue)
1403
+ }
1404
+ }
1254
1405
  }
1255
1406
 
1256
1407
  return (
@@ -1363,21 +1514,10 @@ export const List: ListType = (props) => {
1363
1514
  </box>
1364
1515
 
1365
1516
  {/* Detail panel on the right */}
1366
- {isShowingDetail && currentDetail && (
1367
- <box
1368
- style={{
1369
- marginTop: 1,
1370
- width: '50%',
1371
- paddingLeft: 1,
1372
- paddingRight: 1,
1373
- }}
1374
- border={['left']}
1375
- borderStyle='single'
1376
- borderColor={theme.border}
1377
- >
1378
- {currentDetail}
1379
- </box>
1380
- )}
1517
+ <CurrentItemDetail
1518
+ selectedIndex={selectedIndex}
1519
+ isShowingDetail={isShowingDetail}
1520
+ />
1381
1521
  </box>
1382
1522
  </box>
1383
1523
  </ListDescendantsProvider>
@@ -1388,28 +1528,14 @@ export const List: ListType = (props) => {
1388
1528
 
1389
1529
  // Wrapper component that only renders children when no visible items exist
1390
1530
  function ShowOnNoItems(props: { children: ReactNode; isCustomEmptyView?: boolean }): any {
1391
- // Subscribe to re-render when items are added/removed
1392
- void useListDescendantsRerender()
1393
- // Get live map ref for reading in useLayoutEffect
1394
- const map = useListDescendantsMap()
1531
+ const descendantsMap = useListDescendantsRerender()
1395
1532
  const listContext = useContext(ListContext)
1396
- const [hasVisibleItems, setHasVisibleItems] = useState(true)
1397
-
1398
- // We must check visibility in useLayoutEffect because:
1399
- // 1. map.current is cleared by reset() during render, so it's empty if read during render
1400
- // 2. committedMap is stale - it's a snapshot from the previous render cycle and doesn't
1401
- // reflect prop changes like 'visible' (only tracks which items exist, not their props)
1402
- // 3. Items register in their own useLayoutEffect, so map.current is only populated after
1403
- // all items' layout effects have run
1404
- useLayoutEffect(() => {
1405
- const items = Object.values(map.current)
1406
- .filter((item) => item.index !== -1 && item.props?.visible !== false)
1407
- // For default empty view, also check if custom empty view exists
1408
- const hasCustomEmptyView = !props.isCustomEmptyView && (listContext?.customEmptyViewRef.current ?? false)
1409
- setHasVisibleItems(items.length > 0 || hasCustomEmptyView)
1410
- })
1411
1533
 
1412
- if (hasVisibleItems) return null
1534
+ const hasVisibleItems = Object.values(descendantsMap)
1535
+ .some((item) => item.index !== -1 && item.props?.visible !== false)
1536
+ const hasCustomEmptyView = !props.isCustomEmptyView && (listContext?.customEmptyViewRef.current ?? false)
1537
+
1538
+ if (hasVisibleItems || hasCustomEmptyView) return null
1413
1539
 
1414
1540
  return props.children
1415
1541
  }
@@ -1520,13 +1646,6 @@ const ListItem: ListItemType = (props) => {
1520
1646
  const selectedIndex = listContext?.selectedIndex ?? 0
1521
1647
  const isActive = index === selectedIndex
1522
1648
 
1523
- // Update detail when this item becomes active or detail prop changes (before paint)
1524
- useLayoutEffect(() => {
1525
- if (isActive && listContext?.isShowingDetail && listContext?.setCurrentDetail) {
1526
- listContext.setCurrentDetail(props.detail || null)
1527
- }
1528
- }, [isActive, props.detail, listContext?.isShowingDetail, listContext?.setCurrentDetail])
1529
-
1530
1649
  // Don't render if not visible
1531
1650
  if (!isVisible) return null
1532
1651
 
@@ -1627,19 +1746,10 @@ const ListItemDetail: ListItemDetailType = (props) => {
1627
1746
  }}
1628
1747
  >
1629
1748
  <box gap={1} style={{ flexDirection: 'column' }}>
1630
- {markdown && (
1749
+ {markdown && markdown.trim().length > 0 && (
1631
1750
  <ListMarkdownContent markdown={markdown} />
1632
1751
  )}
1633
- {metadata && (
1634
- <box
1635
- style={{ paddingTop: 1 }}
1636
- // border={['top']}
1637
- // borderStyle='single'
1638
- // borderColor={theme.border}
1639
- >
1640
- {metadata}
1641
- </box>
1642
- )}
1752
+ {metadata}
1643
1753
  </box>
1644
1754
  </ScrollBox>
1645
1755
  </box>
@@ -1665,13 +1775,13 @@ const ListItemDetailMetadata = (props: MetadataProps) => {
1665
1775
  const listDetailMetadataConfig: MetadataConfig = {
1666
1776
  maxValueLen: 20,
1667
1777
  titleMinWidth: computedTitleWidth,
1668
- paddingBottom: 1, // Use integer to avoid inconsistent rounding
1778
+ paddingBottom: 0,
1669
1779
  separatorWidth: 200, // Will be clipped by overflow: hidden
1670
1780
  }
1671
1781
 
1672
1782
  return (
1673
1783
  <MetadataContext.Provider value={listDetailMetadataConfig}>
1674
- <box style={{ flexDirection: 'column' }}>
1784
+ <box gap={1} style={{ flexDirection: 'column' }}>
1675
1785
  {props.children}
1676
1786
  </box>
1677
1787
  </MetadataContext.Provider>
@@ -1812,7 +1922,6 @@ const ListDropdown: ListDropdownType = (props) => {
1812
1922
  <box
1813
1923
  key={dropdownState.value}
1814
1924
  style={{
1815
- paddingTop: 1,
1816
1925
  paddingLeft: 2,
1817
1926
  // minWidth: value.length + 4,
1818
1927
  flexDirection: 'row',
@@ -2069,10 +2178,9 @@ function EmptyViewContent(props: EmptyViewProps): any {
2069
2178
  if (!inFocus) return
2070
2179
 
2071
2180
  // Handle Ctrl+K to show actions dialog via portal
2181
+ // Always open — built-in actions (Change Theme, etc.) are always available
2072
2182
  if (evt.name === 'k' && evt.ctrl) {
2073
- if (props.actions) {
2074
- useStore.setState({ showActionsDialog: true })
2075
- }
2183
+ useStore.setState({ showActionsDialog: true })
2076
2184
  return
2077
2185
  }
2078
2186
 
@@ -2113,8 +2221,8 @@ function EmptyViewContent(props: EmptyViewProps): any {
2113
2221
  {props.description?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || ''}
2114
2222
  </text>
2115
2223
  )}
2116
- {/* Render actions offscreen to capture them */}
2117
- {props.actions && <Offscreen>{props.actions}</Offscreen>}
2224
+ {/* Always mount ActionPanel offscreen so built-in actions are available */}
2225
+ <Offscreen>{props.actions || <ActionPanel />}</Offscreen>
2118
2226
  </box>
2119
2227
  )
2120
2228
  }
@@ -79,7 +79,7 @@ export function LoadingBar(props: LoadingBarProps): any {
79
79
  const getCharacterColor = (index: number): string => {
80
80
  if (!isLoading) {
81
81
  // When not loading, use default theme colors
82
- return index < title.length ? theme.text : '#626262'
82
+ return index < title.length ? theme.text : theme.border
83
83
  }
84
84
 
85
85
  // Title text stays static when loading, only animate the bar
@@ -96,8 +96,8 @@ export function LoadingBar(props: LoadingBarProps): any {
96
96
  return waveColors[distance]
97
97
  }
98
98
 
99
- // Default muted color for characters outside the wave (xterm 241)
100
- return '#626262'
99
+ // Default muted color for characters outside the wave
100
+ return theme.border
101
101
  }
102
102
 
103
103
  return (
@@ -1,11 +1,12 @@
1
1
  import React from 'react'
2
2
  import { colord } from 'colord'
3
3
  import { useAnimationTick, TICK_DIVISORS } from 'termcast/src/components/animation-tick'
4
+ import { useTheme } from 'termcast/src/theme'
4
5
 
5
6
  interface LoadingTextProps {
6
7
  children: string
7
8
  isLoading?: boolean
8
- color: string
9
+ color?: string
9
10
  }
10
11
 
11
12
  /**
@@ -21,7 +22,8 @@ function generateWaveColors(baseColor: string): string[] {
21
22
  }
22
23
 
23
24
  export function LoadingText(props: LoadingTextProps): any {
24
- const { children, isLoading = false, color = '#FFC000' } = props
25
+ const theme = useTheme()
26
+ const { children, isLoading = false, color = theme.primary } = props
25
27
  const tick = useAnimationTick(isLoading ? TICK_DIVISORS.LOADING_TEXT : 0)
26
28
 
27
29
  const characters = children.split('')
@@ -35,7 +35,7 @@ interface MetadataConfig {
35
35
  const defaultConfig: MetadataConfig = {
36
36
  maxValueLen: 20,
37
37
  titleMinWidth: 12,
38
- paddingBottom: 1,
38
+ paddingBottom: 0,
39
39
  separatorWidth: 30,
40
40
  }
41
41
 
@@ -255,7 +255,7 @@ const Metadata: MetadataType = (props) => {
255
255
 
256
256
  return (
257
257
  <MetadataContext.Provider value={config}>
258
- <box style={{ flexDirection: 'column' }}>{props.children}</box>
258
+ <box gap={1} style={{ flexDirection: 'column' }}>{props.children}</box>
259
259
  </MetadataContext.Provider>
260
260
  )
261
261
  }
@@ -0,0 +1,31 @@
1
+ // Row component - horizontal layout container that distributes space evenly.
2
+ // Wraps each child in a flex-grow box so they split available width equally.
3
+ // Useful for placing multiple graphs or detail panels side by side.
4
+
5
+ import { BoxProps } from '@opentui/react'
6
+ import React, { ReactNode } from 'react'
7
+
8
+ export interface RowProps extends BoxProps {
9
+ /** Gap between children in columns (default: 1) */
10
+ gap?: number
11
+
12
+ children: ReactNode
13
+ }
14
+
15
+ function Row(props: RowProps): any {
16
+ const { gap = 1, children, ...rest } = props
17
+ return (
18
+ <box flexDirection="row" gap={gap} width="100%" {...rest}>
19
+ {React.Children.map(children, (child) => {
20
+ if (!React.isValidElement(child)) return child
21
+ return (
22
+ <box flexGrow={1} flexBasis={0} flexShrink={1}>
23
+ {child}
24
+ </box>
25
+ )
26
+ })}
27
+ </box>
28
+ )
29
+ }
30
+
31
+ export { Row }