termcast 1.3.50 → 1.3.52
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 +1130 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.js +80 -0
- package/dist/cli.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -2
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.d.ts +4 -1
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +8 -5
- package/dist/components/actions.js.map +1 -1
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +21 -18
- 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 +7 -5
- 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/graph.d.ts.map +1 -1
- package/dist/components/graph.js +21 -25
- package/dist/components/graph.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 +424 -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 +91 -58
- 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 +1492 -0
- package/src/assets/default-app-icon.png +0 -0
- package/src/cli.tsx +105 -0
- package/src/compile.tsx +5 -2
- package/src/components/actions.tsx +9 -6
- package/src/components/detail.tsx +33 -23
- package/src/components/dropdown.tsx +3 -2
- package/src/components/footer.tsx +40 -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 +11 -7
- 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/graph.tsx +21 -24
- package/src/components/heatmap.tsx +602 -0
- package/src/components/list.tsx +147 -78
- 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 +7 -7
- package/src/examples/graph-row.vitest.tsx +45 -45
- package/src/examples/graph-styles.vitest.tsx +19 -19
- 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>',
|
package/src/compile.tsx
CHANGED
|
@@ -6,13 +6,16 @@ import { getCommandsWithFiles } from './package-json'
|
|
|
6
6
|
import { swiftLoaderPlugin } from './swift-loader'
|
|
7
7
|
|
|
8
8
|
// compile.tsx lives at termcast/src/compile.tsx, so __dirname is termcast/src/
|
|
9
|
+
// When running from dist/, __dirname is termcast/dist/ — always resolve to src/index.tsx
|
|
10
|
+
// so the plugin works both in dev and from the published package.
|
|
9
11
|
const termcastRoot = path.resolve(__dirname, '..')
|
|
12
|
+
const termcastEntry = path.join(termcastRoot, 'src', 'index.tsx')
|
|
10
13
|
|
|
11
14
|
const raycastAliasPlugin: BunPlugin = {
|
|
12
15
|
name: 'raycast-to-termcast',
|
|
13
16
|
setup(build) {
|
|
14
17
|
build.onResolve({ filter: /@raycast\/api/ }, () => ({
|
|
15
|
-
path:
|
|
18
|
+
path: termcastEntry,
|
|
16
19
|
}))
|
|
17
20
|
build.onResolve({ filter: /@raycast\/utils/ }, () => ({
|
|
18
21
|
path: require.resolve('@termcast/utils'),
|
|
@@ -20,7 +23,7 @@ const raycastAliasPlugin: BunPlugin = {
|
|
|
20
23
|
// termcast and termcast/* — resolve directly from the package source
|
|
21
24
|
build.onResolve({ filter: /^termcast/ }, (args) => ({
|
|
22
25
|
path: args.path === 'termcast'
|
|
23
|
-
?
|
|
26
|
+
? termcastEntry
|
|
24
27
|
: require.resolve(args.path, { paths: [termcastRoot] }),
|
|
25
28
|
}))
|
|
26
29
|
build.onResolve({ filter: /^react(\/|$)/ }, (args) => ({
|
|
@@ -700,12 +700,13 @@ function formatShortcut(
|
|
|
700
700
|
/**
|
|
701
701
|
* Check if a keyboard event matches an action shortcut.
|
|
702
702
|
* Handles modifier mapping:
|
|
703
|
-
* - 'cmd' maps to ctrl (
|
|
703
|
+
* - 'cmd' maps to ctrl, super, or hyper (ctrl in normal terminals,
|
|
704
|
+
* super/hyper in standalone apps where WezTerm forwards Cmd via kitty protocol)
|
|
704
705
|
* - 'alt'/'opt' checks evt.meta (opentui uses meta for alt on Linux/Windows)
|
|
705
706
|
* and evt.option (opentui uses option for alt on macOS)
|
|
706
707
|
*/
|
|
707
708
|
export function matchesShortcut(
|
|
708
|
-
evt: { name: string; ctrl?: boolean; alt?: boolean; shift?: boolean; meta?: boolean; option?: boolean },
|
|
709
|
+
evt: { name: string; ctrl?: boolean; alt?: boolean; shift?: boolean; meta?: boolean; option?: boolean; super?: boolean; hyper?: boolean },
|
|
709
710
|
shortcut: { modifiers?: KeyboardKeyModifier[]; key: KeyboardKeyEquivalent },
|
|
710
711
|
): boolean {
|
|
711
712
|
// Check key name matches (case-insensitive)
|
|
@@ -715,10 +716,12 @@ export function matchesShortcut(
|
|
|
715
716
|
|
|
716
717
|
const modifiers = shortcut.modifiers || []
|
|
717
718
|
|
|
718
|
-
// Map cmd to ctrl
|
|
719
|
-
|
|
719
|
+
// Map cmd to ctrl, super, or hyper. In normal terminals cmd arrives as ctrl.
|
|
720
|
+
// In standalone apps WezTerm forwards Cmd as super via kitty protocol.
|
|
721
|
+
const needsCmd = modifiers.some((m) =>
|
|
720
722
|
['cmd', 'ctrl', 'control'].includes(m.toLowerCase()),
|
|
721
723
|
)
|
|
724
|
+
const hasCmd = Boolean(evt.ctrl || evt.super || evt.hyper)
|
|
722
725
|
// alt/opt in shortcuts - opentui uses meta (Linux/Windows) or option (macOS) for alt key
|
|
723
726
|
const needsAlt = modifiers.some((m) =>
|
|
724
727
|
['alt', 'opt', 'option'].includes(m.toLowerCase()),
|
|
@@ -726,14 +729,14 @@ export function matchesShortcut(
|
|
|
726
729
|
const needsShift = modifiers.includes('shift')
|
|
727
730
|
|
|
728
731
|
// Check all required modifiers are pressed
|
|
729
|
-
if (
|
|
732
|
+
if (needsCmd && !hasCmd) return false
|
|
730
733
|
// For alt, check both meta and option (opentui platform differences)
|
|
731
734
|
const hasAlt = evt.alt || evt.meta || evt.option
|
|
732
735
|
if (needsAlt && !hasAlt) return false
|
|
733
736
|
if (needsShift && !evt.shift) return false
|
|
734
737
|
|
|
735
738
|
// Check no extra modifiers are pressed (excluding ones that match)
|
|
736
|
-
if (
|
|
739
|
+
if (hasCmd && !needsCmd) return false
|
|
737
740
|
if (hasAlt && !needsAlt) return false
|
|
738
741
|
if (evt.shift && !needsShift) return false
|
|
739
742
|
|
|
@@ -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
|
|
@@ -195,7 +204,7 @@ const Detail: DetailType = (props) => {
|
|
|
195
204
|
useKeyboard((evt) => {
|
|
196
205
|
if (!inFocus) return
|
|
197
206
|
|
|
198
|
-
if (evt.name === 'k' && evt.ctrl) {
|
|
207
|
+
if (evt.name === 'k' && (evt.ctrl || evt.super)) {
|
|
199
208
|
// Always open — built-in actions (Change Theme, etc.) are always available
|
|
200
209
|
useStore.setState({ showActionsDialog: true })
|
|
201
210
|
} else if (evt.name === 'return' && actions) {
|
|
@@ -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
|
|
@@ -125,6 +152,7 @@ function ToastInline({ toast }: { toast: ToastData }): any {
|
|
|
125
152
|
paddingRight={1}
|
|
126
153
|
overflow='hidden'
|
|
127
154
|
height={1}
|
|
155
|
+
onMouseDown={() => { toast.onHide() }}
|
|
128
156
|
>
|
|
129
157
|
<text flexShrink={0} fg={toastFg} wrapMode='none'>
|
|
130
158
|
{getIcon()}{' '}
|
|
@@ -147,6 +175,7 @@ function ToastInline({ toast }: { toast: ToastData }): any {
|
|
|
147
175
|
flexDirection='row'
|
|
148
176
|
overflow='hidden'
|
|
149
177
|
height={1}
|
|
178
|
+
onMouseDown={() => { toast.onHide() }}
|
|
150
179
|
>
|
|
151
180
|
<text fg={toastFg} flexShrink={1} wrapMode='none'>
|
|
152
181
|
{toast.message || ''}
|
|
@@ -160,12 +189,14 @@ function ToastInline({ toast }: { toast: ToastData }): any {
|
|
|
160
189
|
flexShrink={0}
|
|
161
190
|
overflow='hidden'
|
|
162
191
|
height={1}
|
|
192
|
+
onMouseDown={() => { toast.onHide() }}
|
|
163
193
|
>
|
|
164
194
|
{toast.primaryAction?.title && (
|
|
165
195
|
<box
|
|
166
196
|
flexShrink={0}
|
|
167
197
|
flexDirection='row'
|
|
168
|
-
onMouseDown={() => {
|
|
198
|
+
onMouseDown={(evt) => {
|
|
199
|
+
evt.stopPropagation()
|
|
169
200
|
toast.primaryAction?.onAction()
|
|
170
201
|
}}
|
|
171
202
|
>
|
|
@@ -186,7 +217,8 @@ function ToastInline({ toast }: { toast: ToastData }): any {
|
|
|
186
217
|
<box
|
|
187
218
|
flexShrink={0}
|
|
188
219
|
flexDirection='row'
|
|
189
|
-
onMouseDown={() => {
|
|
220
|
+
onMouseDown={(evt) => {
|
|
221
|
+
evt.stopPropagation()
|
|
190
222
|
toast.secondaryAction?.onAction()
|
|
191
223
|
}}
|
|
192
224
|
>
|
|
@@ -244,21 +276,22 @@ export function Footer({
|
|
|
244
276
|
{children}
|
|
245
277
|
</box>
|
|
246
278
|
{showPoweredBy && (
|
|
247
|
-
<
|
|
279
|
+
<Hoverable
|
|
280
|
+
onMouseDown={() => {
|
|
281
|
+
openInBrowser('https://termcast.app')
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
248
284
|
<text flexShrink={0} fg={theme.textMuted}>
|
|
249
285
|
powered by
|
|
250
286
|
</text>
|
|
251
287
|
<text
|
|
252
288
|
flexShrink={0}
|
|
253
|
-
onMouseDown={() => {
|
|
254
|
-
openInBrowser('https://termcast.app')
|
|
255
|
-
}}
|
|
256
289
|
fg={theme.textMuted}
|
|
257
290
|
attributes={TextAttributes.BOLD}
|
|
258
291
|
>
|
|
259
292
|
termcast.app
|
|
260
293
|
</text>
|
|
261
|
-
</
|
|
294
|
+
</Hoverable>
|
|
262
295
|
)}
|
|
263
296
|
</>
|
|
264
297
|
)}
|
|
@@ -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}
|