termcast 1.3.46 → 1.3.47

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 (85) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +12 -3
  3. package/dist/build.js.map +1 -1
  4. package/dist/components/actions.d.ts +18 -0
  5. package/dist/components/actions.d.ts.map +1 -1
  6. package/dist/components/actions.js +70 -5
  7. package/dist/components/actions.js.map +1 -1
  8. package/dist/components/detail.d.ts.map +1 -1
  9. package/dist/components/detail.js +3 -1
  10. package/dist/components/detail.js.map +1 -1
  11. package/dist/components/dropdown.d.ts.map +1 -1
  12. package/dist/components/dropdown.js +40 -11
  13. package/dist/components/dropdown.js.map +1 -1
  14. package/dist/components/form/dropdown.d.ts.map +1 -1
  15. package/dist/components/form/dropdown.js +26 -10
  16. package/dist/components/form/dropdown.js.map +1 -1
  17. package/dist/components/list.d.ts +7 -0
  18. package/dist/components/list.d.ts.map +1 -1
  19. package/dist/components/list.js +82 -15
  20. package/dist/components/list.js.map +1 -1
  21. package/dist/components/metadata.d.ts.map +1 -1
  22. package/dist/components/metadata.js +2 -1
  23. package/dist/components/metadata.js.map +1 -1
  24. package/dist/components/spinner.d.ts +6 -0
  25. package/dist/components/spinner.d.ts.map +1 -0
  26. package/dist/components/spinner.js +12 -0
  27. package/dist/components/spinner.js.map +1 -0
  28. package/dist/examples/action-shortcut.d.ts +2 -0
  29. package/dist/examples/action-shortcut.d.ts.map +1 -0
  30. package/dist/examples/action-shortcut.js +20 -0
  31. package/dist/examples/action-shortcut.js.map +1 -0
  32. package/dist/examples/internal/scrollbox-with-descendants.js +19 -8
  33. package/dist/examples/internal/scrollbox-with-descendants.js.map +1 -1
  34. package/dist/examples/list-spacing-default.d.ts +5 -0
  35. package/dist/examples/list-spacing-default.d.ts.map +1 -0
  36. package/dist/examples/list-spacing-default.js +10 -0
  37. package/dist/examples/list-spacing-default.js.map +1 -0
  38. package/dist/examples/list-spacing-mode.d.ts +10 -0
  39. package/dist/examples/list-spacing-mode.d.ts.map +1 -0
  40. package/dist/examples/list-spacing-mode.js +26 -0
  41. package/dist/examples/list-spacing-mode.js.map +1 -0
  42. package/dist/examples/list-spacing-relaxed.d.ts +5 -0
  43. package/dist/examples/list-spacing-relaxed.d.ts.map +1 -0
  44. package/dist/examples/list-spacing-relaxed.js +10 -0
  45. package/dist/examples/list-spacing-relaxed.js.map +1 -0
  46. package/dist/index.d.ts +2 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/state.d.ts +9 -0
  51. package/dist/state.d.ts.map +1 -1
  52. package/dist/state.js +2 -0
  53. package/dist/state.js.map +1 -1
  54. package/package.json +3 -3
  55. package/src/build.tsx +12 -3
  56. package/src/components/actions.tsx +77 -5
  57. package/src/components/detail.tsx +3 -1
  58. package/src/components/dropdown.tsx +49 -11
  59. package/src/components/form/dropdown.tsx +32 -10
  60. package/src/components/list.tsx +157 -18
  61. package/src/components/metadata.tsx +3 -2
  62. package/src/components/spinner.tsx +25 -0
  63. package/src/examples/action-shortcut.tsx +53 -0
  64. package/src/examples/action-shortcut.vitest.tsx +178 -0
  65. package/src/examples/actions-context.vitest.tsx +4 -4
  66. package/src/examples/detail-metadata-showcase.vitest.tsx +17 -3
  67. package/src/examples/form-dropdown.vitest.tsx +8 -8
  68. package/src/examples/form-tagpicker.vitest.tsx +4 -4
  69. package/src/examples/github.vitest.tsx +16 -9
  70. package/src/examples/internal/scrollbox-with-descendants.tsx +27 -8
  71. package/src/examples/list-detail-metadata.vitest.tsx +3 -1
  72. package/src/examples/list-loading-empty-view.vitest.tsx +5 -5
  73. package/src/examples/list-scrollbox.vitest.tsx +5 -5
  74. package/src/examples/list-spacing-default.tsx +38 -0
  75. package/src/examples/list-spacing-mode.tsx +137 -0
  76. package/src/examples/list-spacing-mode.vitest.tsx +158 -0
  77. package/src/examples/list-spacing-relaxed.tsx +38 -0
  78. package/src/examples/list-with-detail.vitest.tsx +63 -28
  79. package/src/examples/list-with-sections.vitest.tsx +13 -13
  80. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  81. package/src/examples/simple-grid.vitest.tsx +42 -35
  82. package/src/examples/simple-navigation.vitest.tsx +14 -14
  83. package/src/extensions/dev.vitest.tsx +9 -3
  84. package/src/index.tsx +2 -0
  85. package/src/state.tsx +13 -0
@@ -351,11 +351,11 @@ test('form dropdown keyboard navigation', async () => {
351
351
  ◆ Programming Languages █
352
352
  │ TypeScript, Rust █
353
353
  │ █
354
- │ ○ React █
355
- │ ○ Vue █
356
354
  │› ○ Svelte █
357
355
  │ Backend █
358
356
  │ ○ Node.js █
357
+ │ ○ Python █
358
+ │ ○ Go █
359
359
  │ █
360
360
  │ Choose your preferred programming languages █
361
361
  │ █
@@ -465,11 +465,11 @@ test('form dropdown keyboard navigation', async () => {
465
465
  ◆ Programming Languages █
466
466
  │ TypeScript, Rust █
467
467
  │ █
468
- │ ○ React █
469
- │ ○ Vue █
470
468
  │› ○ Svelte █
471
469
  │ Backend █
472
470
  │ ○ Node.js █
471
+ │ ○ Python █
472
+ │ ○ Go █
473
473
  │ █
474
474
  │ Choose your preferred programming languages █
475
475
  │ █
@@ -522,11 +522,11 @@ test('form dropdown keyboard navigation', async () => {
522
522
  ◆ Programming Languages █
523
523
  │ TypeScript, Rust █
524
524
  │ █
525
- │ ○ React █
526
- │ ○ Vue █
527
525
  │› ○ Svelte █
528
526
  │ Backend █
529
527
  │ ○ Node.js █
528
+ │ ○ Python █
529
+ │ ○ Go █
530
530
  │ █
531
531
  │ Choose your preferred programming languages █
532
532
  │ █
@@ -715,11 +715,11 @@ test('selecting second-to-last visible item should not scroll', async () => {
715
715
  ◆ Programming Languages █
716
716
  │ TypeScript, Rust █
717
717
  │ █
718
+ │ Frontend █
718
719
  │ ● TypeScript █
719
720
  │ ○ JavaScript █
720
721
  │› ○ React █
721
722
  │ ○ Vue █
722
- │ ○ Svelte █
723
723
  │ █
724
724
  │ Choose your preferred programming languages █
725
725
  │ █
@@ -772,11 +772,11 @@ test('selecting second-to-last visible item should not scroll', async () => {
772
772
  ◆ Programming Languages █
773
773
  │ TypeScript, Rust, React █
774
774
  │ █
775
+ │ Frontend █
775
776
  │ ● TypeScript █
776
777
  │ ○ JavaScript █
777
778
  │› ● React █
778
779
  │ ○ Vue █
779
- │ ○ Svelte █
780
780
  │ █
781
781
  │ Choose your preferred programming languages █
782
782
  │ █
@@ -395,11 +395,11 @@ test('form tagpicker keyboard navigation', async () => {
395
395
  ◆ Favorite Sport
396
396
  │ Choose your favorite sport...
397
397
 
398
+ │ ○ Basketball
399
+ │ ○ Football
398
400
  │ ○ Tennis
399
401
  │ ○ Baseball
400
402
  │› ○ Golf
401
- │ ○ Swimming
402
- │ ○ Cycling
403
403
 
404
404
  │ Select your favorite sport from the list
405
405
 
@@ -509,11 +509,11 @@ test('form tagpicker keyboard navigation', async () => {
509
509
  ◆ Favorite Sport
510
510
  │ Choose your favorite sport...
511
511
 
512
- │ ○ Tennis
513
512
  │ ○ Baseball
514
513
  │› ○ Golf
515
514
  │ ○ Swimming
516
515
  │ ○ Cycling
516
+ │ ○ Running
517
517
 
518
518
  │ Select your favorite sport from the list
519
519
 
@@ -566,11 +566,11 @@ test('form tagpicker keyboard navigation', async () => {
566
566
  ◆ Favorite Sport
567
567
  │ Choose your favorite sport...
568
568
 
569
- │ ○ Tennis
570
569
  │ ○ Baseball
571
570
  │› ○ Golf
572
571
  │ ○ Swimming
573
572
  │ ○ Cycling
573
+ │ ○ Running
574
574
 
575
575
  │ Select your favorite sport from the list
576
576
 
@@ -61,6 +61,13 @@ test.skipIf(!extensionExists)('github extension shows command list on launch', a
61
61
  timeout: 30000,
62
62
  })
63
63
 
64
+ // Wait for the full command list to render.
65
+ // The list can paint the first item before all descendants are registered.
66
+ await session.text({
67
+ waitFor: (text) => text.includes('My Pull Requests') && text.includes('Search Repositories'),
68
+ timeout: 30000,
69
+ })
70
+
64
71
  expect(initialView).toContain('My Pull Requests')
65
72
  expect(initialView).toContain('Search Repositories')
66
73
  expect(initialView).toMatchInlineSnapshot(`
@@ -101,7 +108,7 @@ test.skipIf(!extensionExists)('github extension shows command list on launch', a
101
108
  test.skipIf(!extensionExists)('github extension can navigate commands', async () => {
102
109
  // Wait for command list
103
110
  await session.text({
104
- waitFor: (text) => /My Pull Requests|Search Repositories/i.test(text),
111
+ waitFor: (text) => text.includes('My Pull Requests') && text.includes('Search Repositories'),
105
112
  timeout: 30000,
106
113
  })
107
114
 
@@ -172,26 +179,26 @@ test.skipIf(!extensionExists)('github extension can open actions panel', async (
172
179
 
173
180
  > Search commands...
174
181
 
175
- Commands
176
- ›My Pull Requests List pull requests you created, participated in, or view
177
182
  ╭──────────────────────────────────────────────────────────────────────────╮
178
183
  │ │
179
184
  │ Actions esc │
180
185
  │ │
181
186
  │ > Search actions... │
182
187
  │ │
183
- │ ›Run Command
184
- │ Copy File Path
185
- │ Copy Command Info
186
-
188
+ │ ›Run Command
189
+ │ Copy File Path
190
+ │ Copy Command Info
191
+
187
192
  │ Settings │
188
- │ Configure GitHub... ⌃⇧,
193
+ │ Configure GitHub... ⌃⇧,
189
194
  │ Change Theme... │
195
+ │ See Console Logs │
196
+ │ │
197
+ │ │
190
198
  │ │
191
199
  │ ↵ select ↑↓ navigate │
192
200
  │ │
193
201
  ╰──────────────────────────────────────────────────────────────────────────╯
194
- ↵ run command ↑↓ navigate ^k actions powered by termcast
195
202
 
196
203
 
197
204
 
@@ -16,20 +16,39 @@ function ScrollboxWithDescendants() {
16
16
  const [selectedIndex, setSelectedIndex] = React.useState(0)
17
17
  const scrollBoxRef = React.useRef<any>(null)
18
18
 
19
- const scrollToItem = (item: { props?: ItemDescendant }) => {
19
+ const scrollToItemIfNeeded = ({
20
+ item,
21
+ direction,
22
+ }: {
23
+ item: { props?: ItemDescendant }
24
+ direction: -1 | 1
25
+ }) => {
20
26
  const scrollBox = scrollBoxRef.current
21
27
  const elementRef = item.props?.elementRef
22
28
  if (!scrollBox || !elementRef) return
23
29
 
24
30
  const contentY = scrollBox.content?.y || 0
31
+ const scrollTop = scrollBox.scrollTop || 0
25
32
  const viewportHeight = scrollBox.viewport?.height || 10
26
33
 
27
- // Calculate item position relative to content
28
34
  const itemTop = elementRef.y - contentY
35
+ const itemHeight = elementRef.height || 1
36
+ const itemBottom = itemTop + itemHeight
29
37
 
30
- // Scroll so the top of the item is centered in the viewport
31
- const targetScrollTop = itemTop - viewportHeight / 2
32
- scrollBox.scrollTo(Math.max(0, targetScrollTop))
38
+ const viewportTop = scrollTop
39
+ const viewportBottom = scrollTop + viewportHeight
40
+
41
+ if (direction === 1) {
42
+ if (itemBottom > viewportBottom) {
43
+ scrollBox.scrollTo(Math.max(0, itemTop))
44
+ }
45
+ return
46
+ }
47
+
48
+ if (itemTop < viewportTop) {
49
+ const targetScrollTop = itemBottom - viewportHeight
50
+ scrollBox.scrollTo(Math.max(0, targetScrollTop))
51
+ }
33
52
  }
34
53
 
35
54
  const move = (direction: -1 | 1) => {
@@ -40,15 +59,15 @@ function ScrollboxWithDescendants() {
40
59
  if (items.length === 0) return
41
60
 
42
61
  let nextIndex = selectedIndex + direction
43
- if (nextIndex < 0) nextIndex = items.length - 1
44
- if (nextIndex >= items.length) nextIndex = 0
62
+ if (nextIndex < 0) return
63
+ if (nextIndex >= items.length) return
45
64
 
46
65
  const nextItem = items[nextIndex]
47
66
  if (nextItem) {
48
67
  flushSync(() => {
49
68
  setSelectedIndex(nextIndex)
50
69
  })
51
- scrollToItem(nextItem)
70
+ scrollToItemIfNeeded({ item: nextItem, direction })
52
71
  }
53
72
  }
54
73
 
@@ -23,7 +23,9 @@ test('list detail metadata label renders short values in row layout (key: value)
23
23
  text.includes('Metadata Test') &&
24
24
  text.includes('Name') &&
25
25
  text.includes('John Doe') &&
26
- text.includes('Email')
26
+ text.includes('Email') &&
27
+ text.includes('Website') &&
28
+ text.includes('↑↓ navigate')
27
29
  )
28
30
  },
29
31
  })
@@ -25,19 +25,19 @@ test('empty view shows spinner when list is loading', async () => {
25
25
  },
26
26
  })
27
27
 
28
- // Spinner animates. Normalize for stable snapshots.
29
- const normalized = text.replace(/[◰◳◲◱]/g, '')
28
+ // Spinner animates (pulsing dots: ' ' · •). Normalize for stable snapshots.
29
+ const normalized = text.replace(/[·•]/g, '')
30
30
  expect(normalized).toMatchInlineSnapshot(`
31
31
  "
32
32
 
33
33
 
34
34
  Loading Empty View ───────────────────────────────────
35
35
 
36
- > Search...
36
+ Search...
37
37
 
38
38
 
39
39
 
40
- loading...
40
+ loading...
41
41
 
42
42
 
43
43
 
@@ -45,5 +45,5 @@ test('empty view shows spinner when list is loading', async () => {
45
45
 
46
46
  ↑↓ navigate ^k actions"
47
47
  `)
48
- expect(text).toMatch(/[◰◳◲◱]\s+loading\.\.\./)
48
+ expect(text).toMatch(/[·• ]\s*loading\.\.\./)
49
49
  }, 10000)
@@ -69,12 +69,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
69
69
 
70
70
  > Search items...
71
71
 
72
+ ○ Item 1 Description for item 1
73
+ ★ Item 2 Description for item 2
72
74
  ◆ Item 3 Description for item 3
73
75
  ↯ Item 4 Description for item 4
74
76
  ▷ Item 5 Description for item 5
75
- ›▦ Item 6 Description for item 6
76
- ◴ Item 7 Description for item 7
77
- ▯ Item 8 Description for item 8"
77
+ ›▦ Item 6 Description for item 6"
78
78
  `)
79
79
 
80
80
  await session.press('down')
@@ -90,12 +90,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
90
90
 
91
91
  > Search items...
92
92
 
93
- ▦ Item 6 Description for item 6
94
93
  ◴ Item 7 Description for item 7
95
94
  ▯ Item 8 Description for item 8
96
95
  ›▤ Item 9 Description for item 9
97
96
  Item 10 Description for item 10
98
- ○ Item 11 Description for item 11"
97
+ ○ Item 11 Description for item 11
98
+ ★ Item 12 Description for item 12"
99
99
  `)
100
100
 
101
101
  await session.press('up')
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Test file for List with spacingMode="default" (single-line items)
3
+ */
4
+
5
+ import { List, Icon, Color, renderWithProviders } from 'termcast'
6
+
7
+ function ListSpacingDefault() {
8
+ return (
9
+ <List navigationTitle="Default Mode" spacingMode="default">
10
+ <List.Section title="With Icons">
11
+ <List.Item
12
+ icon={Icon.Document}
13
+ title="Report"
14
+ subtitle="Q4 financial summary"
15
+ accessories={[{ tag: { value: 'Draft', color: Color.Yellow } }]}
16
+ />
17
+ <List.Item
18
+ icon={Icon.Code}
19
+ title="API Docs"
20
+ subtitle="REST endpoints guide"
21
+ accessories={[{ text: 'v2.1' }]}
22
+ />
23
+ </List.Section>
24
+ <List.Section title="Without Icons">
25
+ <List.Item
26
+ title="Meeting Notes"
27
+ subtitle="Weekly standup points"
28
+ accessories={[{ tag: 'Important' }]}
29
+ />
30
+ </List.Section>
31
+ <List.Section title="No Subtitle">
32
+ <List.Item icon={Icon.Star} title="Favorites" />
33
+ </List.Section>
34
+ </List>
35
+ )
36
+ }
37
+
38
+ await renderWithProviders(<ListSpacingDefault />)
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Example demonstrating List spacingMode prop.
3
+ *
4
+ * - 'default': Single-line items with title and subtitle on same row
5
+ * - 'relaxed': Two-line items with title on first row, subtitle below aligned with title start
6
+ *
7
+ * Use Action to toggle between modes and see the difference.
8
+ */
9
+
10
+ import { useState } from 'react'
11
+ import {
12
+ List,
13
+ ActionPanel,
14
+ Action,
15
+ Icon,
16
+ Color,
17
+ renderWithProviders,
18
+ type ListSpacingMode,
19
+ } from 'termcast'
20
+
21
+ function ListSpacingModeExample() {
22
+ const [spacingMode, setSpacingMode] = useState<ListSpacingMode>('relaxed')
23
+
24
+ const toggleMode = () => {
25
+ setSpacingMode((prev) => (prev === 'default' ? 'relaxed' : 'default'))
26
+ }
27
+
28
+ return (
29
+ <List
30
+ navigationTitle={`Spacing Mode: ${spacingMode}`}
31
+ spacingMode={spacingMode}
32
+ >
33
+ <List.Section title="With Icons" subtitle="Items have icon, title, subtitle">
34
+ <List.Item
35
+ icon={Icon.Document}
36
+ title="Quarterly Report"
37
+ subtitle="Q4 2024 financial summary and projections"
38
+ accessories={[{ tag: { value: 'Draft', color: Color.Yellow } }]}
39
+ actions={
40
+ <ActionPanel>
41
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
42
+ </ActionPanel>
43
+ }
44
+ />
45
+ <List.Item
46
+ icon={Icon.Code}
47
+ title="API Documentation"
48
+ subtitle="REST endpoints and authentication guide"
49
+ accessories={[
50
+ { text: 'v2.1' },
51
+ { date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000) },
52
+ ]}
53
+ actions={
54
+ <ActionPanel>
55
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
56
+ </ActionPanel>
57
+ }
58
+ />
59
+ <List.Item
60
+ icon={Icon.Gear}
61
+ title="Configuration"
62
+ subtitle="System settings and preferences"
63
+ accessories={[{ tag: { value: 'Active', color: Color.Green } }]}
64
+ actions={
65
+ <ActionPanel>
66
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
67
+ </ActionPanel>
68
+ }
69
+ />
70
+ </List.Section>
71
+
72
+ <List.Section title="Without Icons" subtitle="Plain text items">
73
+ <List.Item
74
+ title="Meeting Notes"
75
+ subtitle="Weekly standup discussion points"
76
+ accessories={[{ date: new Date(Date.now() - 60 * 60 * 1000) }]}
77
+ actions={
78
+ <ActionPanel>
79
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
80
+ </ActionPanel>
81
+ }
82
+ />
83
+ <List.Item
84
+ title="Project Timeline"
85
+ subtitle="Milestones and deadlines for Q1"
86
+ accessories={[{ text: { value: 'Important', color: Color.Red } }]}
87
+ actions={
88
+ <ActionPanel>
89
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
90
+ </ActionPanel>
91
+ }
92
+ />
93
+ </List.Section>
94
+
95
+ <List.Section title="No Subtitle" subtitle="Title only items">
96
+ <List.Item
97
+ icon={Icon.Star}
98
+ title="Favorites"
99
+ accessories={[{ tag: '12 items' }]}
100
+ actions={
101
+ <ActionPanel>
102
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
103
+ </ActionPanel>
104
+ }
105
+ />
106
+ <List.Item
107
+ title="Recent"
108
+ accessories={[{ date: new Date() }]}
109
+ actions={
110
+ <ActionPanel>
111
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
112
+ </ActionPanel>
113
+ }
114
+ />
115
+ </List.Section>
116
+
117
+ <List.Section title="Long Content" subtitle="Testing overflow behavior">
118
+ <List.Item
119
+ icon={Icon.Text}
120
+ title="Very Long Title That Might Need Truncation"
121
+ subtitle="This is a particularly verbose subtitle that describes the item in great detail"
122
+ accessories={[
123
+ { tag: { value: 'Beta', color: Color.Blue } },
124
+ { text: 'Updated' },
125
+ ]}
126
+ actions={
127
+ <ActionPanel>
128
+ <Action title="Toggle Spacing Mode" onAction={toggleMode} />
129
+ </ActionPanel>
130
+ }
131
+ />
132
+ </List.Section>
133
+ </List>
134
+ )
135
+ }
136
+
137
+ await renderWithProviders(<ListSpacingModeExample />)
@@ -0,0 +1,158 @@
1
+ /**
2
+ * E2E tests for List spacingMode prop.
3
+ *
4
+ * Tests both 'default' (single-line) and 'relaxed' (two-line) modes
5
+ * to verify layout differences and subtitle alignment.
6
+ */
7
+
8
+ import { test, expect, afterEach, beforeEach, describe } from 'vitest'
9
+ import { launchTerminal, Session } from 'tuistory/src'
10
+
11
+ describe('spacingMode default', () => {
12
+ let session: Session
13
+
14
+ beforeEach(async () => {
15
+ session = await launchTerminal({
16
+ command: 'bun',
17
+ args: ['src/examples/list-spacing-default.tsx'],
18
+ cols: 70,
19
+ rows: 20,
20
+ })
21
+ })
22
+
23
+ afterEach(() => {
24
+ session?.close()
25
+ })
26
+
27
+ test('renders single-line items with title and subtitle on same row', async () => {
28
+ await session.text({
29
+ waitFor: (text) => /Default Mode/i.test(text),
30
+ })
31
+
32
+ const text = await session.text()
33
+ expect(text).toMatchInlineSnapshot(`
34
+ "
35
+
36
+
37
+ Default Mode ───────────────────────────────────────────────────
38
+
39
+ > Search...
40
+
41
+ With Icons
42
+ ›▯ Report Q4 financial summary [Draft]
43
+ ⟨⟩ API Docs REST endpoints guide v2.1
44
+
45
+ Without Icons
46
+ Meeting Notes Weekly standup points [Important]
47
+
48
+ No Subtitle
49
+ ★ Favorites
50
+
51
+
52
+ ↑↓ navigate ^k actions
53
+
54
+ "
55
+ `)
56
+
57
+ // Title and subtitle on same line
58
+ expect(text).toContain('Report')
59
+ expect(text).toContain('Q4 financial')
60
+ }, 10000)
61
+ })
62
+
63
+ describe('spacingMode relaxed', () => {
64
+ let session: Session
65
+
66
+ beforeEach(async () => {
67
+ session = await launchTerminal({
68
+ command: 'bun',
69
+ args: ['src/examples/list-spacing-relaxed.tsx'],
70
+ cols: 70,
71
+ rows: 25,
72
+ })
73
+ })
74
+
75
+ afterEach(() => {
76
+ session?.close()
77
+ })
78
+
79
+ test('renders two-line items with subtitle below title', async () => {
80
+ await session.text({
81
+ waitFor: (text) => /Relaxed Mode/i.test(text),
82
+ })
83
+
84
+ const text = await session.text()
85
+ expect(text).toMatchInlineSnapshot(`
86
+ "
87
+
88
+
89
+ Relaxed Mode ───────────────────────────────────────────────────
90
+
91
+ > Search...
92
+
93
+ With Icons
94
+ ›▯ Report [Draft]
95
+ Q4 financial summary
96
+
97
+ ⟨⟩ API Docs v2.1
98
+ REST endpoints guide
99
+
100
+
101
+ Without Icons
102
+ Meeting Notes [Important]
103
+ Weekly standup points
104
+
105
+
106
+ No Subtitle
107
+
108
+
109
+ ↑↓ navigate ^k actions
110
+
111
+ "
112
+ `)
113
+
114
+ // Should have content
115
+ expect(text).toContain('Report')
116
+ expect(text).toContain('Q4 financial')
117
+ }, 10000)
118
+
119
+ test('navigates through relaxed items', async () => {
120
+ await session.text({
121
+ waitFor: (text) => /Relaxed Mode/i.test(text),
122
+ })
123
+
124
+ await session.press('down')
125
+ const afterDown = await session.text()
126
+ expect(afterDown).toMatchInlineSnapshot(`
127
+ "
128
+
129
+
130
+ Relaxed Mode ───────────────────────────────────────────────────
131
+
132
+ > Search...
133
+
134
+ With Icons
135
+ ▯ Report [Draft]
136
+ Q4 financial summary
137
+
138
+ ›⟨⟩ API Docs v2.1
139
+ REST endpoints guide
140
+
141
+
142
+ Without Icons
143
+ Meeting Notes [Important]
144
+ Weekly standup points
145
+
146
+
147
+ No Subtitle
148
+
149
+
150
+ ↑↓ navigate ^k actions
151
+
152
+ "
153
+ `)
154
+
155
+ // Second item should be selected
156
+ expect(afterDown).toContain('API Docs')
157
+ }, 10000)
158
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Test file for List with spacingMode="relaxed" (two-line items)
3
+ */
4
+
5
+ import { List, Icon, Color, renderWithProviders } from 'termcast'
6
+
7
+ function ListSpacingRelaxed() {
8
+ return (
9
+ <List navigationTitle="Relaxed Mode" spacingMode="relaxed">
10
+ <List.Section title="With Icons">
11
+ <List.Item
12
+ icon={Icon.Document}
13
+ title="Report"
14
+ subtitle="Q4 financial summary"
15
+ accessories={[{ tag: { value: 'Draft', color: Color.Yellow } }]}
16
+ />
17
+ <List.Item
18
+ icon={Icon.Code}
19
+ title="API Docs"
20
+ subtitle="REST endpoints guide"
21
+ accessories={[{ text: 'v2.1' }]}
22
+ />
23
+ </List.Section>
24
+ <List.Section title="Without Icons">
25
+ <List.Item
26
+ title="Meeting Notes"
27
+ subtitle="Weekly standup points"
28
+ accessories={[{ tag: 'Important' }]}
29
+ />
30
+ </List.Section>
31
+ <List.Section title="No Subtitle">
32
+ <List.Item icon={Icon.Star} title="Favorites" />
33
+ </List.Section>
34
+ </List>
35
+ )
36
+ }
37
+
38
+ await renderWithProviders(<ListSpacingRelaxed />)