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
|
@@ -6,13 +6,14 @@ import React, {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useEffect,
|
|
8
8
|
} from 'react'
|
|
9
|
-
import { useKeyboard } from '@opentui/react'
|
|
9
|
+
import { useKeyboard, flushSync } from '@opentui/react'
|
|
10
10
|
import { useForm, FormProvider } from 'react-hook-form'
|
|
11
11
|
import { ActionPanel } from 'termcast/src/components/actions'
|
|
12
12
|
import { logger } from 'termcast/src/logger'
|
|
13
13
|
import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
14
14
|
import { useDialog } from 'termcast/src/internal/dialog'
|
|
15
|
-
import {
|
|
15
|
+
import { Offscreen } from 'termcast/src/internal/offscreen'
|
|
16
|
+
import { useTheme } from 'termcast/src/theme'
|
|
16
17
|
import { useStore } from 'termcast/src/state'
|
|
17
18
|
import { Footer } from 'termcast/src/components/footer'
|
|
18
19
|
import {
|
|
@@ -103,27 +104,28 @@ export const useFormSubmit = () => {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
function FormFooter(): any {
|
|
107
|
+
const theme = useTheme()
|
|
106
108
|
const hasToast = useStore((s) => s.toast !== null)
|
|
107
109
|
|
|
108
110
|
const content = hasToast ? null : (
|
|
109
111
|
<box style={{ flexDirection: 'row', gap: 3 }}>
|
|
110
112
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
111
|
-
<text flexShrink={0} fg={
|
|
113
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
112
114
|
ctrl ↵
|
|
113
115
|
</text>
|
|
114
|
-
<text flexShrink={0} fg={
|
|
116
|
+
<text flexShrink={0} fg={theme.textMuted}>submit</text>
|
|
115
117
|
</box>
|
|
116
118
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
117
|
-
<text flexShrink={0} fg={
|
|
119
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
118
120
|
tab
|
|
119
121
|
</text>
|
|
120
|
-
<text flexShrink={0} fg={
|
|
122
|
+
<text flexShrink={0} fg={theme.textMuted}>navigate</text>
|
|
121
123
|
</box>
|
|
122
124
|
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
123
|
-
<text flexShrink={0} fg={
|
|
125
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
124
126
|
^k
|
|
125
127
|
</text>
|
|
126
|
-
<text flexShrink={0} fg={
|
|
128
|
+
<text flexShrink={0} fg={theme.textMuted}>actions</text>
|
|
127
129
|
</box>
|
|
128
130
|
</box>
|
|
129
131
|
)
|
|
@@ -197,20 +199,12 @@ export const Form: FormType = ((props) => {
|
|
|
197
199
|
const scrollBoxRef = useRef<ScrollBoxRenderable>(null)
|
|
198
200
|
const descendantsContext = useFormFieldDescendants()
|
|
199
201
|
|
|
200
|
-
// Helper to get sorted field IDs
|
|
201
|
-
const getFieldIds = () => {
|
|
202
|
-
return Object.values(descendantsContext.map.current)
|
|
203
|
-
.filter((item) => item.index !== -1 && item.props?.id)
|
|
204
|
-
.sort((a, b) => a.index - b.index)
|
|
205
|
-
.map((item) => item.props!.id)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
202
|
const scrollToField = (fieldId: string) => {
|
|
209
203
|
const scrollBox = scrollBoxRef.current
|
|
210
204
|
if (!scrollBox) return
|
|
211
205
|
|
|
212
|
-
// Find field in
|
|
213
|
-
const field = Object.values(descendantsContext.
|
|
206
|
+
// Find field in committedMap by matching props.id
|
|
207
|
+
const field = Object.values(descendantsContext.committedMap).find(
|
|
214
208
|
(item) => item.props?.id === fieldId,
|
|
215
209
|
)
|
|
216
210
|
const elementRef = field?.props?.elementRef
|
|
@@ -228,12 +222,22 @@ export const Form: FormType = ((props) => {
|
|
|
228
222
|
}
|
|
229
223
|
|
|
230
224
|
const setFocusedField = (id: string | null) => {
|
|
231
|
-
|
|
225
|
+
flushSync(() => {
|
|
226
|
+
setFocusedFieldRaw(id)
|
|
227
|
+
})
|
|
232
228
|
if (id) {
|
|
233
229
|
scrollToField(id)
|
|
234
230
|
}
|
|
235
231
|
}
|
|
236
232
|
|
|
233
|
+
// Helper to get sorted field IDs - uses committedMap for stability
|
|
234
|
+
const getFieldIds = () => {
|
|
235
|
+
return Object.values(descendantsContext.committedMap)
|
|
236
|
+
.filter((item) => item.index !== -1 && item.props?.id)
|
|
237
|
+
.sort((a, b) => a.index - b.index)
|
|
238
|
+
.map((item) => item.props!.id)
|
|
239
|
+
}
|
|
240
|
+
|
|
237
241
|
// Focus first field helper
|
|
238
242
|
const focusFirstField = () => {
|
|
239
243
|
const fieldIds = getFieldIds()
|
|
@@ -257,16 +261,15 @@ export const Form: FormType = ((props) => {
|
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
// Auto-focus first field when descendants become available
|
|
260
|
-
// Runs on every render until a field is focused (handles async loading)
|
|
261
264
|
useEffect(() => {
|
|
262
265
|
if (focusedField) return
|
|
263
266
|
|
|
264
267
|
const fieldIds = getFieldIds()
|
|
265
268
|
if (fieldIds.length > 0) {
|
|
266
269
|
logger.log(`auto-focusing first field:`, fieldIds[0])
|
|
267
|
-
|
|
270
|
+
setFocusedField(fieldIds[0])
|
|
268
271
|
}
|
|
269
|
-
})
|
|
272
|
+
}, [descendantsContext.committedMap])
|
|
270
273
|
|
|
271
274
|
// Get focus state and dialog
|
|
272
275
|
const inFocus = useIsInFocus()
|
|
@@ -328,31 +331,15 @@ export const Form: FormType = ((props) => {
|
|
|
328
331
|
}
|
|
329
332
|
|
|
330
333
|
if (evt.name === 'k' && evt.ctrl) {
|
|
331
|
-
// Ctrl+K shows actions
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
341
|
-
dialog.pushActions(
|
|
342
|
-
<FormSubmitContext.Provider value={submitContextValue}>
|
|
343
|
-
{props.actions}
|
|
344
|
-
</FormSubmitContext.Provider>,
|
|
345
|
-
'center',
|
|
346
|
-
)
|
|
347
|
-
} else if (evt.name === 'return' && evt.meta && props.actions) {
|
|
348
|
-
// Cmd+Return also executes first action directly
|
|
349
|
-
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
350
|
-
dialog.pushActions(
|
|
351
|
-
<FormSubmitContext.Provider value={submitContextValue}>
|
|
352
|
-
{props.actions}
|
|
353
|
-
</FormSubmitContext.Provider>,
|
|
354
|
-
'center',
|
|
355
|
-
)
|
|
334
|
+
// Ctrl+K shows actions dialog via portal
|
|
335
|
+
if (props.actions) {
|
|
336
|
+
useStore.setState({ showActionsDialog: true })
|
|
337
|
+
}
|
|
338
|
+
} else if ((evt.name === 'return' && evt.ctrl) || (evt.name === 'return' && evt.meta)) {
|
|
339
|
+
// Ctrl+Return or Cmd+Return auto-executes first action via ActionPanel
|
|
340
|
+
if (props.actions) {
|
|
341
|
+
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
342
|
+
}
|
|
356
343
|
}
|
|
357
344
|
})
|
|
358
345
|
|
|
@@ -426,6 +413,8 @@ export const Form: FormType = ((props) => {
|
|
|
426
413
|
</box>
|
|
427
414
|
</ScrollBox>
|
|
428
415
|
<FormFooter />
|
|
416
|
+
{/* Render actions offscreen to capture them with FormSubmitContext */}
|
|
417
|
+
{props.actions && <Offscreen>{props.actions}</Offscreen>}
|
|
429
418
|
</box>
|
|
430
419
|
</box>
|
|
431
420
|
</FocusContext.Provider>
|
|
@@ -461,6 +450,7 @@ Form.Description = Description
|
|
|
461
450
|
|
|
462
451
|
// LinkAccessory component - shows a link in the navigation bar
|
|
463
452
|
function LinkAccessory(props: LinkAccessoryProps): any {
|
|
453
|
+
const theme = useTheme()
|
|
464
454
|
return (
|
|
465
455
|
<box
|
|
466
456
|
style={{
|
|
@@ -471,7 +461,7 @@ function LinkAccessory(props: LinkAccessoryProps): any {
|
|
|
471
461
|
}}
|
|
472
462
|
>
|
|
473
463
|
<text
|
|
474
|
-
fg={
|
|
464
|
+
fg={theme.textMuted}
|
|
475
465
|
attributes={TextAttributes.UNDERLINE}
|
|
476
466
|
wrapMode='none'
|
|
477
467
|
>
|
|
@@ -3,8 +3,8 @@ import { BoxRenderable } from '@opentui/core'
|
|
|
3
3
|
import { useFormContext, Controller } from 'react-hook-form'
|
|
4
4
|
import { useFocusContext, useFormFieldDescendant } from './index'
|
|
5
5
|
import { FormItemProps, FormItemRef } from './types'
|
|
6
|
-
import {
|
|
7
|
-
import { WithLeftBorder } from './with-left-border'
|
|
6
|
+
import { useTheme } from 'termcast/src/theme'
|
|
7
|
+
import { WithLeftBorder, TitleIndicator } from './with-left-border'
|
|
8
8
|
import { useFormNavigation } from './use-form-navigation'
|
|
9
9
|
import { LoadingText } from 'termcast/src/components/loading-text'
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ export interface PasswordFieldProps extends FormItemProps<string> {
|
|
|
15
15
|
export type PasswordFieldRef = FormItemRef
|
|
16
16
|
|
|
17
17
|
export const PasswordField = (props: PasswordFieldProps): any => {
|
|
18
|
+
const theme = useTheme()
|
|
18
19
|
const { control } = useFormContext()
|
|
19
20
|
const focusContext = useFocusContext()
|
|
20
21
|
const { focusedField, setFocusedField } = focusContext
|
|
@@ -40,21 +41,21 @@ export const PasswordField = (props: PasswordFieldProps): any => {
|
|
|
40
41
|
|
|
41
42
|
return (
|
|
42
43
|
<box ref={elementRef} flexDirection="column">
|
|
43
|
-
<WithLeftBorder
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<LoadingText
|
|
50
|
-
isLoading={isFocused && focusContext.isLoading}
|
|
51
|
-
color={isFocused ? Theme.primary : Theme.text}
|
|
44
|
+
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
45
|
+
<TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
|
|
46
|
+
<box
|
|
47
|
+
onMouseDown={() => {
|
|
48
|
+
setFocusedField(props.id)
|
|
49
|
+
}}
|
|
52
50
|
>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
<LoadingText
|
|
52
|
+
isLoading={isFocused && focusContext.isLoading}
|
|
53
|
+
color={isFocused ? theme.primary : theme.text}
|
|
54
|
+
>
|
|
55
|
+
{props.title || ''}
|
|
56
|
+
</LoadingText>
|
|
57
|
+
</box>
|
|
58
|
+
</TitleIndicator>
|
|
58
59
|
<input
|
|
59
60
|
value={displayValue}
|
|
60
61
|
onInput={(newDisplay: string) => {
|
|
@@ -85,19 +86,16 @@ export const PasswordField = (props: PasswordFieldProps): any => {
|
|
|
85
86
|
setFocusedField(props.id)
|
|
86
87
|
}}
|
|
87
88
|
/>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<text fg={Theme.error}>
|
|
89
|
+
{(fieldState.error || props.error || props.info) && <box height={1} />}
|
|
90
|
+
{(fieldState.error || props.error) && (
|
|
91
|
+
<text fg={theme.error}>
|
|
92
92
|
{fieldState.error?.message || props.error}
|
|
93
93
|
</text>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
</WithLeftBorder>
|
|
100
|
-
)}
|
|
94
|
+
)}
|
|
95
|
+
{props.info && (
|
|
96
|
+
<text fg={theme.textMuted}>{props.info}</text>
|
|
97
|
+
)}
|
|
98
|
+
</WithLeftBorder>
|
|
101
99
|
</box>
|
|
102
100
|
) as React.ReactElement
|
|
103
101
|
}}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { useTheme } from 'termcast/src/theme'
|
|
3
3
|
import { WithLeftBorder } from './with-left-border'
|
|
4
4
|
|
|
5
5
|
export const Separator = (): any => {
|
|
6
|
+
const theme = useTheme()
|
|
6
7
|
return null
|
|
7
8
|
return (
|
|
8
9
|
<>
|
|
9
10
|
<WithLeftBorder withDiamond isFocused={false}>
|
|
10
|
-
<text fg={
|
|
11
|
+
<text fg={theme.border}>{''.repeat(40)}</text>
|
|
11
12
|
</WithLeftBorder>
|
|
12
13
|
|
|
|
13
14
|
</>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RefAttributes,
|
|
7
7
|
} from 'react'
|
|
8
8
|
import { Image } from 'termcast/src/components/list'
|
|
9
|
+
import { getIconValue } from 'termcast/src/components/icon'
|
|
9
10
|
import { Dropdown, DropdownProps, DropdownItemProps } from './dropdown'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -158,7 +159,7 @@ const TagPickerItem: FunctionComponent<TagPickerItemProps> = (props): any => {
|
|
|
158
159
|
<Dropdown.Item
|
|
159
160
|
value={props.value}
|
|
160
161
|
title={props.title || props.value}
|
|
161
|
-
icon={props.icon
|
|
162
|
+
icon={getIconValue(props.icon) || undefined}
|
|
162
163
|
/>
|
|
163
164
|
)
|
|
164
165
|
}
|
|
@@ -3,8 +3,8 @@ import { BoxRenderable, TextareaRenderable } from '@opentui/core'
|
|
|
3
3
|
import { useFormContext } from 'react-hook-form'
|
|
4
4
|
import { useFocusContext, useFormFieldDescendant } from './index'
|
|
5
5
|
import { FormItemProps, FormItemRef } from './types'
|
|
6
|
-
import {
|
|
7
|
-
import { WithLeftBorder } from './with-left-border'
|
|
6
|
+
import { useTheme } from 'termcast/src/theme'
|
|
7
|
+
import { WithLeftBorder, TitleIndicator } from './with-left-border'
|
|
8
8
|
import { useFormNavigation } from './use-form-navigation'
|
|
9
9
|
import { createTextareaFormRef } from './form-ref'
|
|
10
10
|
import { LoadingText } from 'termcast/src/components/loading-text'
|
|
@@ -17,6 +17,7 @@ export interface TextAreaProps extends FormItemProps<string> {
|
|
|
17
17
|
export type TextAreaRef = FormItemRef
|
|
18
18
|
|
|
19
19
|
export const TextArea = (props: TextAreaProps): any => {
|
|
20
|
+
const theme = useTheme()
|
|
20
21
|
const { register, formState } = useFormContext()
|
|
21
22
|
const focusContext = useFocusContext()
|
|
22
23
|
const { focusedField, setFocusedField } = focusContext
|
|
@@ -60,23 +61,22 @@ export const TextArea = (props: TextAreaProps): any => {
|
|
|
60
61
|
|
|
61
62
|
return (
|
|
62
63
|
<box ref={elementRef} flexDirection="column">
|
|
63
|
-
<WithLeftBorder
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<LoadingText
|
|
70
|
-
isLoading={isFocused && focusContext.isLoading}
|
|
71
|
-
color={isFocused ? Theme.primary : Theme.text}
|
|
64
|
+
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
65
|
+
<TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
|
|
66
|
+
<box
|
|
67
|
+
onMouseDown={() => {
|
|
68
|
+
setFocusedField(props.id)
|
|
69
|
+
}}
|
|
72
70
|
>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
<LoadingText
|
|
72
|
+
isLoading={isFocused && focusContext.isLoading}
|
|
73
|
+
color={isFocused ? theme.primary : theme.text}
|
|
74
|
+
>
|
|
75
|
+
{props.title || ''}
|
|
76
|
+
</LoadingText>
|
|
77
|
+
</box>
|
|
78
|
+
</TitleIndicator>
|
|
79
|
+
<box flexGrow={1} paddingBottom={1}>
|
|
80
80
|
<textarea
|
|
81
81
|
ref={handleRef}
|
|
82
82
|
wrapMode='none'
|
|
@@ -90,20 +90,15 @@ export const TextArea = (props: TextAreaProps): any => {
|
|
|
90
90
|
}}
|
|
91
91
|
/>
|
|
92
92
|
</box>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{(fieldError || props.error) && (
|
|
96
|
-
<WithLeftBorder isFocused={isFocused}>
|
|
97
|
-
<text fg={Theme.error}>
|
|
93
|
+
{(fieldError || props.error) && (
|
|
94
|
+
<text fg={theme.error}>
|
|
98
95
|
{(fieldError?.message as string) || props.error}
|
|
99
96
|
</text>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
</WithLeftBorder>
|
|
106
|
-
)}
|
|
97
|
+
)}
|
|
98
|
+
{props.info && (
|
|
99
|
+
<text fg={theme.textMuted}>{props.info}</text>
|
|
100
|
+
)}
|
|
101
|
+
</WithLeftBorder>
|
|
107
102
|
</box>
|
|
108
103
|
)
|
|
109
104
|
}
|
|
@@ -3,8 +3,8 @@ import { BoxRenderable, TextareaRenderable } from '@opentui/core'
|
|
|
3
3
|
import { useFormContext } from 'react-hook-form'
|
|
4
4
|
import { useFocusContext, useFormFieldDescendant } from './index'
|
|
5
5
|
import { FormItemProps, FormItemRef } from './types'
|
|
6
|
-
import {
|
|
7
|
-
import { WithLeftBorder } from './with-left-border'
|
|
6
|
+
import { useTheme } from 'termcast/src/theme'
|
|
7
|
+
import { WithLeftBorder, TitleIndicator } from './with-left-border'
|
|
8
8
|
import { useFormNavigation } from './use-form-navigation'
|
|
9
9
|
import { createTextareaFormRef } from './form-ref'
|
|
10
10
|
import { LoadingText } from 'termcast/src/components/loading-text'
|
|
@@ -16,6 +16,7 @@ export interface TextFieldProps extends FormItemProps<string> {
|
|
|
16
16
|
export type TextFieldRef = FormItemRef
|
|
17
17
|
|
|
18
18
|
export const TextField = (props: TextFieldProps): any => {
|
|
19
|
+
const theme = useTheme()
|
|
19
20
|
const { register, formState } = useFormContext()
|
|
20
21
|
const focusContext = useFocusContext()
|
|
21
22
|
const { focusedField, setFocusedField } = focusContext
|
|
@@ -61,21 +62,21 @@ export const TextField = (props: TextFieldProps): any => {
|
|
|
61
62
|
|
|
62
63
|
return (
|
|
63
64
|
<box ref={elementRef} flexDirection="column">
|
|
64
|
-
<WithLeftBorder
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<LoadingText
|
|
71
|
-
isLoading={isFocused && focusContext.isLoading}
|
|
72
|
-
color={isFocused ? Theme.primary : Theme.text}
|
|
65
|
+
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
66
|
+
<TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
|
|
67
|
+
<box
|
|
68
|
+
onMouseDown={() => {
|
|
69
|
+
setFocusedField(props.id)
|
|
70
|
+
}}
|
|
73
71
|
>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
<LoadingText
|
|
73
|
+
isLoading={isFocused && focusContext.isLoading}
|
|
74
|
+
color={isFocused ? theme.primary : theme.text}
|
|
75
|
+
>
|
|
76
|
+
{props.title || ''}
|
|
77
|
+
</LoadingText>
|
|
78
|
+
</box>
|
|
79
|
+
</TitleIndicator>
|
|
79
80
|
<textarea
|
|
80
81
|
ref={handleRef}
|
|
81
82
|
height={1}
|
|
@@ -92,19 +93,16 @@ export const TextField = (props: TextFieldProps): any => {
|
|
|
92
93
|
setFocusedField(props.id)
|
|
93
94
|
}}
|
|
94
95
|
/>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<text fg={Theme.error}>
|
|
96
|
+
{(fieldError || props.error || props.info) && <box height={1} />}
|
|
97
|
+
{(fieldError || props.error) && (
|
|
98
|
+
<text fg={theme.error}>
|
|
99
99
|
{(fieldError?.message as string) || props.error}
|
|
100
100
|
</text>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</WithLeftBorder>
|
|
107
|
-
)}
|
|
101
|
+
)}
|
|
102
|
+
{props.info && (
|
|
103
|
+
<text fg={theme.textMuted}>{props.info}</text>
|
|
104
|
+
)}
|
|
105
|
+
</WithLeftBorder>
|
|
108
106
|
</box>
|
|
109
107
|
)
|
|
110
108
|
}
|
|
@@ -7,10 +7,10 @@ export function useFormNavigationHelpers(id: string) {
|
|
|
7
7
|
const scrollContext = useFormScrollContext()
|
|
8
8
|
const { setFocusedField } = useFocusContext()
|
|
9
9
|
|
|
10
|
-
// Get sorted field IDs from descendants
|
|
10
|
+
// Get sorted field IDs from descendants - use committedMap for stability
|
|
11
11
|
const getFieldIds = () => {
|
|
12
12
|
if (!scrollContext) return []
|
|
13
|
-
const descendants = Object.values(scrollContext.descendantsContext.
|
|
13
|
+
const descendants = Object.values(scrollContext.descendantsContext.committedMap)
|
|
14
14
|
.filter((item) => item.index !== -1 && item.props?.id)
|
|
15
15
|
.sort((a, b) => a.index - b.index)
|
|
16
16
|
return descendants.map((item) => item.props!.id)
|
|
@@ -53,9 +53,8 @@ export function useFormNavigation(
|
|
|
53
53
|
const isInFocus = useIsInFocus()
|
|
54
54
|
const isFocused = focusedField === id
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
handleArrows = false
|
|
56
|
+
// handleTabs defaults to false to avoid conflict with global Form handler
|
|
57
|
+
let { handleArrows = true, handleTabs = false } = options || {}
|
|
59
58
|
|
|
60
59
|
const { navigateToPrevious, navigateToNext } = useFormNavigationHelpers(id)
|
|
61
60
|
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { useTheme } from 'termcast/src/theme'
|
|
3
3
|
import { colord } from 'colord'
|
|
4
4
|
import { useAnimationTick, TICK_DIVISORS } from 'termcast/src/components/animation-tick'
|
|
5
5
|
|
|
6
|
-
const spinnerFrames = [
|
|
7
|
-
{ char: ' ', color: Theme.accent },
|
|
8
|
-
{ char: '·', color: Theme.accent },
|
|
9
|
-
{ char: '•', color: colord(Theme.accent).lighten(0.1).toHex() },
|
|
10
|
-
// { char: '●', color: colord(Theme.accent).lighten(0.2).toHex() },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
6
|
function Spinner(): any {
|
|
7
|
+
const theme = useTheme()
|
|
14
8
|
const tick = useAnimationTick(TICK_DIVISORS.SPINNER)
|
|
9
|
+
const spinnerFrames = [
|
|
10
|
+
{ char: ' ', color: theme.accent },
|
|
11
|
+
{ char: '·', color: theme.accent },
|
|
12
|
+
{ char: '•', color: colord(theme.accent).lighten(0.1).toHex() },
|
|
13
|
+
]
|
|
15
14
|
const frame = spinnerFrames[tick % spinnerFrames.length]
|
|
16
15
|
return (
|
|
17
16
|
<text flexShrink={0} fg={frame.color}>
|
|
@@ -20,6 +19,44 @@ function Spinner(): any {
|
|
|
20
19
|
)
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
interface TitleIndicatorProps {
|
|
23
|
+
isFocused: boolean
|
|
24
|
+
isLoading?: boolean
|
|
25
|
+
customCharacter?: { focused: string; unfocused: string }
|
|
26
|
+
children: React.ReactNode
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* TitleIndicator renders a title row with a diamond/custom indicator that
|
|
31
|
+
* uses negative margin to overlay the left border character.
|
|
32
|
+
* Must be used inside a WithLeftBorder component.
|
|
33
|
+
*/
|
|
34
|
+
export const TitleIndicator = ({
|
|
35
|
+
isFocused,
|
|
36
|
+
isLoading,
|
|
37
|
+
customCharacter,
|
|
38
|
+
children,
|
|
39
|
+
}: TitleIndicatorProps): any => {
|
|
40
|
+
const theme = useTheme()
|
|
41
|
+
const chars = customCharacter || { focused: '◆', unfocused: '◇' }
|
|
42
|
+
const color = isFocused ? theme.accent : theme.text
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<box flexDirection='row' marginLeft={-3}>
|
|
46
|
+
{isFocused && isLoading ? (
|
|
47
|
+
<Spinner />
|
|
48
|
+
) : (
|
|
49
|
+
<text flexShrink={0} fg={color}>
|
|
50
|
+
<b>{isFocused ? chars.focused : chars.unfocused}</b>
|
|
51
|
+
</text>
|
|
52
|
+
)}
|
|
53
|
+
<box flexShrink={0} flexGrow={1} paddingLeft={2}>
|
|
54
|
+
{children}
|
|
55
|
+
</box>
|
|
56
|
+
</box>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
23
60
|
interface WithLeftBorderProps {
|
|
24
61
|
children: React.ReactNode
|
|
25
62
|
withDiamond?: boolean
|
|
@@ -42,9 +79,10 @@ export const WithLeftBorder = ({
|
|
|
42
79
|
paddingLeft = 2,
|
|
43
80
|
paddingTop = 0,
|
|
44
81
|
}: WithLeftBorderProps): any => {
|
|
82
|
+
const theme = useTheme()
|
|
45
83
|
if (withDiamond || customCharacter) {
|
|
46
84
|
const chars = customCharacter || { focused: '◆', unfocused: '◇' }
|
|
47
|
-
const color = isFocused ?
|
|
85
|
+
const color = isFocused ? theme.accent : theme.text
|
|
48
86
|
return (
|
|
49
87
|
<box flexDirection='row'>
|
|
50
88
|
{isFocused && isLoading ? (
|
|
@@ -65,7 +103,7 @@ export const WithLeftBorder = ({
|
|
|
65
103
|
paddingLeft={paddingLeft}
|
|
66
104
|
border={['left']}
|
|
67
105
|
// borderStyle={isFocused ? 'heavy' : 'single'}
|
|
68
|
-
borderColor={isFocused ?
|
|
106
|
+
borderColor={isFocused ? theme.accent : theme.text}
|
|
69
107
|
flexShrink={0}
|
|
70
108
|
flexDirection='row'
|
|
71
109
|
>
|
package/src/components/icon.tsx
CHANGED
|
@@ -529,6 +529,75 @@ export function getIconEmoji(icon: string): string {
|
|
|
529
529
|
return icon
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Extract icon string from any icon format.
|
|
534
|
+
* Handles all Raycast icon formats:
|
|
535
|
+
* - string: direct icon name or emoji
|
|
536
|
+
* - { source: string }: image with source
|
|
537
|
+
* - { source: string, tintColor?: string }: image with tint
|
|
538
|
+
* - { source: { light, dark } }: theme-aware source
|
|
539
|
+
* - { value: ImageLike, tooltip: string }: icon with tooltip
|
|
540
|
+
* - { light: string, dark: string }: theme-aware icon directly
|
|
541
|
+
* - { fileIcon: string }: file icon
|
|
542
|
+
*
|
|
543
|
+
* Returns empty string for unhandled formats (never [object Object])
|
|
544
|
+
*/
|
|
545
|
+
export function getIconValue(icon: unknown): string {
|
|
546
|
+
if (!icon) {
|
|
547
|
+
return ''
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (typeof icon === 'string') {
|
|
551
|
+
return getIconEmoji(icon)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (typeof icon !== 'object') {
|
|
555
|
+
return ''
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const obj = icon as Record<string, unknown>
|
|
559
|
+
|
|
560
|
+
// Handle { value: ..., tooltip: ... } format
|
|
561
|
+
if ('value' in obj && obj.value) {
|
|
562
|
+
return getIconValue(obj.value)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Handle { source: ... } format
|
|
566
|
+
if ('source' in obj) {
|
|
567
|
+
const source = obj.source
|
|
568
|
+
if (typeof source === 'string') {
|
|
569
|
+
return getIconEmoji(source)
|
|
570
|
+
}
|
|
571
|
+
// Handle { source: { light, dark } }
|
|
572
|
+
if (typeof source === 'object' && source !== null) {
|
|
573
|
+
const sourceObj = source as Record<string, unknown>
|
|
574
|
+
if ('light' in sourceObj && typeof sourceObj.light === 'string') {
|
|
575
|
+
return getIconEmoji(sourceObj.light)
|
|
576
|
+
}
|
|
577
|
+
if ('dark' in sourceObj && typeof sourceObj.dark === 'string') {
|
|
578
|
+
return getIconEmoji(sourceObj.dark)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return ''
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Handle { light, dark } directly on icon
|
|
585
|
+
if ('light' in obj && typeof obj.light === 'string') {
|
|
586
|
+
return getIconEmoji(obj.light)
|
|
587
|
+
}
|
|
588
|
+
if ('dark' in obj && typeof obj.dark === 'string') {
|
|
589
|
+
return getIconEmoji(obj.dark)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Handle { fileIcon: string }
|
|
593
|
+
if ('fileIcon' in obj && typeof obj.fileIcon === 'string') {
|
|
594
|
+
return '📁'
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Unknown format - return empty string instead of [object Object]
|
|
598
|
+
return ''
|
|
599
|
+
}
|
|
600
|
+
|
|
532
601
|
interface IconProps {
|
|
533
602
|
source: string
|
|
534
603
|
tintColor?: string
|