termcast 1.3.50 → 1.3.51

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 (164) hide show
  1. package/dist/apis/environment.d.ts +1 -0
  2. package/dist/apis/environment.d.ts.map +1 -1
  3. package/dist/apis/environment.js +5 -0
  4. package/dist/apis/environment.js.map +1 -1
  5. package/dist/app.d.ts +33 -0
  6. package/dist/app.d.ts.map +1 -0
  7. package/dist/app.js +1125 -0
  8. package/dist/app.js.map +1 -0
  9. package/dist/cli.js +80 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/components/detail.d.ts.map +1 -1
  12. package/dist/components/detail.js +20 -17
  13. package/dist/components/detail.js.map +1 -1
  14. package/dist/components/dropdown.d.ts.map +1 -1
  15. package/dist/components/dropdown.js +3 -2
  16. package/dist/components/dropdown.js.map +1 -1
  17. package/dist/components/footer.d.ts +6 -0
  18. package/dist/components/footer.d.ts.map +1 -1
  19. package/dist/components/footer.js +15 -6
  20. package/dist/components/footer.js.map +1 -1
  21. package/dist/components/form/checkbox.d.ts.map +1 -1
  22. package/dist/components/form/checkbox.js +1 -13
  23. package/dist/components/form/checkbox.js.map +1 -1
  24. package/dist/components/form/date-picker.js +2 -2
  25. package/dist/components/form/date-picker.js.map +1 -1
  26. package/dist/components/form/description.js +1 -1
  27. package/dist/components/form/description.js.map +1 -1
  28. package/dist/components/form/dropdown.d.ts.map +1 -1
  29. package/dist/components/form/dropdown.js +19 -3
  30. package/dist/components/form/dropdown.js.map +1 -1
  31. package/dist/components/form/file-picker.d.ts.map +1 -1
  32. package/dist/components/form/file-picker.js +22 -4
  33. package/dist/components/form/file-picker.js.map +1 -1
  34. package/dist/components/form/index.d.ts +3 -1
  35. package/dist/components/form/index.d.ts.map +1 -1
  36. package/dist/components/form/index.js +6 -4
  37. package/dist/components/form/index.js.map +1 -1
  38. package/dist/components/form/password-field.js +3 -3
  39. package/dist/components/form/password-field.js.map +1 -1
  40. package/dist/components/form/text-area.d.ts.map +1 -1
  41. package/dist/components/form/text-area.js +29 -6
  42. package/dist/components/form/text-area.js.map +1 -1
  43. package/dist/components/form/text-field.js +3 -3
  44. package/dist/components/form/text-field.js.map +1 -1
  45. package/dist/components/heatmap.d.ts +80 -0
  46. package/dist/components/heatmap.d.ts.map +1 -0
  47. package/dist/components/heatmap.js +405 -0
  48. package/dist/components/heatmap.js.map +1 -0
  49. package/dist/components/list.d.ts +2 -0
  50. package/dist/components/list.d.ts.map +1 -1
  51. package/dist/components/list.js +80 -52
  52. package/dist/components/list.js.map +1 -1
  53. package/dist/components/markdown.d.ts +7 -0
  54. package/dist/components/markdown.d.ts.map +1 -0
  55. package/dist/components/markdown.js +19 -0
  56. package/dist/components/markdown.js.map +1 -0
  57. package/dist/components/metadata.d.ts.map +1 -1
  58. package/dist/components/metadata.js +4 -1
  59. package/dist/components/metadata.js.map +1 -1
  60. package/dist/components/progress-bar.d.ts +37 -0
  61. package/dist/components/progress-bar.d.ts.map +1 -0
  62. package/dist/components/progress-bar.js +34 -0
  63. package/dist/components/progress-bar.js.map +1 -0
  64. package/dist/components/table.d.ts +3 -2
  65. package/dist/components/table.d.ts.map +1 -1
  66. package/dist/components/table.js +78 -63
  67. package/dist/components/table.js.map +1 -1
  68. package/dist/diagram-parser.d.ts +17 -3
  69. package/dist/diagram-parser.d.ts.map +1 -1
  70. package/dist/diagram-parser.js +17 -3
  71. package/dist/diagram-parser.js.map +1 -1
  72. package/dist/examples/list-slot.d.ts +2 -0
  73. package/dist/examples/list-slot.d.ts.map +1 -0
  74. package/dist/examples/list-slot.js +14 -0
  75. package/dist/examples/list-slot.js.map +1 -0
  76. package/dist/examples/list-with-dropdown.js +2 -4
  77. package/dist/examples/list-with-dropdown.js.map +1 -1
  78. package/dist/examples/simple-heatmap.d.ts +2 -0
  79. package/dist/examples/simple-heatmap.d.ts.map +1 -0
  80. package/dist/examples/simple-heatmap.js +37 -0
  81. package/dist/examples/simple-heatmap.js.map +1 -0
  82. package/dist/examples/simple-progress-bar.d.ts +2 -0
  83. package/dist/examples/simple-progress-bar.d.ts.map +1 -0
  84. package/dist/examples/simple-progress-bar.js +36 -0
  85. package/dist/examples/simple-progress-bar.js.map +1 -0
  86. package/dist/index.d.ts +6 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +6 -0
  89. package/dist/index.js.map +1 -1
  90. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  91. package/dist/internal/date-picker-widget.js +5 -4
  92. package/dist/internal/date-picker-widget.js.map +1 -1
  93. package/dist/internal/navigation.d.ts.map +1 -1
  94. package/dist/internal/navigation.js +7 -2
  95. package/dist/internal/navigation.js.map +1 -1
  96. package/dist/internal/providers.d.ts.map +1 -1
  97. package/dist/internal/providers.js +42 -4
  98. package/dist/internal/providers.js.map +1 -1
  99. package/dist/logger.js +6 -1
  100. package/dist/logger.js.map +1 -1
  101. package/dist/state.d.ts +2 -0
  102. package/dist/state.d.ts.map +1 -1
  103. package/dist/state.js +31 -2
  104. package/dist/state.js.map +1 -1
  105. package/dist/theme.d.ts +1 -0
  106. package/dist/theme.d.ts.map +1 -1
  107. package/dist/theme.js +23 -1
  108. package/dist/theme.js.map +1 -1
  109. package/dist/utils.d.ts.map +1 -1
  110. package/dist/utils.js +6 -1
  111. package/dist/utils.js.map +1 -1
  112. package/package.json +3 -3
  113. package/src/apis/environment.tsx +6 -0
  114. package/src/app.tsx +1487 -0
  115. package/src/assets/default-app-icon.png +0 -0
  116. package/src/cli.tsx +105 -0
  117. package/src/components/detail.tsx +32 -22
  118. package/src/components/dropdown.tsx +3 -2
  119. package/src/components/footer.tsx +37 -7
  120. package/src/components/form/checkbox.tsx +2 -17
  121. package/src/components/form/date-picker.tsx +2 -2
  122. package/src/components/form/description.tsx +1 -1
  123. package/src/components/form/dropdown.tsx +22 -3
  124. package/src/components/form/file-picker.tsx +33 -10
  125. package/src/components/form/index.tsx +10 -6
  126. package/src/components/form/password-field.tsx +3 -3
  127. package/src/components/form/text-area.tsx +31 -6
  128. package/src/components/form/text-field.tsx +3 -3
  129. package/src/components/heatmap.tsx +584 -0
  130. package/src/components/list.tsx +135 -72
  131. package/src/components/markdown.tsx +30 -0
  132. package/src/components/metadata.tsx +9 -2
  133. package/src/components/progress-bar.tsx +112 -0
  134. package/src/components/table.tsx +88 -71
  135. package/src/diagram-parser.tsx +17 -3
  136. package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
  137. package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
  138. package/src/examples/form-basic.vitest.tsx +117 -16
  139. package/src/examples/graph-bar-chart.vitest.tsx +2 -2
  140. package/src/examples/graph-row.vitest.tsx +10 -10
  141. package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
  142. package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
  143. package/src/examples/list-dropdown-default.vitest.tsx +78 -58
  144. package/src/examples/list-slot.tsx +38 -0
  145. package/src/examples/list-with-detail.vitest.tsx +8 -8
  146. package/src/examples/list-with-dropdown.tsx +2 -2
  147. package/src/examples/list-with-dropdown.vitest.tsx +16 -16
  148. package/src/examples/list-with-sections.vitest.tsx +45 -32
  149. package/src/examples/simple-detail-table.vitest.tsx +2 -2
  150. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  151. package/src/examples/simple-grid.vitest.tsx +27 -53
  152. package/src/examples/simple-heatmap.tsx +63 -0
  153. package/src/examples/simple-heatmap.vitest.tsx +88 -0
  154. package/src/examples/simple-progress-bar.tsx +82 -0
  155. package/src/examples/simple-progress-bar.vitest.tsx +72 -0
  156. package/src/examples/table-edge-cases.vitest.tsx +1 -1
  157. package/src/index.tsx +19 -0
  158. package/src/internal/date-picker-widget.tsx +23 -12
  159. package/src/internal/navigation.tsx +7 -2
  160. package/src/internal/providers.tsx +48 -3
  161. package/src/logger.tsx +6 -1
  162. package/src/state.tsx +38 -2
  163. package/src/theme.tsx +26 -2
  164. package/src/utils.tsx +6 -1
@@ -79,7 +79,9 @@ export class TableRenderable extends Renderable {
79
79
  }
80
80
 
81
81
  set rows(value: TableCellContent[][]) {
82
- const structureChanged = value.length !== this._rows.length
82
+ const newColCount = value[0]?.length || 0
83
+ const oldColCount = this._rows[0]?.length || 0
84
+ const structureChanged = value.length !== this._rows.length || newColCount !== oldColCount
83
85
  this._rows = value
84
86
  this._tableDirty = true
85
87
  if (structureChanged) this._tableStructureDirty = true
@@ -164,7 +166,7 @@ export class TableRenderable extends Renderable {
164
166
  this.remove(child.id)
165
167
  }
166
168
 
167
- const colCount = this._headers.length
169
+ const colCount = this._headers.length || this._rows[0]?.length || 0
168
170
  if (colCount === 0 || this._rows.length === 0) return
169
171
 
170
172
  const headingStyle =
@@ -193,31 +195,35 @@ export class TableRenderable extends Renderable {
193
195
  headerFg: StyleDefinition['fg'],
194
196
  stripeBg: StyleDefinition['fg'],
195
197
  ): void {
198
+ const hasHeaders = this._headers.length > 0
199
+
196
200
  for (let col = 0; col < colCount; col++) {
197
201
  const columnBox = new BoxRenderable(this.ctx, {
198
202
  id: `${this.id}-col-${col}`,
199
203
  flexDirection: 'column',
200
204
  })
201
205
 
202
- const headerContent = this._headers[col] ?? ''
203
- let headerStyledText = this.toStyledText(headerContent)
204
- headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
206
+ if (hasHeaders) {
207
+ const headerContent = this._headers[col] ?? ''
208
+ let headerStyledText = this.toStyledText(headerContent)
209
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
205
210
 
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)
211
+ const headerBox = new BoxRenderable(this.ctx, {
212
+ id: `${this.id}-col-${col}-header-box`,
213
+ backgroundColor: headerBg,
214
+ })
215
+ headerBox.add(
216
+ new TextRenderable(this.ctx, {
217
+ id: `${this.id}-col-${col}-header`,
218
+ content: headerStyledText,
219
+ height: 1,
220
+ overflow: 'hidden',
221
+ paddingLeft: 1,
222
+ paddingRight: 1,
223
+ }),
224
+ )
225
+ columnBox.add(headerBox)
226
+ }
221
227
 
222
228
  for (let row = 0; row < this._rows.length; row++) {
223
229
  const cell = this._rows[row]?.[col] ?? ''
@@ -254,28 +260,30 @@ export class TableRenderable extends Renderable {
254
260
  headerFg: StyleDefinition['fg'],
255
261
  stripeBg: StyleDefinition['fg'],
256
262
  ): 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
- )
263
+ if (this._headers.length > 0) {
264
+ const headerRow = new BoxRenderable(this.ctx, {
265
+ id: `${this.id}-header-row`,
266
+ flexDirection: 'row',
267
+ backgroundColor: headerBg,
268
+ })
269
+ for (let col = 0; col < colCount; col++) {
270
+ const headerContent = this._headers[col] ?? ''
271
+ let headerStyledText = this.toStyledText(headerContent)
272
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
273
+
274
+ headerRow.add(
275
+ new TextRenderable(this.ctx, {
276
+ id: `${this.id}-header-${col}`,
277
+ content: headerStyledText,
278
+ flexGrow: 1,
279
+ flexBasis: 0,
280
+ paddingLeft: 1,
281
+ paddingRight: 1,
282
+ }),
283
+ )
284
+ }
285
+ this.add(headerRow)
277
286
  }
278
- this.add(headerRow)
279
287
 
280
288
  for (let row = 0; row < this._rows.length; row++) {
281
289
  const isOddRow = row % 2 === 1
@@ -322,7 +330,8 @@ export class TableRenderable extends Renderable {
322
330
  const stripeBg = concealStyle?.bg
323
331
 
324
332
  const columns = (this as any)._childrenInLayoutOrder as Renderable[]
325
- const colCount = this._headers.length
333
+ const colCount = this._headers.length || this._rows[0]?.length || 0
334
+ const hasHeaders = this._headers.length > 0
326
335
 
327
336
  for (let col = 0; col < colCount; col++) {
328
337
  const columnBox = columns[col]
@@ -330,23 +339,26 @@ export class TableRenderable extends Renderable {
330
339
 
331
340
  const columnChildren = (columnBox as any)._childrenInLayoutOrder as Renderable[]
332
341
 
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
342
+ if (hasHeaders) {
343
+ // Update header
344
+ const headerBox = columnChildren[0]
345
+ if (headerBox instanceof BoxRenderable) {
346
+ headerBox.backgroundColor = headerBg ?? 'transparent'
347
+ const headerChildren = (headerBox as any)._childrenInLayoutOrder as Renderable[]
348
+ const headerText = headerChildren[0]
349
+ if (headerText instanceof TextRenderable) {
350
+ const headerContent = this._headers[col] ?? ''
351
+ let headerStyledText = this.toStyledText(headerContent)
352
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
353
+ headerText.content = headerStyledText
354
+ }
344
355
  }
345
356
  }
346
357
 
347
358
  // Update data rows
359
+ const rowOffset = hasHeaders ? 1 : 0
348
360
  for (let row = 0; row < this._rows.length; row++) {
349
- const cellContainer = columnChildren[row + 1]
361
+ const cellContainer = columnChildren[row + rowOffset]
350
362
  if (cellContainer instanceof BoxRenderable) {
351
363
  const isOddRow = row % 2 === 1
352
364
  cellContainer.backgroundColor = isOddRow && stripeBg ? stripeBg : 'transparent'
@@ -370,25 +382,29 @@ export class TableRenderable extends Renderable {
370
382
  const stripeBg = concealStyle?.bg
371
383
 
372
384
  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
385
+ const colCount = this._headers.length || this._rows[0]?.length || 0
386
+ const hasHeaders = this._headers.length > 0
387
+
388
+ if (hasHeaders) {
389
+ const headerRow = allRows[0]
390
+ if (headerRow instanceof BoxRenderable) {
391
+ headerRow.backgroundColor = headerBg ?? 'transparent'
392
+ const headerCells = (headerRow as any)._childrenInLayoutOrder as Renderable[]
393
+ for (let col = 0; col < colCount; col++) {
394
+ const headerText = headerCells[col]
395
+ if (headerText instanceof TextRenderable) {
396
+ const headerContent = this._headers[col] ?? ''
397
+ let headerStyledText = this.toStyledText(headerContent)
398
+ headerStyledText = this.styledHeaderChunks(headerStyledText, headingStyle, headerFg)
399
+ headerText.content = headerStyledText
400
+ }
386
401
  }
387
402
  }
388
403
  }
389
404
 
405
+ const rowOffset = hasHeaders ? 1 : 0
390
406
  for (let row = 0; row < this._rows.length; row++) {
391
- const rowBox = allRows[row + 1]
407
+ const rowBox = allRows[row + rowOffset]
392
408
  if (!(rowBox instanceof BoxRenderable)) continue
393
409
 
394
410
  const isOddRow = row % 2 === 1
@@ -461,8 +477,9 @@ type TableLayoutProps = Partial<
461
477
  >
462
478
 
463
479
  export interface TableProps extends TableLayoutProps {
464
- /** Column header labels */
465
- headers: string[]
480
+ /** Column header labels. When omitted, no header row is rendered — useful for
481
+ * key-value tables where headers like "Field"/"Value" add no information. */
482
+ headers?: string[]
466
483
  /** Row data – each inner array is one row of cell strings */
467
484
  rows: string[][]
468
485
  /** When true, cell text wraps instead of being truncated to one line. Default false. */
@@ -474,7 +491,7 @@ export interface TableProps extends TableLayoutProps {
474
491
  }
475
492
 
476
493
  export function Table(props: TableProps): any {
477
- const { headers, rows, wrapText, width = '100%', height, ...layoutProps } = props
494
+ const { headers = [], rows, wrapText, width = '100%', height, ...layoutProps } = props
478
495
 
479
496
  const themeName = useStore((state) => state.currentThemeName)
480
497
  const syntaxStyle = React.useMemo(() => {
@@ -128,9 +128,23 @@ export function diagramToDebugString(parsed: ParsedDiagramLine[]): string {
128
128
 
129
129
  /**
130
130
  * Convert ASCII diagram characters to Unicode box-drawing equivalents.
131
- * Eliminates visual gaps between lines.
132
- * - `|` -> `│` (vertical lines)
133
- * - `--` or more -> `──` (horizontal lines, but not single hyphens in text)
131
+ *
132
+ * ASCII glyphs have visible gaps in monospaced fonts because they don't
133
+ * fill the entire terminal cell:
134
+ * `|` — the pipe glyph is shorter than the cell height, so stacked
135
+ * pipes show a gap between every row.
136
+ * `-` — the hyphen glyph is narrower than the cell width and sits
137
+ * centered, so consecutive hyphens show gaps between each cell.
138
+ *
139
+ * Unicode box-drawing characters are designed to span the full cell,
140
+ * connecting seamlessly with adjacent cells:
141
+ * `│` (U+2502) — fills full cell height, no vertical gaps.
142
+ * `─` (U+2500) — fills full cell width, no horizontal gaps.
143
+ *
144
+ * Replacements:
145
+ * `|` -> `│` (every occurrence)
146
+ * `--` (2+) -> `──` (only runs of 2+ to avoid converting single
147
+ * hyphens in regular text like "e-mail")
134
148
  */
135
149
  export function convertAsciiToUnicode(content: string): string {
136
150
  return content
@@ -87,10 +87,10 @@ test('many columns (20) clips with overflow hidden', async () => {
87
87
 
88
88
  > Search...
89
89
 
90
- Weekly Traffic 3 channels across 6 d │ ███ ███ ███
91
- Revenue by Region EMEA / APAC / Amer │ ███ ███ ███ ███
92
- Server Load CPU / Memory / IO │ ███ ███ ███ ███ ███ ███
93
- ›Many Columns (20) Overflow test with │ ███ ███ ███ ███ ███ ███ ███ ███ ██
90
+ Weekly Traffic 3 channels across 6 d │
91
+ Revenue by Region EMEA / APAC / Amer │ ███
92
+ Server Load CPU / Memory / IO │ ███ ███ ███ ███ ██
93
+ ›Many Columns (20) Overflow test with │ ███ ███ ███ ███ ███ ███ ██
94
94
  Many Series (8) Legend overflow test │ ███ ███ ███ ███ ███ ███ ███ ███ ██
95
95
  Long Labels Labels wider than bar co │ ███ ███ ███ ███ ███ ███ ███ ███ ██
96
96
  Week 1 vs Week 2 Two graphs in a Row │ ███ ███ ███ ███ ███ ███ ███ ███ ██
@@ -91,7 +91,7 @@ test('detail metadata showcase renders markdown and metadata together', async ()
91
91
 
92
92
  Team: Platform
93
93
 
94
- ────────────────────────────────────────────────────────────────────────────────────────────
94
+ ─────────────────────────────────────────────────────────────────────────────────────────────
95
95
 
96
96
  Status: Active
97
97
 
@@ -101,7 +101,7 @@ test('detail metadata showcase renders markdown and metadata together', async ()
101
101
 
102
102
  Risk: Medium
103
103
 
104
- ────────────────────────────────────────────────────────────────────────────────────────────
104
+ ─────────────────────────────────────────────────────────────────────────────────────────────
105
105
 
106
106
  Description: This is a comprehensive metadata showcase that demonstrates all the different
107
107
  ways you can display information using the Detail.Metadata component.
@@ -112,7 +112,7 @@ test('detail metadata showcase renders markdown and metadata together', async ()
112
112
 
113
113
  Reviewer: Bob Smith
114
114
 
115
- ────────────────────────────────────────────────────────────────────────────────────────────
115
+ ─────────────────────────────────────────────────────────────────────────────────────────────
116
116
 
117
117
  Repository: github.com/example
118
118
 
@@ -120,7 +120,7 @@ test('detail metadata showcase renders markdown and metadata together', async ()
120
120
 
121
121
  PR Link: github.com/organization/repository/pull/12345
122
122
 
123
- ────────────────────────────────────────────────────────────────────────────────────────────
123
+ ─────────────────────────────────────────────────────────────────────────────────────────────
124
124
 
125
125
  Labels: documentation enhancement good first issue
126
126
 
@@ -134,7 +134,7 @@ test('detail metadata showcase renders markdown and metadata together', async ()
134
134
 
135
135
  Due Date: 2024-02-01
136
136
 
137
- ────────────────────────────────────────────────────────────────────────────────────────────
137
+ ─────────────────────────────────────────────────────────────────────────────────────────────
138
138
 
139
139
  Metrics
140
140
 
@@ -147,6 +147,7 @@ test('detail metadata showcase renders markdown and metadata together', async ()
147
147
  Watchers: @alice @bob @charlie
148
148
 
149
149
 
150
+ esc go back ^k actions powered by termcast.app
150
151
 
151
152
 
152
153
 
@@ -159,7 +160,6 @@ test('detail metadata showcase renders markdown and metadata together', async ()
159
160
 
160
161
 
161
162
 
162
- esc go back ^k actions powered by termcast.app
163
163
 
164
164
  "
165
165
  `)
@@ -443,7 +443,7 @@ test('detail metadata renders tag lists with multiple items', async () => {
443
443
 
444
444
  Team: Platform
445
445
 
446
- ────────────────────────────────────────────────────────────────────────────────────────────
446
+ ─────────────────────────────────────────────────────────────────────────────────────────────
447
447
 
448
448
  Status: Active
449
449
 
@@ -453,7 +453,7 @@ test('detail metadata renders tag lists with multiple items', async () => {
453
453
 
454
454
  Risk: Medium
455
455
 
456
- ────────────────────────────────────────────────────────────────────────────────────────────
456
+ ─────────────────────────────────────────────────────────────────────────────────────────────
457
457
 
458
458
  Description: This is a comprehensive metadata showcase that demonstrates all the different
459
459
  ways you can display information using the Detail.Metadata component.
@@ -464,7 +464,7 @@ test('detail metadata renders tag lists with multiple items', async () => {
464
464
 
465
465
  Reviewer: Bob Smith
466
466
 
467
- ────────────────────────────────────────────────────────────────────────────────────────────
467
+ ─────────────────────────────────────────────────────────────────────────────────────────────
468
468
 
469
469
  Repository: github.com/example
470
470
 
@@ -472,7 +472,7 @@ test('detail metadata renders tag lists with multiple items', async () => {
472
472
 
473
473
  PR Link: github.com/organization/repository/pull/12345
474
474
 
475
- ────────────────────────────────────────────────────────────────────────────────────────────
475
+ ─────────────────────────────────────────────────────────────────────────────────────────────
476
476
 
477
477
  Labels: documentation enhancement good first issue
478
478
 
@@ -486,7 +486,7 @@ test('detail metadata renders tag lists with multiple items', async () => {
486
486
 
487
487
  Due Date: 2024-02-01
488
488
 
489
- ────────────────────────────────────────────────────────────────────────────────────────────
489
+ ─────────────────────────────────────────────────────────────────────────────────────────────
490
490
 
491
491
  Metrics
492
492
 
@@ -499,6 +499,7 @@ test('detail metadata renders tag lists with multiple items', async () => {
499
499
  Watchers: @alice @bob @charlie
500
500
 
501
501
 
502
+ esc go back ^k actions powered by termcast.app
502
503
 
503
504
 
504
505
 
@@ -511,7 +512,6 @@ test('detail metadata renders tag lists with multiple items', async () => {
511
512
 
512
513
 
513
514
 
514
- esc go back ^k actions powered by termcast.app
515
515
 
516
516
  "
517
517
  `)
@@ -3,6 +3,22 @@ import { launchTerminal, Session } from 'tuistory/src'
3
3
 
4
4
  let session: Session
5
5
 
6
+ function escapeRegExp(value: string) {
7
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
8
+ }
9
+
10
+ function focusedFieldRegex(title: string) {
11
+ return new RegExp(`◆\\s+${escapeRegExp(title)}`, 'm')
12
+ }
13
+
14
+ async function waitForFocusedField(title: string) {
15
+ return session.text({
16
+ waitFor: (text) => {
17
+ return focusedFieldRegex(title).test(text)
18
+ },
19
+ })
20
+ }
21
+
6
22
  beforeEach(async () => {
7
23
  session = await launchTerminal({
8
24
  command: 'bun',
@@ -204,8 +220,8 @@ test('form date picker selection with space and enter', async () => {
204
220
 
205
221
  ◇ Date of Birth
206
222
 
207
- │ ← 2026
208
- │ ← February
223
+ │ ← 2026
224
+ │ ← February
209
225
 
210
226
 
211
227
  ctrl ↵ submit tab navigate ^k actions
@@ -262,8 +278,8 @@ test('form date picker selection with space and enter', async () => {
262
278
 
263
279
  ◇ Date of Birth
264
280
 
265
- │ ← 2026
266
- │ ← February
281
+ │ ← 2026
282
+ │ ← February
267
283
 
268
284
 
269
285
  ctrl ↵ submit tab navigate ^k actions
@@ -321,8 +337,8 @@ test('form date picker selection with space and enter', async () => {
321
337
 
322
338
  ◇ Date of Birth
323
339
 
324
- │ ← 2026
325
- │ ← February
340
+ │ ← 2026
341
+ │ ← February
326
342
 
327
343
 
328
344
  ctrl ↵ submit tab navigate ^k actions
@@ -396,8 +412,8 @@ test('form dropdown navigation', async () => {
396
412
 
397
413
  ◇ Date of Birth
398
414
 
399
- │ ← 2026
400
- │ ← February
415
+ │ ← 2026
416
+ │ ← February
401
417
 
402
418
 
403
419
  ctrl ↵ submit tab navigate ^k actions
@@ -454,8 +470,8 @@ test('form dropdown navigation', async () => {
454
470
 
455
471
  ◇ Date of Birth
456
472
 
457
- │ ← 2026
458
- │ ← February
473
+ │ ← 2026
474
+ │ ← February
459
475
 
460
476
 
461
477
  ctrl ↵ submit tab navigate ^k actions
@@ -514,8 +530,8 @@ test('form dropdown navigation', async () => {
514
530
 
515
531
  ◇ Date of Birth
516
532
 
517
- │ ← 2026
518
- │ ← February
533
+ │ ← 2026
534
+ │ ← February
519
535
 
520
536
 
521
537
  ctrl ↵ submit tab navigate ^k actions
@@ -572,8 +588,8 @@ test('form dropdown navigation', async () => {
572
588
 
573
589
  ◇ Date of Birth
574
590
 
575
- │ ← 2026
576
- │ ← February
591
+ │ ← 2026
592
+ │ ← February
577
593
 
578
594
 
579
595
  ctrl ↵ submit tab navigate ^k actions
@@ -746,8 +762,8 @@ test('arrow down from checkbox to dropdown lands on first item', async () => {
746
762
 
747
763
  ◇ Date of Birth
748
764
 
749
- │ ← 2026
750
- │ ← February
765
+ │ ← 2026
766
+ │ ← February
751
767
 
752
768
 
753
769
  ctrl ↵ submit tab navigate ^k actions
@@ -917,3 +933,88 @@ test('textarea arrow keys move focus between adjacent form fields', async () =>
917
933
  const afterUpSnapshot = await session.text()
918
934
  expect(afterUpSnapshot).toMatch(/◆\s+Biography/)
919
935
  }, 10000)
936
+
937
+ test('arrow up/down navigates all form inputs and respects widget edges', async () => {
938
+ await session.text({
939
+ waitFor: (text) => {
940
+ return /Form Component Demo/i.test(text)
941
+ },
942
+ })
943
+
944
+ await session.press('tab')
945
+ let text = await waitForFocusedField('Username')
946
+ expect(text).toMatch(/◆\s+Username/)
947
+
948
+ await session.press('down')
949
+ text = await waitForFocusedField('Password')
950
+
951
+ await session.press('down')
952
+ text = await waitForFocusedField('Biography')
953
+
954
+ await session.type('line 1')
955
+ await session.press('enter')
956
+ await session.type('line 2')
957
+
958
+ await session.press('up')
959
+ text = await waitForFocusedField('Biography')
960
+
961
+ await session.press('up')
962
+ text = await waitForFocusedField('Password')
963
+
964
+ await session.press('down')
965
+ text = await waitForFocusedField('Biography')
966
+
967
+ await session.press('down')
968
+ text = await waitForFocusedField('Biography')
969
+
970
+ await session.press('down')
971
+ text = await waitForFocusedField('Email Preferences')
972
+
973
+ await session.press('down')
974
+ text = await waitForFocusedField('Country')
975
+ expect(text).toMatch(/›.*United States/)
976
+
977
+ await session.press('up')
978
+ text = await waitForFocusedField('Email Preferences')
979
+
980
+ await session.press('down')
981
+ text = await waitForFocusedField('Country')
982
+ expect(text).toMatch(/›.*United States/)
983
+
984
+ await session.press('down')
985
+ await session.press('down')
986
+ await session.press('down')
987
+ await session.press('down')
988
+ await session.press('down')
989
+
990
+ text = await waitForFocusedField('Country')
991
+ expect(text).toMatch(/›.*Germany/)
992
+
993
+ await session.press('down')
994
+ text = await waitForFocusedField('Empty Dropdown')
995
+
996
+ await session.press('down')
997
+ text = await waitForFocusedField('Minimal Field')
998
+
999
+ await session.press('down')
1000
+ text = await waitForFocusedField('Date of Birth')
1001
+
1002
+ await session.press('down')
1003
+ text = await waitForFocusedField('Date of Birth')
1004
+
1005
+ let reachedFilePicker = false
1006
+ for (let i = 0; i < 8; i++) {
1007
+ await session.press('down')
1008
+ const nextText = await session.text()
1009
+ if (focusedFieldRegex('Upload Documents').test(nextText)) {
1010
+ reachedFilePicker = true
1011
+ break
1012
+ }
1013
+ }
1014
+
1015
+ expect(reachedFilePicker).toBe(true)
1016
+
1017
+ await session.press('up')
1018
+ text = await waitForFocusedField('Date of Birth')
1019
+ expect(text).toMatch(/◆\s+Date of Birth/)
1020
+ }, 20000)
@@ -35,7 +35,7 @@ test('initial render shows bar chart for Monthly Budget', async () => {
35
35
  ›Monthly Budget Spent / Remaining / Savings │ ┌Spent: 78.6%┐
36
36
  Disk Usage System / Apps / Media / Free │
37
37
  Investment Portfolio Stocks / Bonds / Cash / C │
38
- CPU Time User / System / IO Wait / Idle │ ────────────────────────────────────────────
38
+ CPU Time User / System / IO Wait / Idle │ ───────────────────────────────────────────
39
39
  Revenue by Product 6 product lines │
40
40
  A/B Test Split Control vs Variant (50/50) │ Total: $6,174
41
41
  Storage Full 100% used │
@@ -46,7 +46,7 @@ test('initial render shows bar chart for Monthly Budget', async () => {
46
46
  Stress Test (20 items) Many small equal segmen │
47
47
 
48
48
 
49
- ↵ open detail ↑↓ navigate ^k actions
49
+
50
50
 
51
51
 
52
52
 
@@ -39,17 +39,17 @@ test('side detail shows two graphs in a row', async () => {
39
39
  Mixed Variants Area left, Striped right │ Filled chart (right) shows memory steadily
40
40
  Sparse Data (Zeros) Filled vs Striped with zer │ climbing.
41
41
 
42
- │ 100│ ⡀ 100│
43
- │ │ ⣼⣷⡀ │ ▄▄▀▀
44
- │ 67│ ⣸⣿⡄ ⣸⣿⣿⣧ 67│ ▄▄▀▀▀▀▀▀▀
45
- │ │ ⣼⣶⣿⣿⣷⡀ ⢰⣿⣿⣿⣿⣧ │ ▄▄▀▀▀▀▀▀▀▀▀▀▀▀
46
- │ │ ⢸⣿⣿⣿⣿⣿⣷⣀⣿⣿⣿⣿⣿⣿⣇ │▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
47
- │ 33│⣀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧ 33│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
48
- │⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
49
- │ 0│⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 0│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
50
- │ 0h 6h 12h 18h24h 0h 6h 12h 18h24h
42
+ │ 100│ ⡀ 100│
43
+ │ │ ⢠⣾⣷⡀ │ ▄▄▀▀
44
+ │ 67│ ⣰⣿⣧ ⢀⣿⣿⣿⣇ 67│ ▄▄▀▀▀▀▀▀▀
45
+ │ │ ⢠⣷⣿⣿⣿⣆ ⣾⣿⣿⣿⣿⡄ │ ▄▄▀▀▀▀▀▀▀▀▀▀▀▀
46
+ │ │ ⢀⣿⣿⣿⣿⣿⣿⣆⣸⣿⣿⣿⣿⣿⣿⣄ │▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
47
+ │ 33│⣀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧ 33│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
48
+ │⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
49
+ │ 0│⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 0│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
50
+ │ 0h 6h 12h 18h24h 0h 6h 12h 18h24h
51
51
 
52
- ───────────────────────────────────────────
52
+ ────────────────────────────────────────────
53
53
 
54
54
  │ CPU Peak: 90%
55
55