termcast 1.3.21 → 1.3.22

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 (60) hide show
  1. package/dist/compile.d.ts.map +1 -1
  2. package/dist/compile.js +2 -0
  3. package/dist/compile.js.map +1 -1
  4. package/dist/components/actions.d.ts.map +1 -1
  5. package/dist/components/actions.js +7 -1
  6. package/dist/components/actions.js.map +1 -1
  7. package/dist/components/detail.d.ts.map +1 -1
  8. package/dist/components/detail.js +2 -0
  9. package/dist/components/detail.js.map +1 -1
  10. package/dist/components/form/index.d.ts.map +1 -1
  11. package/dist/components/form/index.js +3 -1
  12. package/dist/components/form/index.js.map +1 -1
  13. package/dist/components/list.d.ts.map +1 -1
  14. package/dist/components/list.js +11 -5
  15. package/dist/components/list.js.map +1 -1
  16. package/dist/examples/simple-navigation.js +10 -4
  17. package/dist/examples/simple-navigation.js.map +1 -1
  18. package/dist/internal/dialog.d.ts +1 -0
  19. package/dist/internal/dialog.d.ts.map +1 -1
  20. package/dist/internal/dialog.js +23 -14
  21. package/dist/internal/dialog.js.map +1 -1
  22. package/dist/internal/navigation.d.ts +9 -1
  23. package/dist/internal/navigation.d.ts.map +1 -1
  24. package/dist/internal/navigation.js +5 -5
  25. package/dist/internal/navigation.js.map +1 -1
  26. package/dist/internal/providers.d.ts.map +1 -1
  27. package/dist/internal/providers.js +2 -2
  28. package/dist/internal/providers.js.map +1 -1
  29. package/dist/internal/scrollbox.d.ts.map +1 -1
  30. package/dist/internal/scrollbox.js +2 -1
  31. package/dist/internal/scrollbox.js.map +1 -1
  32. package/dist/state.d.ts +1 -0
  33. package/dist/state.d.ts.map +1 -1
  34. package/dist/state.js +2 -0
  35. package/dist/state.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/compile.tsx +2 -0
  38. package/src/components/actions.tsx +8 -1
  39. package/src/components/detail.tsx +2 -0
  40. package/src/components/form/index.tsx +3 -1
  41. package/src/components/list.tsx +17 -9
  42. package/src/examples/file-autocomplete.vitest.tsx +7 -7
  43. package/src/examples/form-basic.vitest.tsx +222 -22
  44. package/src/examples/form-scroll.vitest.tsx +12 -12
  45. package/src/examples/internal/simple-scrollbox.vitest.tsx +7 -5
  46. package/src/examples/list-fetch-data.vitest.tsx +1 -1
  47. package/src/examples/list-scrollbox.vitest.tsx +59 -0
  48. package/src/examples/list-with-detail.vitest.tsx +15 -15
  49. package/src/examples/list-with-dropdown.vitest.tsx +14 -14
  50. package/src/examples/list-with-sections.vitest.tsx +2 -2
  51. package/src/examples/simple-grid.vitest.tsx +70 -70
  52. package/src/examples/simple-navigation.tsx +15 -7
  53. package/src/examples/simple-navigation.vitest.tsx +3 -3
  54. package/src/extensions/dev.vitest.tsx +22 -22
  55. package/src/internal/dialog.tsx +39 -26
  56. package/src/internal/navigation.tsx +3 -1
  57. package/src/internal/providers.tsx +4 -2
  58. package/src/internal/scrollbox.tsx +2 -0
  59. package/src/keyboard.test.tsx +69 -0
  60. package/src/state.tsx +4 -0
@@ -39,19 +39,19 @@ test('grid navigation and display', async () => {
39
39
  Search items...
40
40
 
41
41
  Fruits ▲
42
- Apple
43
- Banana
44
- Cherry
42
+ ›? Apple
43
+ ? Banana
44
+ ? Cherry
45
45
 
46
46
  Animals
47
- Dog
48
- Cat
49
- Rabbit
47
+ ? Dog
48
+ ? Cat
49
+ ? Rabbit
50
50
 
51
51
  Others
52
- House
53
- Car
54
- Rocket
52
+ ? House
53
+ ? Car
54
+ ? Rocket
55
55
 
56
56
 
57
57
  ↵ select ↑↓ navigate ^k actions"
@@ -70,19 +70,19 @@ test('grid navigation and display', async () => {
70
70
  Search items...
71
71
 
72
72
  Fruits ▲
73
- Apple
74
- Banana
75
- Cherry
73
+ ? Apple
74
+ ›? Banana
75
+ ? Cherry
76
76
 
77
77
  Animals
78
- Dog
79
- Cat
80
- Rabbit
78
+ ? Dog
79
+ ? Cat
80
+ ? Rabbit
81
81
 
82
82
  Others
83
- House
84
- Car
85
- Rocket
83
+ ? House
84
+ ? Car
85
+ ? Rocket
86
86
 
87
87
 
88
88
  ↵ select ↑↓ navigate ^k actions"
@@ -102,19 +102,19 @@ test('grid navigation and display', async () => {
102
102
  Search items...
103
103
 
104
104
  Fruits ▲
105
- Apple
106
- Banana
107
- Cherry
105
+ ? Apple
106
+ ? Banana
107
+ ? Cherry
108
108
 
109
109
  Animals
110
- Dog
111
- Cat
112
- Rabbit
110
+ ›? Dog
111
+ ? Cat
112
+ ? Rabbit
113
113
 
114
114
  Others
115
- House
116
- Car
117
- Rocket
115
+ ? House
116
+ ? Car
117
+ ? Rocket
118
118
 
119
119
 
120
120
  ↵ select ↑↓ navigate ^k actions"
@@ -166,19 +166,19 @@ test('grid navigation and display', async () => {
166
166
  Search items...
167
167
 
168
168
  Fruits ▲
169
- Apple
170
- Banana
171
- Cherry
169
+ ? Apple
170
+ ? Banana
171
+ ? Cherry
172
172
 
173
173
  Animals
174
- Dog
175
- Cat
176
- Rabbit
174
+ ›? Dog
175
+ ? Cat
176
+ ? Rabbit
177
177
 
178
178
  Others
179
- House
180
- Car
181
- Rocket
179
+ ? House
180
+ ? Car
181
+ ? Rocket
182
182
 
183
183
 
184
184
  ↵ select ↑↓ navigate ^k actions"
@@ -209,8 +209,8 @@ test('grid search functionality', async () => {
209
209
 
210
210
  cat
211
211
 
212
+ ? Cat
212
213
 
213
- Cat
214
214
 
215
215
 
216
216
 
@@ -247,12 +247,12 @@ test('grid search functionality', async () => {
247
247
 
248
248
  space
249
249
 
250
+ ? Rocket
251
+ ? Star
252
+ ? Moon
253
+ ? Sun
250
254
 
251
255
 
252
- Rocket
253
- Star
254
- Moon
255
- Sun
256
256
 
257
257
 
258
258
 
@@ -283,19 +283,19 @@ test('grid search functionality', async () => {
283
283
  Search items...
284
284
 
285
285
  Fruits ▲
286
- Apple
287
- Banana
288
- Cherry
286
+ ›? Apple
287
+ ? Banana
288
+ ? Cherry
289
289
 
290
290
  Animals
291
- Dog
292
- Cat
293
- Rabbit
291
+ ? Dog
292
+ ? Cat
293
+ ? Rabbit
294
294
 
295
295
  Others
296
- House
297
- Car
298
- Rocket
296
+ ? House
297
+ ? Car
298
+ ? Rocket
299
299
 
300
300
 
301
301
  ↵ select ↑↓ navigate ^k actions"
@@ -424,19 +424,19 @@ test('grid item selection and actions', async () => {
424
424
  Search items...
425
425
 
426
426
  Fruits ▲
427
- Apple
428
- Banana
429
- Cherry
427
+ ›? Apple
428
+ ? Banana
429
+ ? Cherry
430
430
 
431
431
  Animals
432
- Dog
433
- Cat
434
- Rabbit
432
+ ? Dog
433
+ ? Cat
434
+ ? Rabbit
435
435
 
436
436
  Others
437
- House
438
- Car
439
- Rocket
437
+ ? House
438
+ ? Car
439
+ ? Rocket
440
440
 
441
441
 
442
442
  ↵ select ↑↓ navigate ^k actions"
@@ -464,19 +464,19 @@ test('grid mouse interaction', async () => {
464
464
  Search items...
465
465
 
466
466
  Fruits ▲
467
- Apple
468
- Banana
469
- Cherry
467
+ ? Apple
468
+ ? Banana
469
+ ? Cherry
470
470
 
471
471
  Animals
472
- Dog
473
- Cat
474
- Rabbit
472
+ ›? Dog
473
+ ? Cat
474
+ ? Rabbit
475
475
 
476
476
  Others
477
- House
478
- Car
479
- Rocket
477
+ ? House
478
+ ? Car
479
+ ? Rocket
480
480
 
481
481
 
482
482
  ↵ select ↑↓ navigate ^k actions"
@@ -503,7 +503,7 @@ test('grid mouse interaction', async () => {
503
503
 
504
504
  Search items...
505
505
 
506
- Apple
506
+ ? Apple
507
507
  ┃ ┃
508
508
  ┃ esc ┃
509
509
  ┃ ┃
@@ -536,7 +536,7 @@ test('grid mouse interaction', async () => {
536
536
 
537
537
  Search items...
538
538
 
539
- Apple
539
+ ? Apple
540
540
  ┃ ┃
541
541
  ┃ esc ┃
542
542
  ┃ ┃
@@ -6,9 +6,12 @@ import { Action, ActionPanel } from 'termcast'
6
6
  import { useNavigation } from 'termcast/src/internal/navigation'
7
7
  import { TermcastProvider } from 'termcast/src/internal/providers'
8
8
 
9
- function DetailView({ title }: { title: string }): any {
9
+ function GoBackAction(): any {
10
10
  const { pop } = useNavigation()
11
+ return <Action title='Go Back' onAction={() => pop()} />
12
+ }
11
13
 
14
+ function DetailView({ title }: { title: string }): any {
12
15
  return (
13
16
  <List
14
17
  searchBarPlaceholder='Detail view - Press ESC to go back'
@@ -21,7 +24,7 @@ function DetailView({ title }: { title: string }): any {
21
24
  subtitle='Press Enter to go back or ESC to navigate back'
22
25
  actions={
23
26
  <ActionPanel>
24
- <Action title='Go Back' onAction={() => pop()} />
27
+ <GoBackAction />
25
28
  <Action.CopyToClipboard content={title} title='Copy Title' />
26
29
  </ActionPanel>
27
30
  }
@@ -31,9 +34,17 @@ function DetailView({ title }: { title: string }): any {
31
34
  )
32
35
  }
33
36
 
34
- function MainView(): any {
37
+ function OpenDetailsAction({ title }: { title: string }): any {
35
38
  const { push } = useNavigation()
39
+ return (
40
+ <Action
41
+ title='Open Details'
42
+ onAction={() => push(<DetailView title={title} />)}
43
+ />
44
+ )
45
+ }
36
46
 
47
+ function MainView(): any {
37
48
  const items = [
38
49
  {
39
50
  id: 'first',
@@ -62,10 +73,7 @@ function MainView(): any {
62
73
  subtitle={item.subtitle}
63
74
  actions={
64
75
  <ActionPanel>
65
- <Action
66
- title='Open Details'
67
- onAction={() => push(<DetailView title={item.title} />)}
68
- />
76
+ <OpenDetailsAction title={item.title} />
69
77
  <Action.CopyToClipboard
70
78
  content={item.title}
71
79
  title='Copy Title'
@@ -535,9 +535,9 @@ test('navigation with actions panel', async () => {
535
535
  ┃ ┃
536
536
  ┃ ┃
537
537
  ┃ ┃
538
-
539
- ┃ ↵ select ↑↓ navigate
540
- ┃"
538
+ ┌────────────────────────────────────┐
539
+ ┃ ↵ select │↓✓nCopiedeto Clipboard - First Item │
540
+ └────────────────────────────────────┘ ┃"
541
541
  `)
542
542
 
543
543
  // Select Go Back action
@@ -93,19 +93,19 @@ test('selecting command with arguments shows arguments form', async () => {
93
93
  "
94
94
 
95
95
 
96
- With Arguments █
97
- Enter the arguments to run this command. ▀
98
-
99
- ◆ Search query
100
- ┃ Search query
96
+ With Arguments █
97
+ Enter the arguments to run this command. ▀
101
98
 
99
+ ◇ Search query
100
+ │ Search query
101
+
102
102
  ◇ Secret key
103
103
  │ Secret key
104
104
  ◇ Category
105
105
  │ Category
106
106
 
107
107
 
108
- alt ↵ submit ↑↓ navigate ^k actions"
108
+ ctrl ↵ submit ↑↓ navigate ^k actions"
109
109
  `)
110
110
  }, 30000)
111
111
 
@@ -137,19 +137,19 @@ test('can fill arguments and run command', async () => {
137
137
  "
138
138
 
139
139
 
140
- With Arguments █
141
- Enter the arguments to run this command. ▀
142
-
143
- ◆ Search query
144
- ┃ my search term
140
+ With Arguments █
141
+ Enter the arguments to run this command. ▀
145
142
 
143
+ ◇ Search query
144
+ │ Search query
145
+
146
146
  ◇ Secret key
147
147
  │ Secret key
148
148
  ◇ Category
149
149
  │ Category
150
150
 
151
151
 
152
- alt ↵ submit ↑↓ navigate ^k actions"
152
+ ctrl ↵ submit ↑↓ navigate ^k actions"
153
153
  `)
154
154
 
155
155
  // Submit the form with Alt+Enter (opens action panel), then Enter (selects submit)
@@ -171,15 +171,15 @@ test('can fill arguments and run command', async () => {
171
171
  Search...
172
172
 
173
173
  Received Arguments
174
- Search Query my search term
175
- Secret Key (empty)
176
- Category (empty)
174
+ ›♣ Search Query (empty)
175
+ Secret Key (empty)
176
+ Category (empty)
177
177
 
178
178
 
179
179
 
180
- sele┌────────────────────────────────────────┐
181
- │ ✓ Copied to Clipboard - my search term
182
- └────────────────────────────────────────┘"
180
+ select ┌─────────────────────────────────┐
181
+ │ ✓ Copied to Clipboard - (empty)
182
+ └─────────────────────────────────┘"
183
183
  `)
184
184
  }, 30000)
185
185
 
@@ -208,10 +208,10 @@ test('can run simple view command without arguments', async () => {
208
208
  Search...
209
209
 
210
210
  Items ▲
211
- First Item This is the first item
212
- Second Item This is the second item
213
- Third Item This is the third item
214
- Fourth Item This is the fourth item
211
+ ›♠ First Item This is the first item
212
+ Second Item This is the second item
213
+ Third Item This is the third item
214
+ Fourth Item This is the fourth item
215
215
 
216
216
 
217
217
 
@@ -1,5 +1,5 @@
1
1
  import { useKeyboard, useTerminalDimensions } from '@opentui/react'
2
- import React, { type ReactNode, useRef } from 'react'
2
+ import React, { type ReactNode, useRef, useContext } from 'react'
3
3
  import { Theme } from 'termcast/src/theme'
4
4
  import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
5
5
  import { CommonProps } from 'termcast/src/utils'
@@ -10,6 +10,7 @@ import {
10
10
  } from 'termcast/src/state'
11
11
  import { logger } from '../logger'
12
12
  import { ToastOverlay } from 'termcast/src/apis/toast'
13
+ import { NavigationContext } from 'termcast/src/internal/navigation'
13
14
 
14
15
  const Border = {
15
16
  topLeft: '',
@@ -145,31 +146,6 @@ export function DialogProvider(props: DialogProviderProps): any {
145
146
  return (
146
147
  <>
147
148
  <InFocus inFocus={!dialogStack?.length}>{props.children}</InFocus>
148
- {dialogStack.length > 0 && (
149
- <box position='absolute'>
150
- {dialogStack.map((item, index) => {
151
- const isLastItem = index === dialogStack.length - 1
152
- return (
153
- <InFocus key={'dialog' + String(index)} inFocus={isLastItem}>
154
- <Dialog
155
- position={item.position}
156
- onClickOutside={() => {
157
- if (!isLastItem) return
158
- const state = useStore.getState()
159
- if (state.dialogStack.length > 0) {
160
- useStore.setState({
161
- dialogStack: state.dialogStack.slice(0, -1),
162
- })
163
- }
164
- }}
165
- >
166
- {item.element}
167
- </Dialog>
168
- </InFocus>
169
- )
170
- })}
171
- </box>
172
- )}
173
149
  <InFocus inFocus={false}>
174
150
  <ToastOverlay />
175
151
  </InFocus>
@@ -177,6 +153,43 @@ export function DialogProvider(props: DialogProviderProps): any {
177
153
  )
178
154
  }
179
155
 
156
+ export function DialogOverlay(): any {
157
+ const dialogStack = useStore((state) => state.dialogStack)
158
+ const navContext = useContext(NavigationContext)
159
+
160
+ if (dialogStack.length === 0) {
161
+ return null
162
+ }
163
+
164
+ return (
165
+ <box position='absolute'>
166
+ {dialogStack.map((item, index) => {
167
+ const isLastItem = index === dialogStack.length - 1
168
+ return (
169
+ <InFocus key={'dialog' + String(index)} inFocus={isLastItem}>
170
+ <Dialog
171
+ position={item.position}
172
+ onClickOutside={() => {
173
+ if (!isLastItem) return
174
+ const state = useStore.getState()
175
+ if (state.dialogStack.length > 0) {
176
+ useStore.setState({
177
+ dialogStack: state.dialogStack.slice(0, -1),
178
+ })
179
+ }
180
+ }}
181
+ >
182
+ <NavigationContext.Provider value={navContext}>
183
+ {item.element}
184
+ </NavigationContext.Provider>
185
+ </Dialog>
186
+ </InFocus>
187
+ )
188
+ })}
189
+ </box>
190
+ )
191
+ }
192
+
180
193
  export function useDialog() {
181
194
  const dialogStack = useStore((state) => state.dialogStack)
182
195
 
@@ -26,12 +26,13 @@ interface NavigationContextType {
26
26
  isPending: boolean
27
27
  }
28
28
 
29
- const NavigationContext = createContext<NavigationContextType | undefined>(
29
+ export const NavigationContext = createContext<NavigationContextType | undefined>(
30
30
  undefined,
31
31
  )
32
32
 
33
33
  interface NavigationProviderProps extends CommonProps {
34
34
  children: ReactNode
35
+ overlay?: ReactNode
35
36
  }
36
37
 
37
38
  export function NavigationProvider(props: NavigationProviderProps): any {
@@ -139,6 +140,7 @@ export function NavigationProvider(props: NavigationProviderProps): any {
139
140
  {React.cloneElement(currentItem?.element as React.ReactElement, {
140
141
  key: stack.length,
141
142
  })}
143
+ {props.overlay}
142
144
  </NavigationContext.Provider>
143
145
  )
144
146
  }
@@ -6,7 +6,7 @@ import React, {
6
6
  } from 'react'
7
7
  import { QueryClient } from '@tanstack/react-query'
8
8
  import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
9
- import { DialogProvider } from 'termcast/src/internal/dialog'
9
+ import { DialogProvider, DialogOverlay } from 'termcast/src/internal/dialog'
10
10
  import { NavigationProvider } from 'termcast/src/internal/navigation'
11
11
  import { CommonProps } from 'termcast/src/utils'
12
12
  import { Cache } from 'termcast/src/apis/cache'
@@ -420,7 +420,9 @@ export function TermcastProvider(props: ProvidersProps): any {
420
420
  <DialogProvider>
421
421
  <box padding={2}>
422
422
  {/* NavigationProvider must be last to ensure parent providers remain in the tree when navigation changes */}
423
- <NavigationProvider>{props.children}</NavigationProvider>
423
+ <NavigationProvider overlay={<DialogOverlay />}>
424
+ {props.children}
425
+ </NavigationProvider>
424
426
  </box>
425
427
  </DialogProvider>
426
428
  </PersistQueryClientProvider>
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
2
  import Theme from '../theme'
3
+ import { MacOSScrollAccel } from '@opentui/core'
3
4
 
4
5
  interface ScrollBoxProps {
5
6
  children?: React.ReactNode
@@ -23,6 +24,7 @@ export function ScrollBox({
23
24
  <scrollbox
24
25
  ref={ref}
25
26
  focused={focused}
27
+ scrollAcceleration={new MacOSScrollAccel()}
26
28
  flexGrow={flexGrow}
27
29
  flexShrink={flexShrink}
28
30
  style={{
@@ -0,0 +1,69 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { Keyboard } from 'termcast/src/keyboard'
3
+ import type {
4
+ KeyboardKeyEquivalent,
5
+ KeyboardKeyModifier,
6
+ KeyboardShortcut,
7
+ } from 'termcast/src/keyboard'
8
+
9
+ describe('Keyboard', () => {
10
+ test('Keyboard.Shortcut.Common has all expected shortcuts', () => {
11
+ const commonShortcuts = Object.keys(Keyboard.Shortcut.Common)
12
+ expect(commonShortcuts).toMatchInlineSnapshot(`
13
+ [
14
+ "Copy",
15
+ "CopyDeeplink",
16
+ "CopyName",
17
+ "CopyPath",
18
+ "Save",
19
+ "Duplicate",
20
+ "Edit",
21
+ "MoveDown",
22
+ "MoveUp",
23
+ "New",
24
+ "Open",
25
+ "OpenWith",
26
+ "Pin",
27
+ "Refresh",
28
+ "Remove",
29
+ "RemoveAll",
30
+ "ToggleQuickLook",
31
+ ]
32
+ `)
33
+ })
34
+
35
+ test('Keyboard.Shortcut.Common.Open has correct structure', () => {
36
+ expect(Keyboard.Shortcut.Common.Open).toMatchInlineSnapshot(`
37
+ {
38
+ "key": "o",
39
+ "modifiers": [
40
+ "cmd",
41
+ ],
42
+ }
43
+ `)
44
+ })
45
+
46
+ test('Keyboard.Shortcut.Common.Copy has correct structure', () => {
47
+ expect(Keyboard.Shortcut.Common.Copy).toMatchInlineSnapshot(`
48
+ {
49
+ "key": "c",
50
+ "modifiers": [
51
+ "cmd",
52
+ "shift",
53
+ ],
54
+ }
55
+ `)
56
+ })
57
+
58
+ test('Keyboard.Shortcut.Common.Remove uses ctrl modifier', () => {
59
+ expect(Keyboard.Shortcut.Common.Remove).toMatchInlineSnapshot(`
60
+ {
61
+ "key": "x",
62
+ "modifiers": [
63
+ "ctrl",
64
+ ],
65
+ }
66
+ `)
67
+ })
68
+
69
+ })
package/src/state.tsx CHANGED
@@ -30,6 +30,8 @@ interface AppState {
30
30
  // OAuth state
31
31
  googleAccessToken?: string
32
32
  googleIdToken?: string
33
+ // Actions overlay state
34
+ forceShowActionsOverlay: boolean
33
35
  }
34
36
 
35
37
  export const useStore = create<AppState>(() => ({
@@ -48,4 +50,6 @@ export const useStore = create<AppState>(() => ({
48
50
  // OAuth state
49
51
  googleAccessToken: undefined,
50
52
  googleIdToken: undefined,
53
+ // Actions overlay state
54
+ forceShowActionsOverlay: false,
51
55
  }))