termcast 1.3.50 → 1.3.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/environment.d.ts +1 -0
- package/dist/apis/environment.d.ts.map +1 -1
- package/dist/apis/environment.js +5 -0
- package/dist/apis/environment.js.map +1 -1
- package/dist/app.d.ts +33 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +1125 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.js +80 -0
- package/dist/cli.js.map +1 -1
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +20 -17
- package/dist/components/detail.js.map +1 -1
- package/dist/components/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown.js +3 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/footer.d.ts +6 -0
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +15 -6
- package/dist/components/footer.js.map +1 -1
- package/dist/components/form/checkbox.d.ts.map +1 -1
- package/dist/components/form/checkbox.js +1 -13
- package/dist/components/form/checkbox.js.map +1 -1
- package/dist/components/form/date-picker.js +2 -2
- package/dist/components/form/date-picker.js.map +1 -1
- package/dist/components/form/description.js +1 -1
- 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 +19 -3
- package/dist/components/form/dropdown.js.map +1 -1
- package/dist/components/form/file-picker.d.ts.map +1 -1
- package/dist/components/form/file-picker.js +22 -4
- package/dist/components/form/file-picker.js.map +1 -1
- package/dist/components/form/index.d.ts +3 -1
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +6 -4
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/password-field.js +3 -3
- package/dist/components/form/password-field.js.map +1 -1
- package/dist/components/form/text-area.d.ts.map +1 -1
- package/dist/components/form/text-area.js +29 -6
- package/dist/components/form/text-area.js.map +1 -1
- package/dist/components/form/text-field.js +3 -3
- package/dist/components/form/text-field.js.map +1 -1
- package/dist/components/heatmap.d.ts +80 -0
- package/dist/components/heatmap.d.ts.map +1 -0
- package/dist/components/heatmap.js +405 -0
- package/dist/components/heatmap.js.map +1 -0
- package/dist/components/list.d.ts +2 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +80 -52
- package/dist/components/list.js.map +1 -1
- package/dist/components/markdown.d.ts +7 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +19 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/metadata.d.ts.map +1 -1
- package/dist/components/metadata.js +4 -1
- package/dist/components/metadata.js.map +1 -1
- package/dist/components/progress-bar.d.ts +37 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +34 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/table.d.ts +3 -2
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +78 -63
- package/dist/components/table.js.map +1 -1
- package/dist/diagram-parser.d.ts +17 -3
- package/dist/diagram-parser.d.ts.map +1 -1
- package/dist/diagram-parser.js +17 -3
- package/dist/diagram-parser.js.map +1 -1
- package/dist/examples/list-slot.d.ts +2 -0
- package/dist/examples/list-slot.d.ts.map +1 -0
- package/dist/examples/list-slot.js +14 -0
- package/dist/examples/list-slot.js.map +1 -0
- package/dist/examples/list-with-dropdown.js +2 -4
- package/dist/examples/list-with-dropdown.js.map +1 -1
- package/dist/examples/simple-heatmap.d.ts +2 -0
- package/dist/examples/simple-heatmap.d.ts.map +1 -0
- package/dist/examples/simple-heatmap.js +37 -0
- package/dist/examples/simple-heatmap.js.map +1 -0
- package/dist/examples/simple-progress-bar.d.ts +2 -0
- package/dist/examples/simple-progress-bar.d.ts.map +1 -0
- package/dist/examples/simple-progress-bar.js +36 -0
- package/dist/examples/simple-progress-bar.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- 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 +5 -4
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/navigation.d.ts.map +1 -1
- package/dist/internal/navigation.js +7 -2
- package/dist/internal/navigation.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +42 -4
- package/dist/internal/providers.js.map +1 -1
- package/dist/logger.js +6 -1
- package/dist/logger.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +31 -2
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +23 -1
- package/dist/theme.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/apis/environment.tsx +6 -0
- package/src/app.tsx +1487 -0
- package/src/assets/default-app-icon.png +0 -0
- package/src/cli.tsx +105 -0
- package/src/components/detail.tsx +32 -22
- package/src/components/dropdown.tsx +3 -2
- package/src/components/footer.tsx +37 -7
- package/src/components/form/checkbox.tsx +2 -17
- package/src/components/form/date-picker.tsx +2 -2
- package/src/components/form/description.tsx +1 -1
- package/src/components/form/dropdown.tsx +22 -3
- package/src/components/form/file-picker.tsx +33 -10
- package/src/components/form/index.tsx +10 -6
- package/src/components/form/password-field.tsx +3 -3
- package/src/components/form/text-area.tsx +31 -6
- package/src/components/form/text-field.tsx +3 -3
- package/src/components/heatmap.tsx +584 -0
- package/src/components/list.tsx +135 -72
- package/src/components/markdown.tsx +30 -0
- package/src/components/metadata.tsx +9 -2
- package/src/components/progress-bar.tsx +112 -0
- package/src/components/table.tsx +88 -71
- package/src/diagram-parser.tsx +17 -3
- package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
- package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
- package/src/examples/form-basic.vitest.tsx +117 -16
- package/src/examples/graph-bar-chart.vitest.tsx +2 -2
- package/src/examples/graph-row.vitest.tsx +10 -10
- package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
- package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
- package/src/examples/list-dropdown-default.vitest.tsx +78 -58
- package/src/examples/list-slot.tsx +38 -0
- package/src/examples/list-with-detail.vitest.tsx +8 -8
- package/src/examples/list-with-dropdown.tsx +2 -2
- package/src/examples/list-with-dropdown.vitest.tsx +16 -16
- package/src/examples/list-with-sections.vitest.tsx +45 -32
- package/src/examples/simple-detail-table.vitest.tsx +2 -2
- package/src/examples/simple-file-picker.vitest.tsx +1 -1
- package/src/examples/simple-grid.vitest.tsx +27 -53
- package/src/examples/simple-heatmap.tsx +63 -0
- package/src/examples/simple-heatmap.vitest.tsx +88 -0
- package/src/examples/simple-progress-bar.tsx +82 -0
- package/src/examples/simple-progress-bar.vitest.tsx +72 -0
- package/src/examples/table-edge-cases.vitest.tsx +1 -1
- package/src/index.tsx +19 -0
- package/src/internal/date-picker-widget.tsx +23 -12
- package/src/internal/navigation.tsx +7 -2
- package/src/internal/providers.tsx +48 -3
- package/src/logger.tsx +6 -1
- package/src/state.tsx +38 -2
- package/src/theme.tsx +26 -2
- package/src/utils.tsx +6 -1
|
Binary file
|
package/src/cli.tsx
CHANGED
|
@@ -17,6 +17,8 @@ import './globals'
|
|
|
17
17
|
import { startDevMode, triggerRebuild } from './extensions/dev'
|
|
18
18
|
import { compileExtension } from './compile'
|
|
19
19
|
import { releaseExtension } from './release'
|
|
20
|
+
import { buildApp } from './app'
|
|
21
|
+
import { themeNames } from './themes'
|
|
20
22
|
import { runHomeCommand } from './extensions/home'
|
|
21
23
|
import { showToast, Toast } from './apis/toast'
|
|
22
24
|
import packageJson from '../package.json'
|
|
@@ -257,6 +259,109 @@ cli
|
|
|
257
259
|
}
|
|
258
260
|
})
|
|
259
261
|
|
|
262
|
+
cli
|
|
263
|
+
.command(
|
|
264
|
+
'app build [path]',
|
|
265
|
+
'Build a standalone desktop app from a termcast extension (macOS .app or Windows folder)',
|
|
266
|
+
)
|
|
267
|
+
.option('--name <name>', 'App display name (default: package.json title)')
|
|
268
|
+
.option('--icon <path>', 'Custom icon PNG path (default: extension icon or bundled default)')
|
|
269
|
+
.option('--bundle-id <id>', 'macOS bundle identifier (default: com.termcast.{name})')
|
|
270
|
+
.option('--release', 'Upload the app zip to the latest GitHub release')
|
|
271
|
+
.option('--entry <file>', 'Custom entry file (instead of auto-generated one)')
|
|
272
|
+
.option('--platform <platform>', 'Target platform: darwin or win32 (default: current OS)')
|
|
273
|
+
.option('--arch <arch>', 'Target architecture: arm64 or x64 (default: current machine)')
|
|
274
|
+
.option('--no-installer', 'Skip NSIS installer generation on Windows')
|
|
275
|
+
.option('--font <family>', 'Font family name (e.g. "Inter Mono", "Fira Code")')
|
|
276
|
+
.option('--font-dir <path>', 'Directory of .ttf/.otf files to bundle (auto-detects fonts/ in extension)')
|
|
277
|
+
.option('--font-size <size>', 'Font size in points (default: 14)')
|
|
278
|
+
.option('--line-height <height>', 'Line height multiplier (default: 1.2)')
|
|
279
|
+
.option('--theme <name>', 'Default theme name (default: nerv)')
|
|
280
|
+
.action(
|
|
281
|
+
async (
|
|
282
|
+
extensionPath: string,
|
|
283
|
+
options: {
|
|
284
|
+
name?: string
|
|
285
|
+
icon?: string
|
|
286
|
+
bundleId?: string
|
|
287
|
+
release?: boolean
|
|
288
|
+
entry?: string
|
|
289
|
+
platform?: string
|
|
290
|
+
arch?: string
|
|
291
|
+
installer?: boolean
|
|
292
|
+
font?: string
|
|
293
|
+
fontDir?: string
|
|
294
|
+
fontSize?: string
|
|
295
|
+
lineHeight?: string
|
|
296
|
+
theme?: string
|
|
297
|
+
},
|
|
298
|
+
) => {
|
|
299
|
+
extensionPath = path.resolve(extensionPath || process.cwd())
|
|
300
|
+
|
|
301
|
+
if (options.arch && options.arch !== 'arm64' && options.arch !== 'x64') {
|
|
302
|
+
console.error(`Invalid --arch "${options.arch}". Must be "arm64" or "x64".`)
|
|
303
|
+
process.exit(1)
|
|
304
|
+
}
|
|
305
|
+
if (options.platform && options.platform !== 'darwin' && options.platform !== 'win32') {
|
|
306
|
+
console.error(`Invalid --platform "${options.platform}". Must be "darwin" or "win32".`)
|
|
307
|
+
process.exit(1)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const fontSize = options.fontSize ? parseFloat(options.fontSize) : undefined
|
|
311
|
+
if (fontSize !== undefined && !Number.isFinite(fontSize)) {
|
|
312
|
+
console.error(`Invalid --font-size "${options.fontSize}". Must be a number.`)
|
|
313
|
+
process.exit(1)
|
|
314
|
+
}
|
|
315
|
+
const lineHeight = options.lineHeight ? parseFloat(options.lineHeight) : undefined
|
|
316
|
+
if (lineHeight !== undefined && !Number.isFinite(lineHeight)) {
|
|
317
|
+
console.error(`Invalid --line-height "${options.lineHeight}". Must be a number.`)
|
|
318
|
+
process.exit(1)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (options.theme && !themeNames.includes(options.theme)) {
|
|
322
|
+
console.error(`Invalid --theme "${options.theme}". Available themes: ${themeNames.join(', ')}`)
|
|
323
|
+
process.exit(1)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const result = await buildApp({
|
|
328
|
+
extensionPath,
|
|
329
|
+
name: options.name,
|
|
330
|
+
icon: options.icon,
|
|
331
|
+
bundleId: options.bundleId,
|
|
332
|
+
release: options.release,
|
|
333
|
+
entry: options.entry,
|
|
334
|
+
platform: options.platform as 'darwin' | 'win32' | undefined,
|
|
335
|
+
arch: options.arch as 'arm64' | 'x64' | undefined,
|
|
336
|
+
// goke parses --no-installer as installer:false
|
|
337
|
+
noInstaller: options.installer === false,
|
|
338
|
+
fontFamily: options.font,
|
|
339
|
+
fontDir: options.fontDir,
|
|
340
|
+
fontSize,
|
|
341
|
+
lineHeight,
|
|
342
|
+
theme: options.theme,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const resolvedPlatform = options.platform || process.platform
|
|
346
|
+
console.log(`\nApp built: ${result.appPath}`)
|
|
347
|
+
if (resolvedPlatform === 'win32') {
|
|
348
|
+
if (result.installerPath) {
|
|
349
|
+
console.log(`Installer: ${result.installerPath}`)
|
|
350
|
+
console.log(`Distribute the installer .exe. Users double-click to install.`)
|
|
351
|
+
} else {
|
|
352
|
+
console.log(`Distribute the folder as a zip. Users run: ${result.appName}.exe`)
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
console.log(`Run it with: open "${result.appPath}"`)
|
|
356
|
+
}
|
|
357
|
+
process.exit(0)
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error('App build failed:', error instanceof Error ? error.message : error)
|
|
360
|
+
process.exit(1)
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
|
|
260
365
|
cli
|
|
261
366
|
.command(
|
|
262
367
|
'raycast-pr <prNumber>',
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import React, { ReactNode, useMemo, ReactElement } from 'react'
|
|
2
2
|
import { TextAttributes } from '@opentui/core'
|
|
3
|
-
import { useKeyboard, useTerminalDimensions
|
|
4
|
-
import { useTheme
|
|
3
|
+
import { useKeyboard, useTerminalDimensions } from '@opentui/react'
|
|
4
|
+
import { useTheme } from 'termcast/src/theme'
|
|
5
5
|
import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
6
6
|
import { ActionPanel, Action } from 'termcast/src/components/actions'
|
|
7
|
-
import { Footer } from 'termcast/src/components/footer'
|
|
7
|
+
import { Footer, Hoverable } from 'termcast/src/components/footer'
|
|
8
8
|
|
|
9
9
|
import { useDialog } from 'termcast/src/internal/dialog'
|
|
10
|
+
import { useNavigation } from 'termcast/src/internal/navigation'
|
|
10
11
|
import { ScrollBox } from 'termcast/src/internal/scrollbox'
|
|
11
12
|
import { useStore } from 'termcast/src/state'
|
|
12
13
|
import { Offscreen } from 'termcast/src/internal/offscreen'
|
|
13
14
|
import { Metadata, MetadataContext, extractTitleLengths, defaultConfig } from 'termcast/src/components/metadata'
|
|
14
15
|
import type { LabelProps, SeparatorProps, LinkProps, TagListProps, TagListItemProps, MetadataConfig } from 'termcast/src/components/metadata'
|
|
15
|
-
import {
|
|
16
|
+
import { Markdown } from 'termcast/src/components/markdown'
|
|
16
17
|
|
|
17
18
|
interface ActionsInterface {
|
|
18
19
|
actions?: ReactNode
|
|
@@ -101,36 +102,54 @@ DetailMetadata.TagList = Metadata.TagList
|
|
|
101
102
|
function DetailFooter({
|
|
102
103
|
hasActions,
|
|
103
104
|
firstActionTitle,
|
|
105
|
+
onGoBack,
|
|
104
106
|
}: {
|
|
105
107
|
hasActions?: boolean
|
|
106
108
|
firstActionTitle?: string
|
|
109
|
+
onGoBack?: () => void
|
|
107
110
|
}): any {
|
|
108
111
|
const theme = useTheme()
|
|
109
112
|
|
|
110
113
|
return (
|
|
111
114
|
<Footer paddingLeft={0} paddingRight={0}>
|
|
112
115
|
<box style={{ flexDirection: 'row', gap: 3 }}>
|
|
113
|
-
<
|
|
116
|
+
<Hoverable
|
|
117
|
+
onMouseDown={() => {
|
|
118
|
+
// Defer pop so opentui finishes its mouse event walk before
|
|
119
|
+
// React unmounts the component tree (avoids Yoga WASM crash)
|
|
120
|
+
queueMicrotask(() => {
|
|
121
|
+
onGoBack?.()
|
|
122
|
+
})
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
114
125
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
115
126
|
esc
|
|
116
127
|
</text>
|
|
117
128
|
<text flexShrink={0} fg={theme.textMuted}>go back</text>
|
|
118
|
-
</
|
|
129
|
+
</Hoverable>
|
|
119
130
|
{hasActions && (
|
|
120
|
-
<
|
|
131
|
+
<Hoverable
|
|
132
|
+
onMouseDown={() => {
|
|
133
|
+
useStore.setState({ showActionsDialog: true })
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
121
136
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
122
137
|
^k
|
|
123
138
|
</text>
|
|
124
139
|
<text flexShrink={0} fg={theme.textMuted}>actions</text>
|
|
125
|
-
</
|
|
140
|
+
</Hoverable>
|
|
126
141
|
)}
|
|
127
142
|
{hasActions && firstActionTitle && (
|
|
128
|
-
<
|
|
143
|
+
<Hoverable
|
|
144
|
+
onMouseDown={() => {
|
|
145
|
+
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
129
148
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
130
149
|
↵
|
|
131
150
|
</text>
|
|
132
151
|
<text flexShrink={0} fg={theme.textMuted}>{firstActionTitle}</text>
|
|
133
|
-
</
|
|
152
|
+
</Hoverable>
|
|
134
153
|
)}
|
|
135
154
|
</box>
|
|
136
155
|
</Footer>
|
|
@@ -170,23 +189,13 @@ function getFirstActionTitle(actions: ReactNode): string | undefined {
|
|
|
170
189
|
return firstTitle
|
|
171
190
|
}
|
|
172
191
|
|
|
173
|
-
// Renders markdown with link URL stripping via renderNode.
|
|
174
|
-
// Links show only their title text (bold, primary) with click-to-open.
|
|
175
|
-
function MarkdownContent({ markdown }: { markdown: string }): any {
|
|
176
|
-
const renderer = useRenderer()
|
|
177
|
-
const renderNode = useMemo(() => {
|
|
178
|
-
return createMarkdownRenderNode(renderer)
|
|
179
|
-
}, [renderer])
|
|
180
192
|
|
|
181
|
-
return (
|
|
182
|
-
<markdown content={markdown} syntaxStyle={markdownSyntaxStyle} conceal renderNode={renderNode} />
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
193
|
|
|
186
194
|
const Detail: DetailType = (props) => {
|
|
187
195
|
const { actions } = props
|
|
188
196
|
const dialog = useDialog()
|
|
189
197
|
const inFocus = useIsInFocus()
|
|
198
|
+
const navigation = useNavigation()
|
|
190
199
|
|
|
191
200
|
const firstActionTitle = useMemo(() => {
|
|
192
201
|
return actions ? getFirstActionTitle(actions) : undefined
|
|
@@ -224,7 +233,7 @@ const Detail: DetailType = (props) => {
|
|
|
224
233
|
}}
|
|
225
234
|
>
|
|
226
235
|
{props.markdown && (
|
|
227
|
-
<
|
|
236
|
+
<Markdown content={props.markdown} />
|
|
228
237
|
)}
|
|
229
238
|
{props.metadata}
|
|
230
239
|
</box>
|
|
@@ -237,6 +246,7 @@ const Detail: DetailType = (props) => {
|
|
|
237
246
|
<DetailFooter
|
|
238
247
|
hasActions={true}
|
|
239
248
|
firstActionTitle={firstActionTitle}
|
|
249
|
+
onGoBack={navigation.pop}
|
|
240
250
|
/>
|
|
241
251
|
{/* Always mount ActionPanel offscreen so built-in actions are available */}
|
|
242
252
|
<Offscreen>{actions || <ActionPanel />}</Offscreen>
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
ScrollBoxRenderable,
|
|
15
15
|
TextareaRenderable,
|
|
16
16
|
} from '@opentui/core'
|
|
17
|
-
import { useTheme } from 'termcast/src/theme'
|
|
17
|
+
import { getInteractiveHoverBackground, useTheme } from 'termcast/src/theme'
|
|
18
18
|
import { getIconValue } from 'termcast/src/components/icon'
|
|
19
19
|
import { logger } from 'termcast/src/logger'
|
|
20
20
|
import { useStore } from 'termcast/src/state'
|
|
@@ -441,6 +441,7 @@ function ItemOption(props: {
|
|
|
441
441
|
elementRef?: React.Ref<any>
|
|
442
442
|
}) {
|
|
443
443
|
const theme = useTheme()
|
|
444
|
+
const hoverBackgroundColor = getInteractiveHoverBackground(theme)
|
|
444
445
|
const [isHovered, setIsHovered] = useState(false)
|
|
445
446
|
|
|
446
447
|
return (
|
|
@@ -451,7 +452,7 @@ function ItemOption(props: {
|
|
|
451
452
|
backgroundColor: props.active
|
|
452
453
|
? theme.primary
|
|
453
454
|
: isHovered
|
|
454
|
-
?
|
|
455
|
+
? hoverBackgroundColor
|
|
455
456
|
: undefined,
|
|
456
457
|
paddingLeft: props.active ? 0 : 1,
|
|
457
458
|
paddingRight: 1,
|
|
@@ -13,6 +13,32 @@ import {
|
|
|
13
13
|
} from 'termcast/src/state'
|
|
14
14
|
import { useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
15
15
|
|
|
16
|
+
interface HoverableProps {
|
|
17
|
+
children: ReactNode
|
|
18
|
+
onMouseDown?: () => void
|
|
19
|
+
flexShrink?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Clickable box that shows a subtle background on hover.
|
|
23
|
+
// Keeps hover state local to each label for minimal re-renders.
|
|
24
|
+
export function Hoverable({ children, onMouseDown, flexShrink = 0 }: HoverableProps): any {
|
|
25
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
26
|
+
const theme = useTheme()
|
|
27
|
+
return (
|
|
28
|
+
<box
|
|
29
|
+
flexDirection='row'
|
|
30
|
+
gap={1}
|
|
31
|
+
flexShrink={flexShrink}
|
|
32
|
+
backgroundColor={isHovered ? theme.backgroundElement : undefined}
|
|
33
|
+
onMouseMove={() => { setIsHovered(true) }}
|
|
34
|
+
onMouseOut={() => { setIsHovered(false) }}
|
|
35
|
+
onMouseDown={onMouseDown}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</box>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
16
42
|
interface FooterProps {
|
|
17
43
|
children?: ReactNode
|
|
18
44
|
paddingLeft?: number
|
|
@@ -117,6 +143,7 @@ function ToastInline({ toast }: { toast: ToastData }): any {
|
|
|
117
143
|
overflow='hidden'
|
|
118
144
|
height={1}
|
|
119
145
|
backgroundColor={toastBg}
|
|
146
|
+
onMouseDown={() => { toast.onHide() }}
|
|
120
147
|
>
|
|
121
148
|
{/* Title box */}
|
|
122
149
|
<box
|
|
@@ -165,7 +192,8 @@ function ToastInline({ toast }: { toast: ToastData }): any {
|
|
|
165
192
|
<box
|
|
166
193
|
flexShrink={0}
|
|
167
194
|
flexDirection='row'
|
|
168
|
-
onMouseDown={() => {
|
|
195
|
+
onMouseDown={(evt) => {
|
|
196
|
+
evt.stopPropagation()
|
|
169
197
|
toast.primaryAction?.onAction()
|
|
170
198
|
}}
|
|
171
199
|
>
|
|
@@ -186,7 +214,8 @@ function ToastInline({ toast }: { toast: ToastData }): any {
|
|
|
186
214
|
<box
|
|
187
215
|
flexShrink={0}
|
|
188
216
|
flexDirection='row'
|
|
189
|
-
onMouseDown={() => {
|
|
217
|
+
onMouseDown={(evt) => {
|
|
218
|
+
evt.stopPropagation()
|
|
190
219
|
toast.secondaryAction?.onAction()
|
|
191
220
|
}}
|
|
192
221
|
>
|
|
@@ -244,21 +273,22 @@ export function Footer({
|
|
|
244
273
|
{children}
|
|
245
274
|
</box>
|
|
246
275
|
{showPoweredBy && (
|
|
247
|
-
<
|
|
276
|
+
<Hoverable
|
|
277
|
+
onMouseDown={() => {
|
|
278
|
+
openInBrowser('https://termcast.app')
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
248
281
|
<text flexShrink={0} fg={theme.textMuted}>
|
|
249
282
|
powered by
|
|
250
283
|
</text>
|
|
251
284
|
<text
|
|
252
285
|
flexShrink={0}
|
|
253
|
-
onMouseDown={() => {
|
|
254
|
-
openInBrowser('https://termcast.app')
|
|
255
|
-
}}
|
|
256
286
|
fg={theme.textMuted}
|
|
257
287
|
attributes={TextAttributes.BOLD}
|
|
258
288
|
>
|
|
259
289
|
termcast.app
|
|
260
290
|
</text>
|
|
261
|
-
</
|
|
291
|
+
</Hoverable>
|
|
262
292
|
)}
|
|
263
293
|
</>
|
|
264
294
|
)}
|
|
@@ -59,19 +59,10 @@ export const Checkbox = (props: CheckboxProps): any => {
|
|
|
59
59
|
defaultValue={props.defaultValue || props.value || false}
|
|
60
60
|
render={({ field, fieldState, formState }) => {
|
|
61
61
|
return (
|
|
62
|
-
<box ref={elementRef} flexDirection='column'>
|
|
62
|
+
<box ref={elementRef} flexDirection='column' onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }); handleToggle() }}>
|
|
63
63
|
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
64
64
|
<TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
|
|
65
|
-
<box
|
|
66
|
-
onMouseDown={() => {
|
|
67
|
-
// Always focus the field when clicked
|
|
68
|
-
if (!isFocused) {
|
|
69
|
-
setFocusedField(props.id)
|
|
70
|
-
}
|
|
71
|
-
// Always toggle the value when clicked
|
|
72
|
-
handleToggle()
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
65
|
+
<box>
|
|
75
66
|
<LoadingText
|
|
76
67
|
isLoading={isFocused && focusContext.isLoading}
|
|
77
68
|
color={isFocused ? theme.primary : theme.text}
|
|
@@ -83,12 +74,6 @@ export const Checkbox = (props: CheckboxProps): any => {
|
|
|
83
74
|
<text
|
|
84
75
|
fg={isFocused ? theme.accent : theme.text}
|
|
85
76
|
selectable={false}
|
|
86
|
-
onMouseDown={() => {
|
|
87
|
-
if (!isFocused) {
|
|
88
|
-
setFocusedField(props.id)
|
|
89
|
-
}
|
|
90
|
-
handleToggle()
|
|
91
|
-
}}
|
|
92
77
|
>
|
|
93
78
|
{field.value ? '●' : '○'} {props.label}
|
|
94
79
|
</text>
|
|
@@ -55,12 +55,12 @@ const DatePickerComponent = (props: DatePickerProps): any => {
|
|
|
55
55
|
defaultValue={props.defaultValue || props.value || null}
|
|
56
56
|
render={({ field, fieldState, formState }) => {
|
|
57
57
|
return (
|
|
58
|
-
<box ref={elementRef} flexDirection='column'>
|
|
58
|
+
<box ref={elementRef} flexDirection='column' onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }) }}>
|
|
59
59
|
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
60
60
|
<TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
|
|
61
61
|
<box
|
|
62
62
|
onMouseDown={() => {
|
|
63
|
-
setFocusedField(props.id)
|
|
63
|
+
setFocusedField(props.id, { skipScroll: true })
|
|
64
64
|
}}
|
|
65
65
|
>
|
|
66
66
|
<LoadingText
|
|
@@ -39,7 +39,7 @@ export const Description = (props: DescriptionProps): any => {
|
|
|
39
39
|
flexDirection='column'
|
|
40
40
|
maxWidth={FORM_MAX_WIDTH}
|
|
41
41
|
onMouseDown={() => {
|
|
42
|
-
focusContext.setFocusedField(id)
|
|
42
|
+
focusContext.setFocusedField(id, { skipScroll: true })
|
|
43
43
|
}}
|
|
44
44
|
>
|
|
45
45
|
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
@@ -268,6 +268,11 @@ const DropdownContent = ({
|
|
|
268
268
|
const item = descendantsContext.committedMap[descendantId]
|
|
269
269
|
const title = item?.props?.title || value
|
|
270
270
|
|
|
271
|
+
// Sync focusedIndex so keyboard nav continues from clicked item
|
|
272
|
+
if (item && item.index !== -1) {
|
|
273
|
+
setFocusedIndex(item.index)
|
|
274
|
+
}
|
|
275
|
+
|
|
271
276
|
if (props.hasMultipleSelection) {
|
|
272
277
|
const currentValues = Array.isArray(field.value) ? [...field.value] : []
|
|
273
278
|
const index = currentValues.indexOf(value)
|
|
@@ -350,6 +355,20 @@ const DropdownContent = ({
|
|
|
350
355
|
}
|
|
351
356
|
}
|
|
352
357
|
|
|
358
|
+
if (itemCount === 0) {
|
|
359
|
+
if (evt.name === 'down') {
|
|
360
|
+
navigateToNext()
|
|
361
|
+
evt.stopPropagation()
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (evt.name === 'up') {
|
|
366
|
+
navigateToPrevious()
|
|
367
|
+
evt.stopPropagation()
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
353
372
|
// Type-ahead search
|
|
354
373
|
if (
|
|
355
374
|
evt.name.length === 1 &&
|
|
@@ -428,12 +447,12 @@ const DropdownContent = ({
|
|
|
428
447
|
return (
|
|
429
448
|
<FormDropdownDescendantsProvider value={descendantsContext}>
|
|
430
449
|
<FormDropdownContext.Provider value={contextValue}>
|
|
431
|
-
<box ref={elementRef} flexDirection='column'>
|
|
450
|
+
<box ref={elementRef} flexDirection='column' onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }) }}>
|
|
432
451
|
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
433
452
|
<TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
|
|
434
453
|
<box
|
|
435
454
|
onMouseDown={() => {
|
|
436
|
-
setFocusedField(props.id)
|
|
455
|
+
setFocusedField(props.id, { skipScroll: true })
|
|
437
456
|
}}
|
|
438
457
|
>
|
|
439
458
|
<LoadingText
|
|
@@ -448,7 +467,7 @@ const DropdownContent = ({
|
|
|
448
467
|
fg={selectedTitles.length > 0 ? theme.text : theme.textMuted}
|
|
449
468
|
selectable={false}
|
|
450
469
|
onMouseDown={() => {
|
|
451
|
-
setFocusedField(props.id)
|
|
470
|
+
setFocusedField(props.id, { skipScroll: true })
|
|
452
471
|
}}
|
|
453
472
|
>
|
|
454
473
|
{selectedTitles.length > 0
|
|
@@ -62,7 +62,7 @@ const FilePickerField = ({
|
|
|
62
62
|
formState: any
|
|
63
63
|
props: FilePickerProps
|
|
64
64
|
isFocused: boolean
|
|
65
|
-
setFocusedField: (id: string) => void
|
|
65
|
+
setFocusedField: (id: string, opts?: { skipScroll?: boolean }) => void
|
|
66
66
|
isFormLoading: boolean
|
|
67
67
|
}): any => {
|
|
68
68
|
const theme = useTheme()
|
|
@@ -86,6 +86,7 @@ const FilePickerField = ({
|
|
|
86
86
|
}, [])
|
|
87
87
|
|
|
88
88
|
const dialog = useDialog()
|
|
89
|
+
const { navigateToPrevious, navigateToNext } = useFormNavigationHelpers(props.id)
|
|
89
90
|
|
|
90
91
|
// Create store once for sharing state with dialog
|
|
91
92
|
const [store] = React.useState(() => createFileAutocompleteStore())
|
|
@@ -129,6 +130,18 @@ const FilePickerField = ({
|
|
|
129
130
|
if (!isFocused || !isInFocus) return
|
|
130
131
|
if (dialog.stack.length > 0) return // Let autocomplete handle keys
|
|
131
132
|
|
|
133
|
+
if (evt.name === 'up') {
|
|
134
|
+
navigateToPrevious()
|
|
135
|
+
evt.stopPropagation()
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (evt.name === 'down') {
|
|
140
|
+
navigateToNext()
|
|
141
|
+
evt.stopPropagation()
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
// Left arrow removes last selected file when input is empty
|
|
133
146
|
if (evt.name === 'left') {
|
|
134
147
|
const inputValue = inputRef.current?.plainText || ''
|
|
@@ -173,10 +186,10 @@ const FilePickerField = ({
|
|
|
173
186
|
<TitleIndicator isFocused={isFocused} isLoading={isFormLoading}>
|
|
174
187
|
<box
|
|
175
188
|
onMouseDown={() => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
setFocusedField(props.id, { skipScroll: true })
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
<LoadingText
|
|
180
193
|
isLoading={isFocused && isFormLoading}
|
|
181
194
|
color={isFocused ? theme.primary : theme.text}
|
|
182
195
|
>
|
|
@@ -197,7 +210,7 @@ const FilePickerField = ({
|
|
|
197
210
|
placeholder={props.placeholder || 'Enter file path...'}
|
|
198
211
|
focused={isFocused}
|
|
199
212
|
showCursor={dialog.stack.length === 0}
|
|
200
|
-
onMouseDown={() => setFocusedField(props.id)}
|
|
213
|
+
onMouseDown={() => setFocusedField(props.id, { skipScroll: true })}
|
|
201
214
|
onContentChange={() => {
|
|
202
215
|
const value = inputRef.current?.plainText || ''
|
|
203
216
|
store.setState({ filter: value })
|
|
@@ -209,9 +222,19 @@ const FilePickerField = ({
|
|
|
209
222
|
{selectedFiles.length > 0 && (
|
|
210
223
|
<box flexDirection='column' marginTop={1}>
|
|
211
224
|
<text fg={theme.textMuted}>Selected files:</text>
|
|
212
|
-
{selectedFiles.map((file: string,
|
|
213
|
-
<text
|
|
214
|
-
|
|
225
|
+
{selectedFiles.map((file: string, idx: number) => (
|
|
226
|
+
<text
|
|
227
|
+
key={idx}
|
|
228
|
+
fg={theme.text}
|
|
229
|
+
onMouseDown={() => {
|
|
230
|
+
const newFiles = selectedFiles.filter((_: string, i: number) => i !== idx)
|
|
231
|
+
field.onChange(newFiles)
|
|
232
|
+
if (props.onChange) {
|
|
233
|
+
props.onChange(newFiles)
|
|
234
|
+
}
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
• {file} ✕
|
|
215
238
|
</text>
|
|
216
239
|
))}
|
|
217
240
|
</box>
|
|
@@ -252,7 +275,7 @@ export const FilePicker = (props: FilePickerProps): any => {
|
|
|
252
275
|
defaultValue={props.defaultValue || props.value || []}
|
|
253
276
|
render={(renderProps) => {
|
|
254
277
|
return (
|
|
255
|
-
<box ref={elementRef} flexDirection='column'>
|
|
278
|
+
<box ref={elementRef} flexDirection='column' onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }) }}>
|
|
256
279
|
<FilePickerField
|
|
257
280
|
{...renderProps}
|
|
258
281
|
props={props}
|
|
@@ -15,7 +15,7 @@ import { useDialog } from 'termcast/src/internal/dialog'
|
|
|
15
15
|
import { Offscreen } from 'termcast/src/internal/offscreen'
|
|
16
16
|
import { useTheme } from 'termcast/src/theme'
|
|
17
17
|
import { useStore } from 'termcast/src/state'
|
|
18
|
-
import { Footer } from 'termcast/src/components/footer'
|
|
18
|
+
import { Footer, Hoverable } from 'termcast/src/components/footer'
|
|
19
19
|
import {
|
|
20
20
|
TextAttributes,
|
|
21
21
|
ScrollBoxRenderable,
|
|
@@ -77,7 +77,7 @@ export const useFormScrollContext = () => {
|
|
|
77
77
|
// Context for managing focused field and loading state
|
|
78
78
|
interface FocusContextValue {
|
|
79
79
|
focusedField: string | null
|
|
80
|
-
setFocusedField: (id: string | null) => void
|
|
80
|
+
setFocusedField: (id: string | null, opts?: { skipScroll?: boolean }) => void
|
|
81
81
|
isLoading: boolean
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -121,12 +121,16 @@ function FormFooter(): any {
|
|
|
121
121
|
</text>
|
|
122
122
|
<text flexShrink={0} fg={theme.textMuted}>navigate</text>
|
|
123
123
|
</box>
|
|
124
|
-
<
|
|
124
|
+
<Hoverable
|
|
125
|
+
onMouseDown={() => {
|
|
126
|
+
useStore.setState({ showActionsDialog: true })
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
125
129
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
126
130
|
^k
|
|
127
131
|
</text>
|
|
128
132
|
<text flexShrink={0} fg={theme.textMuted}>actions</text>
|
|
129
|
-
</
|
|
133
|
+
</Hoverable>
|
|
130
134
|
</box>
|
|
131
135
|
)
|
|
132
136
|
|
|
@@ -221,11 +225,11 @@ export const Form: FormType = ((props) => {
|
|
|
221
225
|
scrollBox.scrollTo(Math.max(0, targetScrollTop))
|
|
222
226
|
}
|
|
223
227
|
|
|
224
|
-
const setFocusedField = (id: string | null) => {
|
|
228
|
+
const setFocusedField = (id: string | null, opts?: { skipScroll?: boolean }) => {
|
|
225
229
|
flushSync(() => {
|
|
226
230
|
setFocusedFieldRaw(id)
|
|
227
231
|
})
|
|
228
|
-
if (id) {
|
|
232
|
+
if (id && !opts?.skipScroll) {
|
|
229
233
|
scrollToField(id)
|
|
230
234
|
}
|
|
231
235
|
}
|
|
@@ -40,12 +40,12 @@ export const PasswordField = (props: PasswordFieldProps): any => {
|
|
|
40
40
|
const displayValue = '*'.repeat(displayLength)
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
|
-
<box ref={elementRef} flexDirection="column">
|
|
43
|
+
<box ref={elementRef} flexDirection="column" onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }) }}>
|
|
44
44
|
<WithLeftBorder isFocused={isFocused} paddingBottom={1}>
|
|
45
45
|
<TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
|
|
46
46
|
<box
|
|
47
47
|
onMouseDown={() => {
|
|
48
|
-
setFocusedField(props.id)
|
|
48
|
+
setFocusedField(props.id, { skipScroll: true })
|
|
49
49
|
}}
|
|
50
50
|
>
|
|
51
51
|
<LoadingText
|
|
@@ -83,7 +83,7 @@ export const PasswordField = (props: PasswordFieldProps): any => {
|
|
|
83
83
|
placeholder={props.placeholder}
|
|
84
84
|
focused={isFocused}
|
|
85
85
|
onMouseDown={() => {
|
|
86
|
-
setFocusedField(props.id)
|
|
86
|
+
setFocusedField(props.id, { skipScroll: true })
|
|
87
87
|
}}
|
|
88
88
|
/>
|
|
89
89
|
{(fieldState.error || props.error || props.info) && <box height={1} />}
|