termcast 1.3.32 → 1.3.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-utils.d.ts.map +1 -1
- package/dist/action-utils.js +8 -0
- package/dist/action-utils.js.map +1 -1
- package/dist/apis/cache.d.ts +1 -2
- package/dist/apis/cache.d.ts.map +1 -1
- package/dist/apis/cache.js +138 -54
- package/dist/apis/cache.js.map +1 -1
- package/dist/apis/clipboard.d.ts.map +1 -1
- package/dist/apis/clipboard.js +4 -0
- package/dist/apis/clipboard.js.map +1 -1
- package/dist/apis/oauth.d.ts.map +1 -1
- package/dist/apis/oauth.js +31 -4
- package/dist/apis/oauth.js.map +1 -1
- package/dist/build.d.ts +0 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +30 -51
- package/dist/build.js.map +1 -1
- package/dist/cli.js +31 -14
- package/dist/cli.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -1
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.d.ts +14 -0
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +151 -59
- package/dist/components/actions.js.map +1 -1
- package/dist/components/alert.d.ts.map +1 -1
- package/dist/components/alert.js +6 -5
- package/dist/components/alert.js.map +1 -1
- package/dist/components/animation-tick.d.ts +1 -1
- package/dist/components/animation-tick.js +1 -1
- package/dist/components/animation-tick.js.map +1 -1
- package/dist/components/detail.d.ts +5 -31
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +36 -52
- package/dist/components/detail.js.map +1 -1
- package/dist/components/dropdown.d.ts +1 -1
- package/dist/components/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown.js +50 -22
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +19 -18
- package/dist/components/footer.js.map +1 -1
- package/dist/components/form/checkbox.d.ts.map +1 -1
- package/dist/components/form/checkbox.js +12 -11
- package/dist/components/form/checkbox.js.map +1 -1
- package/dist/components/form/date-picker.d.ts.map +1 -1
- package/dist/components/form/date-picker.js +7 -22
- package/dist/components/form/date-picker.js.map +1 -1
- package/dist/components/form/description.d.ts +1 -1
- package/dist/components/form/description.d.ts.map +1 -1
- package/dist/components/form/description.js +6 -5
- package/dist/components/form/description.js.map +1 -1
- package/dist/components/form/dropdown.d.ts.map +1 -1
- package/dist/components/form/dropdown.js +53 -50
- package/dist/components/form/dropdown.js.map +1 -1
- package/dist/components/form/file-autocomplete.d.ts.map +1 -1
- package/dist/components/form/file-autocomplete.js +5 -4
- package/dist/components/form/file-autocomplete.js.map +1 -1
- package/dist/components/form/file-picker.d.ts.map +1 -1
- package/dist/components/form/file-picker.js +23 -22
- package/dist/components/form/file-picker.js.map +1 -1
- package/dist/components/form/form-end.d.ts.map +1 -1
- package/dist/components/form/form-end.js +6 -4
- package/dist/components/form/form-end.js.map +1 -1
- package/dist/components/form/form-field-wrapper.d.ts +15 -0
- package/dist/components/form/form-field-wrapper.d.ts.map +1 -0
- package/dist/components/form/form-field-wrapper.js +29 -0
- package/dist/components/form/form-field-wrapper.js.map +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +31 -30
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/password-field.d.ts.map +1 -1
- package/dist/components/form/password-field.js +7 -6
- package/dist/components/form/password-field.js.map +1 -1
- package/dist/components/form/separator.d.ts.map +1 -1
- package/dist/components/form/separator.js +3 -2
- package/dist/components/form/separator.js.map +1 -1
- package/dist/components/form/tagpicker.d.ts.map +1 -1
- package/dist/components/form/tagpicker.js +2 -1
- package/dist/components/form/tagpicker.js.map +1 -1
- package/dist/components/form/text-area.d.ts.map +1 -1
- package/dist/components/form/text-area.js +7 -6
- package/dist/components/form/text-area.js.map +1 -1
- package/dist/components/form/text-field.d.ts.map +1 -1
- package/dist/components/form/text-field.js +7 -6
- package/dist/components/form/text-field.js.map +1 -1
- package/dist/components/form/use-form-navigation.d.ts.map +1 -1
- package/dist/components/form/use-form-navigation.js +4 -4
- package/dist/components/form/use-form-navigation.js.map +1 -1
- package/dist/components/form/with-left-border.d.ts +15 -0
- package/dist/components/form/with-left-border.d.ts.map +1 -1
- package/dist/components/form/with-left-border.js +21 -9
- package/dist/components/form/with-left-border.js.map +1 -1
- package/dist/components/icon.d.ts +14 -0
- package/dist/components/icon.d.ts.map +1 -1
- package/dist/components/icon.js +60 -0
- package/dist/components/icon.js.map +1 -1
- package/dist/components/image.d.ts +47 -2
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +46 -7
- package/dist/components/image.js.map +1 -1
- package/dist/components/list.d.ts +5 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +188 -132
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-bar.d.ts.map +1 -1
- package/dist/components/loading-bar.js +4 -3
- package/dist/components/loading-bar.js.map +1 -1
- package/dist/components/metadata.d.ts +70 -0
- package/dist/components/metadata.d.ts.map +1 -0
- package/dist/components/metadata.js +82 -0
- package/dist/components/metadata.js.map +1 -0
- package/dist/components/theme-picker.d.ts.map +1 -1
- package/dist/components/theme-picker.js +3 -2
- package/dist/components/theme-picker.js.map +1 -1
- package/dist/descendants-v2.d.ts +60 -0
- package/dist/descendants-v2.d.ts.map +1 -0
- package/dist/descendants-v2.js +144 -0
- package/dist/descendants-v2.js.map +1 -0
- package/dist/examples/actions-context.d.ts +2 -0
- package/dist/examples/actions-context.d.ts.map +1 -0
- package/dist/examples/actions-context.js +33 -0
- package/dist/examples/actions-context.js.map +1 -0
- package/dist/examples/form-basic.d.ts.map +1 -1
- package/dist/examples/form-basic.js +1 -1
- package/dist/examples/form-basic.js.map +1 -1
- package/dist/examples/form-dropdown.js +1 -1
- package/dist/examples/form-dropdown.js.map +1 -1
- package/dist/examples/internal/custom-action-renderables.d.ts +70 -0
- package/dist/examples/internal/custom-action-renderables.d.ts.map +1 -0
- package/dist/examples/internal/custom-action-renderables.js +163 -0
- package/dist/examples/internal/custom-action-renderables.js.map +1 -0
- package/dist/examples/internal/custom-dropdown.d.ts +99 -0
- package/dist/examples/internal/custom-dropdown.d.ts.map +1 -0
- package/dist/examples/internal/custom-dropdown.js +270 -0
- package/dist/examples/internal/custom-dropdown.js.map +1 -0
- package/dist/examples/internal/custom-renderable-form.d.ts +43 -0
- package/dist/examples/internal/custom-renderable-form.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-form.js +284 -0
- package/dist/examples/internal/custom-renderable-form.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list-default-search.d.ts +2 -0
- package/dist/examples/internal/custom-renderable-list-default-search.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list-default-search.js +16 -0
- package/dist/examples/internal/custom-renderable-list-default-search.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts +2 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.js +24 -0
- package/dist/examples/internal/custom-renderable-list-v2-default-search.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2.d.ts +189 -0
- package/dist/examples/internal/custom-renderable-list-v2.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list-v2.js +708 -0
- package/dist/examples/internal/custom-renderable-list-v2.js.map +1 -0
- package/dist/examples/internal/custom-renderable-list.d.ts +72 -0
- package/dist/examples/internal/custom-renderable-list.d.ts.map +1 -0
- package/dist/examples/internal/custom-renderable-list.js +544 -0
- package/dist/examples/internal/custom-renderable-list.js.map +1 -0
- package/dist/examples/internal/rhf-custom-ref.js +5 -4
- package/dist/examples/internal/rhf-custom-ref.js.map +1 -1
- package/dist/examples/internal/scrollbox-with-descendants.js +4 -2
- package/dist/examples/internal/scrollbox-with-descendants.js.map +1 -1
- package/dist/examples/list-controlled-search.d.ts +2 -0
- package/dist/examples/list-controlled-search.d.ts.map +1 -0
- package/dist/examples/list-controlled-search.js +12 -0
- package/dist/examples/list-controlled-search.js.map +1 -0
- package/dist/examples/list-detail-metadata.js +1 -1
- package/dist/examples/list-detail-metadata.js.map +1 -1
- package/dist/examples/simple-image-mask.d.ts +8 -0
- package/dist/examples/simple-image-mask.d.ts.map +1 -0
- package/dist/examples/simple-image-mask.js +12 -0
- package/dist/examples/simple-image-mask.js.map +1 -0
- package/dist/examples/toast-variations.js +1 -1
- package/dist/examples/toast-variations.js.map +1 -1
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +3 -2
- package/dist/extensions/dev.js.map +1 -1
- package/dist/extensions/react-refresh-init.d.ts.map +1 -1
- package/dist/extensions/react-refresh-init.js +4 -3
- package/dist/extensions/react-refresh-init.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/date-picker-widget.d.ts.map +1 -1
- package/dist/internal/date-picker-widget.js +2 -1
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/dialog.d.ts +6 -0
- package/dist/internal/dialog.d.ts.map +1 -1
- package/dist/internal/dialog.js +59 -18
- package/dist/internal/dialog.js.map +1 -1
- package/dist/internal/navigation.d.ts.map +1 -1
- package/dist/internal/navigation.js +8 -1
- package/dist/internal/navigation.js.map +1 -1
- package/dist/internal/offscreen.d.ts +3 -0
- package/dist/internal/offscreen.d.ts.map +1 -1
- package/dist/internal/offscreen.js +5 -0
- package/dist/internal/offscreen.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +20 -3
- package/dist/internal/providers.js.map +1 -1
- package/dist/internal/scrollbox.d.ts.map +1 -1
- package/dist/internal/scrollbox.js +3 -2
- package/dist/internal/scrollbox.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/preload.js +5 -17
- package/dist/preload.js.map +1 -1
- package/dist/state.d.ts +4 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +4 -0
- package/dist/state.js.map +1 -1
- package/dist/test-border-overlay.d.ts +2 -0
- package/dist/test-border-overlay.d.ts.map +1 -0
- package/dist/test-border-overlay.js +7 -0
- package/dist/test-border-overlay.js.map +1 -0
- package/dist/test-layout-2.d.ts +2 -0
- package/dist/test-layout-2.d.ts.map +1 -0
- package/dist/test-layout-2.js +5 -0
- package/dist/test-layout-2.js.map +1 -0
- package/dist/test-layout.d.ts +2 -0
- package/dist/test-layout.d.ts.map +1 -0
- package/dist/test-layout.js +7 -0
- package/dist/test-layout.js.map +1 -0
- package/dist/theme.d.ts +1 -2
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +5 -9
- package/dist/theme.js.map +1 -1
- package/dist/utils/run-command.d.ts +1 -1
- package/dist/utils/run-command.d.ts.map +1 -1
- package/dist/utils/run-command.js +27 -7
- package/dist/utils/run-command.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +44 -23
- package/dist/utils.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +24 -4
- package/dist/watcher.js.map +1 -1
- package/package.json +14 -12
- package/src/action-utils.tsx +10 -0
- package/src/apis/cache.test.ts +35 -3
- package/src/apis/cache.tsx +184 -59
- package/src/apis/clipboard.tsx +5 -0
- package/src/apis/oauth.tsx +33 -4
- package/src/build.tsx +35 -58
- package/src/cli.tsx +156 -134
- package/src/compile.tsx +6 -3
- package/src/compile.vitest.tsx +33 -15
- package/src/components/actions.tsx +230 -99
- package/src/components/alert.tsx +11 -10
- package/src/components/animation-tick.tsx +1 -1
- package/src/components/detail.tsx +56 -151
- package/src/components/dropdown.tsx +70 -36
- package/src/components/footer.tsx +58 -33
- package/src/components/form/checkbox.tsx +30 -32
- package/src/components/form/date-picker.tsx +27 -47
- package/src/components/form/description.tsx +19 -18
- package/src/components/form/dropdown.tsx +95 -103
- package/src/components/form/file-autocomplete.tsx +9 -8
- package/src/components/form/file-picker.tsx +46 -46
- package/src/components/form/form-end.tsx +6 -4
- package/src/components/form/index.tsx +38 -48
- package/src/components/form/password-field.tsx +25 -27
- package/src/components/form/separator.tsx +3 -2
- package/src/components/form/tagpicker.tsx +2 -1
- package/src/components/form/text-area.tsx +25 -30
- package/src/components/form/text-field.tsx +25 -27
- package/src/components/form/use-form-navigation.tsx +4 -5
- package/src/components/form/with-left-border.tsx +48 -10
- package/src/components/icon.tsx +69 -0
- package/src/components/image.tsx +60 -7
- package/src/components/list.tsx +270 -202
- package/src/components/loading-bar.tsx +4 -3
- package/src/components/metadata.tsx +217 -0
- package/src/components/theme-picker.tsx +3 -2
- package/src/examples/actions-context.tsx +63 -0
- package/src/examples/actions-context.vitest.tsx +110 -0
- package/src/examples/actions-dialog-layout.vitest.tsx +2 -1
- package/src/examples/file-autocomplete.vitest.tsx +15 -15
- package/src/examples/form-basic.tsx +12 -0
- package/src/examples/form-basic.vitest.tsx +74 -74
- package/src/examples/form-dropdown.tsx +8 -0
- package/src/examples/form-dropdown.vitest.tsx +364 -421
- package/src/examples/form-tagpicker.vitest.tsx +56 -54
- package/src/examples/github.vitest.tsx +252 -0
- package/src/examples/internal/rhf-custom-ref.tsx +16 -15
- package/src/examples/internal/scrollbox-with-descendants.tsx +4 -2
- package/src/examples/internal/simple-dialog.tsx +1 -1
- package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -9
- package/src/examples/list-controlled-search.tsx +28 -0
- package/src/examples/list-controlled-search.vitest.tsx +49 -0
- package/src/examples/list-detail-metadata.tsx +8 -5
- package/src/examples/list-detail-metadata.vitest.tsx +22 -22
- package/src/examples/list-dropdown-default.vitest.tsx +12 -12
- package/src/examples/list-scrollbox.vitest.tsx +52 -38
- package/src/examples/list-with-detail.vitest.tsx +45 -41
- package/src/examples/list-with-dropdown.vitest.tsx +5 -5
- package/src/examples/list-with-sections.vitest.tsx +65 -12
- package/src/examples/list-with-toast.vitest.tsx +4 -4
- package/src/examples/simple-file-picker.vitest.tsx +12 -12
- package/src/examples/simple-grid.vitest.tsx +53 -53
- package/src/examples/simple-image-mask.tsx +58 -0
- package/src/examples/simple-navigation.vitest.tsx +19 -19
- package/src/examples/store.vitest.tsx +1 -1
- package/src/examples/swift-extension.vitest.tsx +4 -2
- package/src/examples/synonyms.vitest.tsx +31 -9
- package/src/examples/toast-action.vitest.tsx +8 -8
- package/src/examples/toast-variations.tsx +1 -1
- package/src/examples/toast-variations.vitest.tsx +69 -134
- package/src/extensions/dev.tsx +3 -2
- package/src/extensions/dev.vitest.tsx +65 -28
- package/src/extensions/react-refresh-init.tsx +4 -3
- package/src/index.tsx +3 -1
- package/src/internal/date-picker-widget.tsx +2 -1
- package/src/internal/dialog.tsx +100 -28
- package/src/internal/navigation.tsx +8 -1
- package/src/internal/offscreen.tsx +10 -0
- package/src/internal/providers.tsx +34 -8
- package/src/internal/scrollbox.tsx +4 -2
- package/src/logger.tsx +4 -0
- package/src/preload.tsx +5 -17
- package/src/state.tsx +12 -0
- package/src/theme.tsx +6 -9
- package/src/utils/run-command.tsx +32 -8
- package/src/utils.tsx +58 -23
- package/src/watcher.tsx +26 -6
package/src/components/list.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
ReactElement,
|
|
10
10
|
ReactNode,
|
|
11
11
|
createContext,
|
|
12
|
+
useCallback,
|
|
12
13
|
useContext,
|
|
13
14
|
useEffect,
|
|
14
15
|
useLayoutEffect,
|
|
@@ -17,6 +18,7 @@ import React, {
|
|
|
17
18
|
useState
|
|
18
19
|
} from 'react'
|
|
19
20
|
import { LoadingBar } from 'termcast/src/components/loading-bar'
|
|
21
|
+
import { LoadingText } from 'termcast/src/components/loading-text'
|
|
20
22
|
import { Footer } from 'termcast/src/components/footer'
|
|
21
23
|
import { createDescendants } from 'termcast/src/descendants'
|
|
22
24
|
import { useStore } from 'termcast/src/state'
|
|
@@ -27,9 +29,9 @@ import { Offscreen } from 'termcast/src/internal/offscreen'
|
|
|
27
29
|
import { ScrollBox } from 'termcast/src/internal/scrollbox'
|
|
28
30
|
|
|
29
31
|
import { Color, resolveColor } from 'termcast/src/colors'
|
|
30
|
-
import { getIconEmoji } from 'termcast/src/components/icon'
|
|
32
|
+
import { getIconEmoji, getIconValue } from 'termcast/src/components/icon'
|
|
31
33
|
import { ActionPanel } from 'termcast/src/components/actions'
|
|
32
|
-
import {
|
|
34
|
+
import { useTheme, markdownSyntaxStyle } from 'termcast/src/theme'
|
|
33
35
|
import { CommonProps } from 'termcast/src/utils'
|
|
34
36
|
|
|
35
37
|
export { Color }
|
|
@@ -71,32 +73,42 @@ interface ActionsInterface {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
function ListFooter(): any {
|
|
76
|
+
const theme = useTheme()
|
|
74
77
|
const firstActionTitle = useStore((s) => s.firstActionTitle)
|
|
75
78
|
const hasToast = useStore((s) => s.toast !== null)
|
|
76
79
|
const listContext = useContext(ListContext)
|
|
77
80
|
const isShowingDetail = listContext?.isShowingDetail ?? false
|
|
81
|
+
const hasDropdown = listContext?.hasDropdown ?? false
|
|
78
82
|
|
|
79
83
|
const content = hasToast ? null : (
|
|
80
84
|
<box style={{ flexDirection: 'row', gap: 3 }}>
|
|
81
85
|
{firstActionTitle && (
|
|
82
86
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
83
|
-
<text flexShrink={0} fg={
|
|
87
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
84
88
|
↵
|
|
85
89
|
</text>
|
|
86
|
-
<text flexShrink={0} fg={
|
|
90
|
+
<text flexShrink={0} fg={theme.textMuted}>{firstActionTitle.toLowerCase()}</text>
|
|
87
91
|
</box>
|
|
88
92
|
)}
|
|
89
93
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
90
|
-
<text flexShrink={0} fg={
|
|
94
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
91
95
|
↑↓
|
|
92
96
|
</text>
|
|
93
|
-
<text flexShrink={0} fg={
|
|
97
|
+
<text flexShrink={0} fg={theme.textMuted}>navigate</text>
|
|
94
98
|
</box>
|
|
99
|
+
{hasDropdown && (
|
|
100
|
+
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
101
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
102
|
+
^p
|
|
103
|
+
</text>
|
|
104
|
+
<text flexShrink={0} fg={theme.textMuted}>dropdown</text>
|
|
105
|
+
</box>
|
|
106
|
+
)}
|
|
95
107
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
96
|
-
<text flexShrink={0} fg={
|
|
108
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
97
109
|
^k
|
|
98
110
|
</text>
|
|
99
|
-
<text flexShrink={0} fg={
|
|
111
|
+
<text flexShrink={0} fg={theme.textMuted}>actions</text>
|
|
100
112
|
</box>
|
|
101
113
|
</box>
|
|
102
114
|
)
|
|
@@ -104,6 +116,38 @@ function ListFooter(): any {
|
|
|
104
116
|
return <Footer hidePoweredBy={isShowingDetail}>{content}</Footer>
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Component that subscribes to descendants changes and renders current item's
|
|
121
|
+
* actions offscreen. This ensures actions are captured even when items register
|
|
122
|
+
* after the initial render (preserving context via closures).
|
|
123
|
+
*/
|
|
124
|
+
function CurrentItemActionsOffscreen(props: {
|
|
125
|
+
selectedIndex: number
|
|
126
|
+
fallbackActions?: ReactNode
|
|
127
|
+
}): any {
|
|
128
|
+
// Subscribe to descendants changes - this hook triggers re-render when items register
|
|
129
|
+
const descendantsMap = useListDescendantsRerender()
|
|
130
|
+
|
|
131
|
+
// Get current item's actions
|
|
132
|
+
const items = Object.values(descendantsMap)
|
|
133
|
+
.filter((item) => item.index !== -1)
|
|
134
|
+
.sort((a, b) => a.index - b.index)
|
|
135
|
+
|
|
136
|
+
const currentItem = items.find((item) => item.index === props.selectedIndex)
|
|
137
|
+
const actions = currentItem?.props?.actions ?? props.fallbackActions ?? null
|
|
138
|
+
|
|
139
|
+
// Clear first action title when there are no actions
|
|
140
|
+
useLayoutEffect(() => {
|
|
141
|
+
if (!actions) {
|
|
142
|
+
useStore.setState({ firstActionTitle: '' })
|
|
143
|
+
}
|
|
144
|
+
}, [actions])
|
|
145
|
+
|
|
146
|
+
if (!actions) return null
|
|
147
|
+
|
|
148
|
+
return <Offscreen>{actions}</Offscreen>
|
|
149
|
+
}
|
|
150
|
+
|
|
107
151
|
interface NavigationChildInterface {
|
|
108
152
|
navigationTitle?: string
|
|
109
153
|
isLoading?: boolean
|
|
@@ -126,7 +170,11 @@ interface PaginationInterface {
|
|
|
126
170
|
}
|
|
127
171
|
|
|
128
172
|
export namespace Image {
|
|
129
|
-
export type ImageLike = string | { source: string; tintColor?: string }
|
|
173
|
+
export type ImageLike = string | { source: string; tintColor?: string; mask?: ImageMask }
|
|
174
|
+
export enum ImageMask {
|
|
175
|
+
Circle = 'circle',
|
|
176
|
+
RoundedRectangle = 'roundedRectangle',
|
|
177
|
+
}
|
|
130
178
|
}
|
|
131
179
|
|
|
132
180
|
export type ItemAccessory =
|
|
@@ -298,6 +346,9 @@ interface ListContextValue {
|
|
|
298
346
|
isFiltering: boolean
|
|
299
347
|
setCurrentDetail?: (detail: ReactNode) => void
|
|
300
348
|
isShowingDetail?: boolean
|
|
349
|
+
customEmptyViewRef: React.MutableRefObject<boolean>
|
|
350
|
+
isLoading?: boolean
|
|
351
|
+
hasDropdown?: boolean
|
|
301
352
|
}
|
|
302
353
|
|
|
303
354
|
const ListContext = createContext<ListContextValue | undefined>(undefined)
|
|
@@ -383,6 +434,7 @@ interface ListDropdownDialogProps extends DropdownProps {
|
|
|
383
434
|
}
|
|
384
435
|
|
|
385
436
|
function ListDropdownDialog(props: ListDropdownDialogProps): any {
|
|
437
|
+
const theme = useTheme()
|
|
386
438
|
const [searchText, setSearchTextRaw] = useState('')
|
|
387
439
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
388
440
|
const inputRef = useRef<TextareaRenderable>(null)
|
|
@@ -469,11 +521,11 @@ function ListDropdownDialog(props: ListDropdownDialogProps): any {
|
|
|
469
521
|
justifyContent: 'space-between',
|
|
470
522
|
}}
|
|
471
523
|
>
|
|
472
|
-
<text flexShrink={0} fg={
|
|
473
|
-
<text flexShrink={0} fg={
|
|
524
|
+
<text flexShrink={0} fg={theme.textMuted}>{props.tooltip}</text>
|
|
525
|
+
<text flexShrink={0} fg={theme.textMuted}>esc</text>
|
|
474
526
|
</box>
|
|
475
527
|
<box style={{ paddingTop: 1, paddingBottom: 1, flexDirection: 'row' }}>
|
|
476
|
-
<text flexShrink={0} fg={
|
|
528
|
+
<text flexShrink={0} fg={theme.textMuted}>> </text>
|
|
477
529
|
<textarea
|
|
478
530
|
ref={inputRef}
|
|
479
531
|
height={1}
|
|
@@ -490,9 +542,9 @@ function ListDropdownDialog(props: ListDropdownDialogProps): any {
|
|
|
490
542
|
placeholder={props.placeholder || 'Search...'}
|
|
491
543
|
focused={inFocus}
|
|
492
544
|
initialValue={searchText}
|
|
493
|
-
focusedBackgroundColor={
|
|
494
|
-
cursorColor={
|
|
495
|
-
focusedTextColor={
|
|
545
|
+
focusedBackgroundColor={theme.backgroundPanel}
|
|
546
|
+
cursorColor={theme.primary}
|
|
547
|
+
focusedTextColor={theme.textMuted}
|
|
496
548
|
/>
|
|
497
549
|
</box>
|
|
498
550
|
</box>
|
|
@@ -517,7 +569,7 @@ function ListDropdownDialog(props: ListDropdownDialogProps): any {
|
|
|
517
569
|
</box>
|
|
518
570
|
{props.isLoading && (
|
|
519
571
|
<box style={{ paddingLeft: 1 }}>
|
|
520
|
-
<text flexShrink={0} fg={
|
|
572
|
+
<text flexShrink={0} fg={theme.textMuted}>Loading...</text>
|
|
521
573
|
</box>
|
|
522
574
|
)}
|
|
523
575
|
</box>
|
|
@@ -529,21 +581,22 @@ function ListDropdownDialog(props: ListDropdownDialogProps): any {
|
|
|
529
581
|
}
|
|
530
582
|
|
|
531
583
|
function DropdownFooter(): any {
|
|
584
|
+
const theme = useTheme()
|
|
532
585
|
const hasToast = useStore((s) => s.toast !== null)
|
|
533
586
|
|
|
534
587
|
const content = hasToast ? null : (
|
|
535
588
|
<box style={{ flexDirection: 'row', gap: 3 }}>
|
|
536
589
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
537
|
-
<text flexShrink={0} fg={
|
|
590
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
538
591
|
↵
|
|
539
592
|
</text>
|
|
540
|
-
<text flexShrink={0} fg={
|
|
593
|
+
<text flexShrink={0} fg={theme.textMuted}>select</text>
|
|
541
594
|
</box>
|
|
542
595
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
543
|
-
<text flexShrink={0} fg={
|
|
596
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
544
597
|
↑↓
|
|
545
598
|
</text>
|
|
546
|
-
<text flexShrink={0} fg={
|
|
599
|
+
<text flexShrink={0} fg={theme.textMuted}>navigate</text>
|
|
547
600
|
</box>
|
|
548
601
|
</box>
|
|
549
602
|
)
|
|
@@ -568,6 +621,7 @@ function ListItemRow(props: {
|
|
|
568
621
|
index?: number
|
|
569
622
|
ref?: React.Ref<BoxRenderable>
|
|
570
623
|
}) {
|
|
624
|
+
const theme = useTheme()
|
|
571
625
|
const { title, subtitle, icon, iconColor, accessories, active, ref } = props
|
|
572
626
|
const [isHovered, setIsHovered] = useState(false)
|
|
573
627
|
|
|
@@ -586,7 +640,7 @@ function ListItemRow(props: {
|
|
|
586
640
|
<text
|
|
587
641
|
key={`text-${textValue}`}
|
|
588
642
|
flexShrink={0}
|
|
589
|
-
fg={active ?
|
|
643
|
+
fg={active ? theme.background : resolveColor(textColor) || theme.info}
|
|
590
644
|
wrapMode="none"
|
|
591
645
|
>
|
|
592
646
|
{textValue}
|
|
@@ -606,7 +660,7 @@ function ListItemRow(props: {
|
|
|
606
660
|
<text
|
|
607
661
|
key={`tag-${tagValue}`}
|
|
608
662
|
flexShrink={0}
|
|
609
|
-
fg={active ?
|
|
663
|
+
fg={active ? theme.background : resolveColor(tagColor) || theme.warning}
|
|
610
664
|
wrapMode="none"
|
|
611
665
|
>
|
|
612
666
|
[{tagValue}]
|
|
@@ -629,7 +683,7 @@ function ListItemRow(props: {
|
|
|
629
683
|
<text
|
|
630
684
|
key={`date-${dateValue.getTime()}`}
|
|
631
685
|
flexShrink={0}
|
|
632
|
-
fg={active ?
|
|
686
|
+
fg={active ? theme.background : resolveColor(dateColor) || theme.success}
|
|
633
687
|
wrapMode="none"
|
|
634
688
|
>
|
|
635
689
|
{formatted}
|
|
@@ -647,9 +701,9 @@ function ListItemRow(props: {
|
|
|
647
701
|
flexDirection: 'row',
|
|
648
702
|
justifyContent: 'space-between',
|
|
649
703
|
backgroundColor: active
|
|
650
|
-
?
|
|
704
|
+
? theme.primary
|
|
651
705
|
: isHovered
|
|
652
|
-
?
|
|
706
|
+
? theme.backgroundPanel
|
|
653
707
|
: undefined,
|
|
654
708
|
paddingLeft: 0,
|
|
655
709
|
paddingRight: 1,
|
|
@@ -666,11 +720,11 @@ function ListItemRow(props: {
|
|
|
666
720
|
>
|
|
667
721
|
<box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden', gap: 1 }}>
|
|
668
722
|
<box style={{ flexDirection: 'row', flexShrink: 0 }}>
|
|
669
|
-
<text flexShrink={0} fg={active ?
|
|
670
|
-
{icon && <text flexShrink={0} fg={active ?
|
|
723
|
+
<text flexShrink={0} fg={active ? theme.background : theme.text} attributes={active ? TextAttributes.BOLD : undefined} selectable={false} wrapMode="none">{active ? '›' : ' '}</text>
|
|
724
|
+
{icon && <text flexShrink={0} fg={active ? theme.background : iconColor || theme.text} selectable={false} wrapMode="none">{getIconEmoji(icon)} </text>}
|
|
671
725
|
<text
|
|
672
726
|
flexShrink={0}
|
|
673
|
-
fg={active ?
|
|
727
|
+
fg={active ? theme.background : theme.text}
|
|
674
728
|
attributes={active ? TextAttributes.BOLD : undefined}
|
|
675
729
|
selectable={false}
|
|
676
730
|
wrapMode="none"
|
|
@@ -681,7 +735,7 @@ function ListItemRow(props: {
|
|
|
681
735
|
{subtitle && (
|
|
682
736
|
<text
|
|
683
737
|
flexShrink={0}
|
|
684
|
-
fg={active ?
|
|
738
|
+
fg={active ? theme.background : theme.textMuted}
|
|
685
739
|
selectable={false}
|
|
686
740
|
wrapMode="none"
|
|
687
741
|
>
|
|
@@ -719,12 +773,30 @@ export const List: ListType = (props) => {
|
|
|
719
773
|
...otherProps
|
|
720
774
|
} = props
|
|
721
775
|
|
|
722
|
-
const
|
|
776
|
+
const theme = useTheme()
|
|
777
|
+
const [internalSearchText, setInternalSearchText] = useState('')
|
|
723
778
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
724
779
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
|
725
780
|
const [currentDetail, setCurrentDetail] = useState<ReactNode>(null)
|
|
726
|
-
|
|
781
|
+
|
|
727
782
|
const inputRef = useRef<TextareaRenderable>(null)
|
|
783
|
+
const customEmptyViewRef = useRef(false)
|
|
784
|
+
|
|
785
|
+
// Ref callback that registers the textarea in global state for ESC handling
|
|
786
|
+
const setInputRef = useCallback((node: TextareaRenderable | null) => {
|
|
787
|
+
if (!node) return
|
|
788
|
+
|
|
789
|
+
inputRef.current = node
|
|
790
|
+
useStore.setState({ activeSearchInputRef: node })
|
|
791
|
+
|
|
792
|
+
// React 19: return cleanup function for unmount
|
|
793
|
+
return () => {
|
|
794
|
+
if (useStore.getState().activeSearchInputRef === node) {
|
|
795
|
+
useStore.setState({ activeSearchInputRef: null })
|
|
796
|
+
}
|
|
797
|
+
inputRef.current = null
|
|
798
|
+
}
|
|
799
|
+
}, [])
|
|
728
800
|
const scrollBoxRef = useRef<ScrollBoxRenderable>(null)
|
|
729
801
|
const descendantsContext = useListDescendants()
|
|
730
802
|
const navigationPending = useNavigationPending()
|
|
@@ -773,12 +845,17 @@ export const List: ListType = (props) => {
|
|
|
773
845
|
setIsDropdownOpen(true)
|
|
774
846
|
}
|
|
775
847
|
|
|
776
|
-
//
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
848
|
+
// Sync selection to the first visible item whenever searchText changes.
|
|
849
|
+
// Runs after children's useLayoutEffects (descendants registered) but before paint,
|
|
850
|
+
// so there is no intermediate frame with stale selection.
|
|
851
|
+
// Works for both controlled and uncontrolled searchText.
|
|
852
|
+
const prevSearchTextRef = useRef(searchText)
|
|
853
|
+
useLayoutEffect(() => {
|
|
854
|
+
if (prevSearchTextRef.current === searchText) return
|
|
855
|
+
prevSearchTextRef.current = searchText
|
|
856
|
+
|
|
857
|
+
if (!isFilteringEnabled) return
|
|
858
|
+
|
|
782
859
|
const items = Object.values(descendantsContext.map.current)
|
|
783
860
|
.filter((item) => item.index !== -1 && item.props?.visible !== false)
|
|
784
861
|
.sort((a, b) => a.index - b.index)
|
|
@@ -786,7 +863,7 @@ export const List: ListType = (props) => {
|
|
|
786
863
|
if (items.length > 0 && items[0]) {
|
|
787
864
|
setSelectedIndex(items[0].index)
|
|
788
865
|
}
|
|
789
|
-
}
|
|
866
|
+
})
|
|
790
867
|
|
|
791
868
|
const listContextValue = useMemo<ListContextValue>(
|
|
792
869
|
() => ({
|
|
@@ -799,19 +876,22 @@ export const List: ListType = (props) => {
|
|
|
799
876
|
isFiltering: isFilteringEnabled,
|
|
800
877
|
setCurrentDetail,
|
|
801
878
|
isShowingDetail,
|
|
879
|
+
customEmptyViewRef,
|
|
880
|
+
isLoading,
|
|
881
|
+
hasDropdown: !!searchBarAccessory,
|
|
802
882
|
}),
|
|
803
|
-
[isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail],
|
|
883
|
+
[isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail, isLoading, searchBarAccessory],
|
|
804
884
|
)
|
|
805
885
|
|
|
806
|
-
// Clear detail when detail view is hidden
|
|
807
|
-
|
|
886
|
+
// Clear detail when detail view is hidden (before paint to avoid flash)
|
|
887
|
+
useLayoutEffect(() => {
|
|
808
888
|
if (!isShowingDetail) {
|
|
809
889
|
setCurrentDetail(null)
|
|
810
890
|
}
|
|
811
891
|
}, [isShowingDetail])
|
|
812
892
|
|
|
813
|
-
// Handle selectedItemId prop changes
|
|
814
|
-
|
|
893
|
+
// Handle selectedItemId prop changes (before paint to avoid flash)
|
|
894
|
+
useLayoutEffect(() => {
|
|
815
895
|
// Only update selection if selectedItemId is explicitly provided
|
|
816
896
|
if (selectedItemId !== undefined) {
|
|
817
897
|
const items = Object.values(descendantsContext.map.current)
|
|
@@ -824,7 +904,7 @@ export const List: ListType = (props) => {
|
|
|
824
904
|
}
|
|
825
905
|
}, [selectedItemId])
|
|
826
906
|
|
|
827
|
-
// Call onSelectionChange when selection changes
|
|
907
|
+
// Call onSelectionChange when selection changes
|
|
828
908
|
useEffect(() => {
|
|
829
909
|
const items = Object.values(descendantsContext.map.current)
|
|
830
910
|
.filter((item) => item.index !== -1)
|
|
@@ -832,21 +912,12 @@ export const List: ListType = (props) => {
|
|
|
832
912
|
|
|
833
913
|
const currentItem = items.find((item) => item.index === selectedIndex)
|
|
834
914
|
|
|
835
|
-
// Track current item's actions for footer display
|
|
836
|
-
const actions = currentItem?.props?.actions ?? props.actions ?? null
|
|
837
|
-
setCurrentItemActions(actions)
|
|
838
|
-
|
|
839
|
-
// Clear first action title when there are no actions
|
|
840
|
-
if (!actions) {
|
|
841
|
-
useStore.setState({ firstActionTitle: '' })
|
|
842
|
-
}
|
|
843
|
-
|
|
844
915
|
// Call onSelectionChange callback if provided
|
|
845
916
|
if (onSelectionChange) {
|
|
846
917
|
const selectedId = currentItem?.props?.id ?? null
|
|
847
918
|
onSelectionChange(selectedId)
|
|
848
919
|
}
|
|
849
|
-
}, [selectedIndex
|
|
920
|
+
}, [selectedIndex])
|
|
850
921
|
|
|
851
922
|
const scrollToItem = (item: { props?: ListItemDescendant }) => {
|
|
852
923
|
const scrollBox = scrollBoxRef.current
|
|
@@ -891,8 +962,10 @@ export const List: ListType = (props) => {
|
|
|
891
962
|
|
|
892
963
|
const nextItem = items[nextVisibleIndex]
|
|
893
964
|
if (nextItem) {
|
|
965
|
+
flushSync(() => {
|
|
966
|
+
setSelectedIndex(nextItem.index)
|
|
967
|
+
})
|
|
894
968
|
scrollToItem(nextItem)
|
|
895
|
-
setSelectedIndex(nextItem.index)
|
|
896
969
|
}
|
|
897
970
|
}
|
|
898
971
|
|
|
@@ -914,32 +987,23 @@ export const List: ListType = (props) => {
|
|
|
914
987
|
.sort((a, b) => a.index - b.index)
|
|
915
988
|
const currentItem = items.find((item) => item.index === selectedIndex)
|
|
916
989
|
|
|
917
|
-
// Handle Ctrl+K to show actions
|
|
990
|
+
// Handle Ctrl+K to show actions dialog via portal
|
|
918
991
|
if (evt.name === 'k' && evt.ctrl) {
|
|
919
|
-
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
// Otherwise show List's own actions
|
|
924
|
-
else if (props.actions) {
|
|
925
|
-
dialog.pushActions(props.actions)
|
|
926
|
-
}
|
|
927
|
-
// Otherwise show empty ActionPanel (still has Settings section with Configure Extension, etc.)
|
|
928
|
-
else {
|
|
929
|
-
dialog.pushActions(<ActionPanel />)
|
|
992
|
+
const hasActions = currentItem?.props?.actions || props.actions
|
|
993
|
+
if (hasActions) {
|
|
994
|
+
useStore.setState({ showActionsDialog: true })
|
|
930
995
|
}
|
|
931
996
|
return
|
|
932
997
|
}
|
|
933
998
|
|
|
934
999
|
if (evt.name === 'up') move(-1)
|
|
935
1000
|
if (evt.name === 'down') move(1)
|
|
936
|
-
// Handle Enter to execute first action
|
|
1001
|
+
// Handle Enter to auto-execute first action via ActionPanel
|
|
937
1002
|
if (evt.name === 'return') {
|
|
938
1003
|
if (!currentItem?.props) return
|
|
939
1004
|
|
|
940
1005
|
if (currentItem.props.actions) {
|
|
941
1006
|
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
942
|
-
dialog.pushActions(currentItem.props.actions)
|
|
943
1007
|
}
|
|
944
1008
|
}
|
|
945
1009
|
})
|
|
@@ -1002,9 +1066,9 @@ export const List: ListType = (props) => {
|
|
|
1002
1066
|
flexShrink: 1,
|
|
1003
1067
|
}}
|
|
1004
1068
|
>
|
|
1005
|
-
<text flexShrink={0} fg={
|
|
1069
|
+
<text flexShrink={0} fg={theme.textMuted}>> </text>
|
|
1006
1070
|
<textarea
|
|
1007
|
-
ref={
|
|
1071
|
+
ref={setInputRef}
|
|
1008
1072
|
height={1}
|
|
1009
1073
|
flexGrow={1}
|
|
1010
1074
|
wrapMode='none'
|
|
@@ -1019,9 +1083,9 @@ export const List: ListType = (props) => {
|
|
|
1019
1083
|
const value = inputRef.current?.plainText || ''
|
|
1020
1084
|
handleSearchChange(value)
|
|
1021
1085
|
}}
|
|
1022
|
-
focusedBackgroundColor={
|
|
1023
|
-
cursorColor={
|
|
1024
|
-
focusedTextColor={
|
|
1086
|
+
focusedBackgroundColor={theme.backgroundPanel}
|
|
1087
|
+
cursorColor={theme.primary}
|
|
1088
|
+
focusedTextColor={theme.text}
|
|
1025
1089
|
/>
|
|
1026
1090
|
</box>
|
|
1027
1091
|
{searchBarAccessory}
|
|
@@ -1038,6 +1102,7 @@ export const List: ListType = (props) => {
|
|
|
1038
1102
|
focused={false}
|
|
1039
1103
|
flexGrow={1}
|
|
1040
1104
|
flexShrink={1}
|
|
1105
|
+
minHeight={6}
|
|
1041
1106
|
style={{
|
|
1042
1107
|
rootOptions: {
|
|
1043
1108
|
backgroundColor: undefined,
|
|
@@ -1056,10 +1121,11 @@ export const List: ListType = (props) => {
|
|
|
1056
1121
|
{/* Footer with keyboard shortcuts or toast */}
|
|
1057
1122
|
<ListFooter />
|
|
1058
1123
|
|
|
1059
|
-
{/* Render current item's actions offscreen to
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1124
|
+
{/* Render current item's actions offscreen to capture them with context preserved */}
|
|
1125
|
+
<CurrentItemActionsOffscreen
|
|
1126
|
+
selectedIndex={selectedIndex}
|
|
1127
|
+
fallbackActions={props.actions}
|
|
1128
|
+
/>
|
|
1063
1129
|
</box>
|
|
1064
1130
|
|
|
1065
1131
|
{/* Detail panel on the right */}
|
|
@@ -1073,7 +1139,7 @@ export const List: ListType = (props) => {
|
|
|
1073
1139
|
}}
|
|
1074
1140
|
border={['left']}
|
|
1075
1141
|
borderStyle='single'
|
|
1076
|
-
borderColor={
|
|
1142
|
+
borderColor={theme.border}
|
|
1077
1143
|
>
|
|
1078
1144
|
{currentDetail}
|
|
1079
1145
|
</box>
|
|
@@ -1086,11 +1152,13 @@ export const List: ListType = (props) => {
|
|
|
1086
1152
|
}
|
|
1087
1153
|
|
|
1088
1154
|
|
|
1089
|
-
|
|
1155
|
+
// Wrapper component that only renders children when no visible items exist
|
|
1156
|
+
function ShowOnNoItems(props: { children: ReactNode; isCustomEmptyView?: boolean }): any {
|
|
1090
1157
|
// Subscribe to re-render when items are added/removed
|
|
1091
1158
|
void useListDescendantsRerender()
|
|
1092
1159
|
// Get live map ref for reading in useLayoutEffect
|
|
1093
1160
|
const map = useListDescendantsMap()
|
|
1161
|
+
const listContext = useContext(ListContext)
|
|
1094
1162
|
const [hasVisibleItems, setHasVisibleItems] = useState(true)
|
|
1095
1163
|
|
|
1096
1164
|
// We must check visibility in useLayoutEffect because:
|
|
@@ -1102,27 +1170,36 @@ function DefaultEmptyView(): any {
|
|
|
1102
1170
|
useLayoutEffect(() => {
|
|
1103
1171
|
const items = Object.values(map.current)
|
|
1104
1172
|
.filter((item) => item.index !== -1 && item.props?.visible !== false)
|
|
1105
|
-
|
|
1173
|
+
// For default empty view, also check if custom empty view exists
|
|
1174
|
+
const hasCustomEmptyView = !props.isCustomEmptyView && (listContext?.customEmptyViewRef.current ?? false)
|
|
1175
|
+
setHasVisibleItems(items.length > 0 || hasCustomEmptyView)
|
|
1106
1176
|
})
|
|
1107
1177
|
|
|
1108
1178
|
if (hasVisibleItems) return null
|
|
1109
1179
|
|
|
1180
|
+
return props.children
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function DefaultEmptyView(): any {
|
|
1184
|
+
const theme = useTheme()
|
|
1110
1185
|
return (
|
|
1111
|
-
<
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1186
|
+
<ShowOnNoItems>
|
|
1187
|
+
<box
|
|
1188
|
+
style={{
|
|
1189
|
+
flexDirection: 'column',
|
|
1190
|
+
alignItems: 'center',
|
|
1191
|
+
justifyContent: 'center',
|
|
1192
|
+
paddingTop: 2,
|
|
1193
|
+
paddingBottom: 2,
|
|
1194
|
+
paddingLeft: 2,
|
|
1195
|
+
paddingRight: 2,
|
|
1196
|
+
}}
|
|
1197
|
+
>
|
|
1198
|
+
<text flexShrink={0} fg={theme.textMuted}>
|
|
1199
|
+
No items found
|
|
1200
|
+
</text>
|
|
1201
|
+
</box>
|
|
1202
|
+
</ShowOnNoItems>
|
|
1126
1203
|
)
|
|
1127
1204
|
}
|
|
1128
1205
|
|
|
@@ -1197,8 +1274,8 @@ const ListItem: ListItemType = (props) => {
|
|
|
1197
1274
|
const selectedIndex = listContext?.selectedIndex ?? 0
|
|
1198
1275
|
const isActive = index === selectedIndex
|
|
1199
1276
|
|
|
1200
|
-
// Update detail when this item becomes active or detail prop changes
|
|
1201
|
-
|
|
1277
|
+
// Update detail when this item becomes active or detail prop changes (before paint)
|
|
1278
|
+
useLayoutEffect(() => {
|
|
1202
1279
|
if (isActive && listContext?.isShowingDetail && listContext?.setCurrentDetail) {
|
|
1203
1280
|
listContext.setCurrentDetail(props.detail || null)
|
|
1204
1281
|
}
|
|
@@ -1212,7 +1289,10 @@ const ListItem: ListItemType = (props) => {
|
|
|
1212
1289
|
if (listContext && index !== -1) {
|
|
1213
1290
|
// If clicking on already selected item, show actions (like pressing Enter)
|
|
1214
1291
|
if (isActive) {
|
|
1215
|
-
|
|
1292
|
+
// Show actions dialog via portal
|
|
1293
|
+
if (props.actions) {
|
|
1294
|
+
useStore.setState({ showActionsDialog: true })
|
|
1295
|
+
}
|
|
1216
1296
|
} else if (listContext.setSelectedIndex) {
|
|
1217
1297
|
// Otherwise just select the item
|
|
1218
1298
|
listContext.setSelectedIndex(index)
|
|
@@ -1262,13 +1342,14 @@ const ListItem: ListItemType = (props) => {
|
|
|
1262
1342
|
}
|
|
1263
1343
|
|
|
1264
1344
|
const ListItemDetail: ListItemDetailType = (props) => {
|
|
1345
|
+
const theme = useTheme()
|
|
1265
1346
|
const { isLoading, markdown, metadata } = props
|
|
1266
1347
|
|
|
1267
1348
|
return (
|
|
1268
1349
|
<box style={{ flexDirection: 'column', flexGrow: 1 }}>
|
|
1269
1350
|
{isLoading && (
|
|
1270
1351
|
<box style={{ paddingBottom: 1 }}>
|
|
1271
|
-
<text flexShrink={0} fg={
|
|
1352
|
+
<text flexShrink={0} fg={theme.textMuted}>Loading...</text>
|
|
1272
1353
|
</box>
|
|
1273
1354
|
)}
|
|
1274
1355
|
|
|
@@ -1287,16 +1368,16 @@ const ListItemDetail: ListItemDetailType = (props) => {
|
|
|
1287
1368
|
},
|
|
1288
1369
|
}}
|
|
1289
1370
|
>
|
|
1290
|
-
<box style={{ flexDirection: 'column' }}>
|
|
1371
|
+
<box gap={1} style={{ flexDirection: 'column' }}>
|
|
1291
1372
|
{markdown && (
|
|
1292
1373
|
<code content={markdown} filetype="markdown" syntaxStyle={markdownSyntaxStyle} drawUnstyledText={false} />
|
|
1293
1374
|
)}
|
|
1294
1375
|
{metadata && (
|
|
1295
1376
|
<box
|
|
1296
1377
|
style={{ paddingTop: 1 }}
|
|
1297
|
-
border={['top']}
|
|
1298
|
-
borderStyle='single'
|
|
1299
|
-
borderColor={
|
|
1378
|
+
// border={['top']}
|
|
1379
|
+
// borderStyle='single'
|
|
1380
|
+
// borderColor={theme.border}
|
|
1300
1381
|
>
|
|
1301
1382
|
{metadata}
|
|
1302
1383
|
</box>
|
|
@@ -1307,65 +1388,32 @@ const ListItemDetail: ListItemDetailType = (props) => {
|
|
|
1307
1388
|
)
|
|
1308
1389
|
}
|
|
1309
1390
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
<box style={{ flexDirection: 'column' }}>
|
|
1313
|
-
{props.children}
|
|
1314
|
-
</box>
|
|
1315
|
-
)
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
const ListItemDetailMetadataLabel = (props: { title: string; text?: string; icon?: Image.ImageLike }) => {
|
|
1319
|
-
return (
|
|
1320
|
-
<box style={{ flexDirection: 'column', paddingBottom: 0.5 }}>
|
|
1321
|
-
<text flexShrink={0} fg={Theme.textMuted}>{props.title}:</text>
|
|
1322
|
-
{props.text && <text flexShrink={0} fg={Theme.text}>{props.text}</text>}
|
|
1323
|
-
</box>
|
|
1324
|
-
)
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
const ListItemDetailMetadataSeparator = () => {
|
|
1328
|
-
return (
|
|
1329
|
-
<box style={{ paddingBottom: 0.5 }}>
|
|
1330
|
-
<text flexShrink={0} fg={Theme.border}>─────────────────</text>
|
|
1331
|
-
</box>
|
|
1332
|
-
)
|
|
1333
|
-
}
|
|
1391
|
+
import { Metadata, MetadataContext } from 'termcast/src/components/metadata'
|
|
1392
|
+
import type { MetadataConfig } from 'termcast/src/components/metadata'
|
|
1334
1393
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
)
|
|
1394
|
+
// List.Item.Detail.Metadata config: smaller padding for compact list detail panel
|
|
1395
|
+
const listDetailMetadataConfig: MetadataConfig = {
|
|
1396
|
+
maxValueLen: 20,
|
|
1397
|
+
titleMinWidth: 12,
|
|
1398
|
+
paddingBottom: 0.5,
|
|
1399
|
+
separatorWidth: 17,
|
|
1342
1400
|
}
|
|
1343
1401
|
|
|
1344
|
-
const
|
|
1402
|
+
const ListItemDetailMetadata = (props: MetadataProps) => {
|
|
1345
1403
|
return (
|
|
1346
|
-
<
|
|
1347
|
-
<
|
|
1348
|
-
<box style={{ flexDirection: 'row', paddingLeft: 1 }}>
|
|
1404
|
+
<MetadataContext.Provider value={listDetailMetadataConfig}>
|
|
1405
|
+
<box style={{ flexDirection: 'column' }}>
|
|
1349
1406
|
{props.children}
|
|
1350
1407
|
</box>
|
|
1351
|
-
</
|
|
1352
|
-
)
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
const ListItemDetailMetadataTagListItem = (props: { text?: string; color?: Color.ColorLike; icon?: Image.ImageLike; onAction?: () => void }) => {
|
|
1356
|
-
return (
|
|
1357
|
-
<box style={{ paddingRight: 1 }}>
|
|
1358
|
-
<text flexShrink={0} fg={resolveColor(props.color) || Theme.accent}>[{props.text}]</text>
|
|
1359
|
-
</box>
|
|
1408
|
+
</MetadataContext.Provider>
|
|
1360
1409
|
)
|
|
1361
1410
|
}
|
|
1362
1411
|
|
|
1363
1412
|
ListItemDetail.Metadata = ListItemDetailMetadata as any
|
|
1364
|
-
ListItemDetailMetadata.Label =
|
|
1365
|
-
ListItemDetailMetadata.Separator =
|
|
1366
|
-
ListItemDetailMetadata.Link =
|
|
1367
|
-
ListItemDetailMetadata.TagList =
|
|
1368
|
-
ListItemDetailMetadataTagList.Item = ListItemDetailMetadataTagListItem as any
|
|
1413
|
+
ListItemDetailMetadata.Label = Metadata.Label as any
|
|
1414
|
+
ListItemDetailMetadata.Separator = Metadata.Separator as any
|
|
1415
|
+
ListItemDetailMetadata.Link = Metadata.Link as any
|
|
1416
|
+
ListItemDetailMetadata.TagList = Metadata.TagList as any
|
|
1369
1417
|
|
|
1370
1418
|
ListItem.Detail = ListItemDetail
|
|
1371
1419
|
|
|
@@ -1378,6 +1426,7 @@ ListItem.Detail = ListItemDetail
|
|
|
1378
1426
|
* value="" (or your preferred reset value) at the top of your dropdown items.
|
|
1379
1427
|
*/
|
|
1380
1428
|
const ListDropdown: ListDropdownType = (props) => {
|
|
1429
|
+
const theme = useTheme()
|
|
1381
1430
|
const listContext = useContext(ListContext)
|
|
1382
1431
|
const [isHovered, setIsHovered] = useState(false)
|
|
1383
1432
|
|
|
@@ -1442,8 +1491,8 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1442
1491
|
[],
|
|
1443
1492
|
)
|
|
1444
1493
|
|
|
1445
|
-
// Open dropdown dialog when triggered
|
|
1446
|
-
|
|
1494
|
+
// Open dropdown dialog when triggered (before paint to avoid flash)
|
|
1495
|
+
useLayoutEffect(() => {
|
|
1447
1496
|
if (isDropdownOpen && !dialog.stack.length) {
|
|
1448
1497
|
// Pass the children to the dialog to render them there
|
|
1449
1498
|
dialog.push({
|
|
@@ -1499,7 +1548,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1499
1548
|
// minWidth: value.length + 4,
|
|
1500
1549
|
flexDirection: 'row',
|
|
1501
1550
|
flexShrink: 0,
|
|
1502
|
-
backgroundColor: isHovered ?
|
|
1551
|
+
backgroundColor: isHovered ? theme.backgroundPanel : undefined,
|
|
1503
1552
|
}}
|
|
1504
1553
|
onMouseMove={() => setIsHovered(true)}
|
|
1505
1554
|
onMouseOut={() => setIsHovered(false)}
|
|
@@ -1511,16 +1560,22 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1511
1560
|
}}
|
|
1512
1561
|
>
|
|
1513
1562
|
{/*<text >^p </text>*/}
|
|
1563
|
+
{listContext.isLoading ? (
|
|
1564
|
+
<LoadingText isLoading color={isHovered ? theme.text : theme.textMuted}>
|
|
1565
|
+
{displayValue || 'Loading...'}
|
|
1566
|
+
</LoadingText>
|
|
1567
|
+
) : (
|
|
1568
|
+
<text
|
|
1569
|
+
flexShrink={0}
|
|
1570
|
+
fg={isHovered ? theme.text : theme.textMuted}
|
|
1571
|
+
selectable={false}
|
|
1572
|
+
>
|
|
1573
|
+
{displayValue}
|
|
1574
|
+
</text>
|
|
1575
|
+
)}
|
|
1514
1576
|
<text
|
|
1515
1577
|
flexShrink={0}
|
|
1516
|
-
fg={isHovered ?
|
|
1517
|
-
selectable={false}
|
|
1518
|
-
>
|
|
1519
|
-
{displayValue}
|
|
1520
|
-
</text>
|
|
1521
|
-
<text
|
|
1522
|
-
flexShrink={0}
|
|
1523
|
-
fg={isHovered ? Theme.text : Theme.textMuted}
|
|
1578
|
+
fg={isHovered ? theme.text : theme.textMuted}
|
|
1524
1579
|
selectable={false}
|
|
1525
1580
|
>
|
|
1526
1581
|
{' '}
|
|
@@ -1533,6 +1588,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1533
1588
|
}
|
|
1534
1589
|
|
|
1535
1590
|
ListDropdown.Item = (props) => {
|
|
1591
|
+
const theme = useTheme()
|
|
1536
1592
|
const dropdownContext = useContext(DropdownContext)
|
|
1537
1593
|
const [isHovered, setIsHovered] = useState(false)
|
|
1538
1594
|
|
|
@@ -1596,9 +1652,9 @@ ListDropdown.Item = (props) => {
|
|
|
1596
1652
|
style={{
|
|
1597
1653
|
flexDirection: 'row',
|
|
1598
1654
|
backgroundColor: isActive
|
|
1599
|
-
?
|
|
1655
|
+
? theme.primary
|
|
1600
1656
|
: isHovered
|
|
1601
|
-
?
|
|
1657
|
+
? theme.backgroundPanel
|
|
1602
1658
|
: undefined,
|
|
1603
1659
|
paddingLeft: isActive ? 0 : 1,
|
|
1604
1660
|
paddingRight: 1,
|
|
@@ -1611,7 +1667,7 @@ ListDropdown.Item = (props) => {
|
|
|
1611
1667
|
>
|
|
1612
1668
|
<box style={{ flexDirection: 'row' }}>
|
|
1613
1669
|
{isActive && (
|
|
1614
|
-
<text flexShrink={0} fg={
|
|
1670
|
+
<text flexShrink={0} fg={theme.background} selectable={false}>
|
|
1615
1671
|
›{''}
|
|
1616
1672
|
</text>
|
|
1617
1673
|
)}
|
|
@@ -1619,10 +1675,10 @@ ListDropdown.Item = (props) => {
|
|
|
1619
1675
|
flexShrink={0}
|
|
1620
1676
|
fg={
|
|
1621
1677
|
isActive
|
|
1622
|
-
?
|
|
1678
|
+
? theme.background
|
|
1623
1679
|
: isCurrent
|
|
1624
|
-
?
|
|
1625
|
-
:
|
|
1680
|
+
? theme.primary
|
|
1681
|
+
: theme.text
|
|
1626
1682
|
}
|
|
1627
1683
|
attributes={isActive ? TextAttributes.BOLD : undefined}
|
|
1628
1684
|
selectable={false}
|
|
@@ -1638,6 +1694,7 @@ ListDropdown.Item = (props) => {
|
|
|
1638
1694
|
}
|
|
1639
1695
|
|
|
1640
1696
|
ListDropdown.Section = (props) => {
|
|
1697
|
+
const theme = useTheme()
|
|
1641
1698
|
const parentContext = useContext(DropdownContext)
|
|
1642
1699
|
|
|
1643
1700
|
// If not inside a Dropdown, just render nothing
|
|
@@ -1665,7 +1722,7 @@ ListDropdown.Section = (props) => {
|
|
|
1665
1722
|
{/* Render section title if we're in the dialog and not searching */}
|
|
1666
1723
|
{showTitle && (
|
|
1667
1724
|
<box style={{ paddingTop: 1, paddingLeft: 1 }}>
|
|
1668
|
-
<text flexShrink={0} fg={
|
|
1725
|
+
<text flexShrink={0} fg={theme.accent} attributes={TextAttributes.BOLD}>
|
|
1669
1726
|
{props.title}
|
|
1670
1727
|
</text>
|
|
1671
1728
|
</box>
|
|
@@ -1679,16 +1736,13 @@ ListDropdown.Section = (props) => {
|
|
|
1679
1736
|
|
|
1680
1737
|
List.Item = ListItem
|
|
1681
1738
|
const ListSection = (props: SectionProps) => {
|
|
1739
|
+
const theme = useTheme()
|
|
1682
1740
|
const parentContext = useContext(ListSectionContext)
|
|
1683
1741
|
const listContext = useContext(ListContext)
|
|
1684
1742
|
const searchText = listContext?.searchText || ''
|
|
1685
1743
|
|
|
1686
|
-
// Don't render empty sections
|
|
1687
|
-
if (React.Children.count(props.children) === 0) {
|
|
1688
|
-
return null
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
1744
|
// Create new context with section title and search text
|
|
1745
|
+
// NOTE: Must be called before any early returns to satisfy React hooks rules
|
|
1692
1746
|
const sectionContextValue = useMemo(
|
|
1693
1747
|
() => ({
|
|
1694
1748
|
...parentContext,
|
|
@@ -1698,6 +1752,11 @@ const ListSection = (props: SectionProps) => {
|
|
|
1698
1752
|
[parentContext, props.title, searchText],
|
|
1699
1753
|
)
|
|
1700
1754
|
|
|
1755
|
+
// Don't render empty sections
|
|
1756
|
+
if (React.Children.count(props.children) === 0) {
|
|
1757
|
+
return null
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1701
1760
|
const isSearching = searchText.trim().length > 0
|
|
1702
1761
|
|
|
1703
1762
|
const children = (
|
|
@@ -1719,7 +1778,7 @@ const ListSection = (props: SectionProps) => {
|
|
|
1719
1778
|
paddingLeft: 1,
|
|
1720
1779
|
}}
|
|
1721
1780
|
>
|
|
1722
|
-
<text flexShrink={0} fg={
|
|
1781
|
+
<text flexShrink={0} fg={theme.accent} attributes={TextAttributes.BOLD}>
|
|
1723
1782
|
{props.title}
|
|
1724
1783
|
</text>
|
|
1725
1784
|
</box>
|
|
@@ -1731,43 +1790,30 @@ const ListSection = (props: SectionProps) => {
|
|
|
1731
1790
|
|
|
1732
1791
|
List.Section = ListSection
|
|
1733
1792
|
List.Dropdown = ListDropdown
|
|
1734
|
-
|
|
1735
|
-
|
|
1793
|
+
// Inner component for EmptyView content (needs hooks at top level)
|
|
1794
|
+
function EmptyViewContent(props: EmptyViewProps): any {
|
|
1795
|
+
const theme = useTheme()
|
|
1736
1796
|
const inFocus = useIsInFocus()
|
|
1737
1797
|
|
|
1738
1798
|
// Handle keyboard for actions
|
|
1739
1799
|
useKeyboard((evt) => {
|
|
1740
1800
|
if (!inFocus) return
|
|
1741
1801
|
|
|
1742
|
-
// Handle Ctrl+K to show actions
|
|
1802
|
+
// Handle Ctrl+K to show actions dialog via portal
|
|
1743
1803
|
if (evt.name === 'k' && evt.ctrl) {
|
|
1744
|
-
|
|
1804
|
+
if (props.actions) {
|
|
1805
|
+
useStore.setState({ showActionsDialog: true })
|
|
1806
|
+
}
|
|
1745
1807
|
return
|
|
1746
1808
|
}
|
|
1747
1809
|
|
|
1748
|
-
// Handle Enter to execute first action
|
|
1810
|
+
// Handle Enter to auto-execute first action via ActionPanel
|
|
1749
1811
|
if (evt.name === 'return' && props.actions) {
|
|
1750
1812
|
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
1751
|
-
dialog.pushActions(props.actions)
|
|
1752
1813
|
}
|
|
1753
1814
|
})
|
|
1754
1815
|
|
|
1755
|
-
|
|
1756
|
-
const getIconString = (icon: Image.ImageLike): string | null => {
|
|
1757
|
-
if (typeof icon === 'string') {
|
|
1758
|
-
return getIconEmoji(icon)
|
|
1759
|
-
}
|
|
1760
|
-
if (icon && typeof icon === 'object' && 'source' in icon) {
|
|
1761
|
-
// For { source: string } or { source: { light, dark } } objects
|
|
1762
|
-
const source = icon.source
|
|
1763
|
-
if (typeof source === 'string') {
|
|
1764
|
-
return getIconEmoji(source)
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
return null
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
const iconEmoji = props.icon ? getIconString(props.icon) : null
|
|
1816
|
+
const iconEmoji = props.icon ? getIconValue(props.icon) || null : null
|
|
1771
1817
|
|
|
1772
1818
|
return (
|
|
1773
1819
|
<box
|
|
@@ -1784,24 +1830,46 @@ List.EmptyView = (props: EmptyViewProps) => {
|
|
|
1784
1830
|
}}
|
|
1785
1831
|
>
|
|
1786
1832
|
{iconEmoji && (
|
|
1787
|
-
<text flexShrink={0} fg={
|
|
1833
|
+
<text flexShrink={0} fg={theme.textMuted} style={{ marginBottom: 1 }}>
|
|
1788
1834
|
{iconEmoji}
|
|
1789
1835
|
</text>
|
|
1790
1836
|
)}
|
|
1791
1837
|
{props.title && (
|
|
1792
|
-
<text flexShrink={0} fg={
|
|
1838
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
1793
1839
|
{props.title?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || ''}
|
|
1794
1840
|
</text>
|
|
1795
1841
|
)}
|
|
1796
1842
|
{props.description && (
|
|
1797
|
-
<text flexShrink={0} fg={
|
|
1843
|
+
<text flexShrink={0} fg={theme.textMuted} wrapMode='word'>
|
|
1798
1844
|
{props.description?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || ''}
|
|
1799
1845
|
</text>
|
|
1800
1846
|
)}
|
|
1847
|
+
{/* Render actions offscreen to capture them */}
|
|
1848
|
+
{props.actions && <Offscreen>{props.actions}</Offscreen>}
|
|
1801
1849
|
</box>
|
|
1802
1850
|
)
|
|
1803
1851
|
}
|
|
1804
1852
|
|
|
1853
|
+
List.EmptyView = (props: EmptyViewProps) => {
|
|
1854
|
+
const listContext = useContext(ListContext)
|
|
1855
|
+
|
|
1856
|
+
// Register that a custom empty view exists
|
|
1857
|
+
useLayoutEffect(() => {
|
|
1858
|
+
if (listContext?.customEmptyViewRef) {
|
|
1859
|
+
listContext.customEmptyViewRef.current = true
|
|
1860
|
+
return () => {
|
|
1861
|
+
listContext.customEmptyViewRef.current = false
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}, [listContext])
|
|
1865
|
+
|
|
1866
|
+
return (
|
|
1867
|
+
<ShowOnNoItems isCustomEmptyView>
|
|
1868
|
+
<EmptyViewContent {...props} />
|
|
1869
|
+
</ShowOnNoItems>
|
|
1870
|
+
)
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1805
1873
|
export default List
|
|
1806
1874
|
|
|
1807
1875
|
// Grid Component Implementation
|