Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/input-isolation-prompt-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@kidd-cli/core': minor
---

Replace implicit input gating with explicit `PromptProps` (`focused`, `disabled`)

- Add `PromptProps` interface with `focused` and `disabled` fields, shared by all prompt components
- Remove `InputBlock` / `useInputBlock` context-based input gating
- Remove `useFocus` from all prompt components (was conflicting with Tabs key interception)
- Remove `@inkjs/ui` dependency (no longer needed)
- Rename `isDisabled` to `disabled` across all prompts and stories
- Stories viewer passes `focused` explicitly to story components in preview mode
- Remove `useInput` proxy wrapper — all components now import `useInput` directly from `ink`
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,6 @@ vite.config.ts.timestamp-*
.zpress/content/
.zpress/dist/
.zpress/cache/

# bun
*.bun-build
5 changes: 0 additions & 5 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
"zod": "catalog:"
},
"devDependencies": {
"@inkjs/ui": "^2.0.0",
"@types/node": "catalog:",
"@types/react": "^19.2.14",
"@types/yargs": "^17.0.35",
Expand All @@ -100,17 +99,13 @@
"vitest": "catalog:"
},
"peerDependencies": {
"@inkjs/ui": ">=2.0.0",
"ink": ">=5.0.0",
"jiti": ">=2.0.0",
"pino": ">=9.0.0",
"react": ">=18.0.0",
"vitest": ">=2.0.0"
},
"peerDependenciesMeta": {
"@inkjs/ui": {
"optional": true
},
"jiti": {
"optional": true
},
Expand Down
39 changes: 20 additions & 19 deletions packages/core/src/stories/viewer/components/field-control.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Option } from '@inkjs/ui'
import { ConfirmInput, MultiSelect, Select, TextInput } from '@inkjs/ui'
import { Box, Text } from 'ink'
import type { ReactElement } from 'react'
import { match } from 'ts-pattern'

import { Confirm } from '../../../ui/prompts/confirm.js'
import { MultiSelect } from '../../../ui/prompts/multi-select.js'
import { Select } from '../../../ui/prompts/select.js'
import { TextInput } from '../../../ui/prompts/text-input.js'
import type { PromptOption } from '../../../ui/prompts/types.js'
import type { FieldControlKind } from '../../types.js'

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -44,35 +47,31 @@ export function FieldControl({
.with('text', () => (
<TextInput
defaultValue={valueToString(value)}
isDisabled={!isFocused}
focused={isFocused}
onSubmit={(submitted) => onChange(submitted)}
/>
))
.with('number', () => (
<TextInput
defaultValue={valueToString(value)}
isDisabled={!isFocused}
focused={isFocused}
onSubmit={(submitted) => onChange(parseNumericValue(submitted))}
/>
))
.with('boolean', () => (
<Box>
<ConfirmInput
isDisabled={!isFocused}
defaultChoice={match(value)
.with(true, () => 'confirm' as const)
.otherwise(() => 'cancel' as const)}
onConfirm={() => onChange(true)}
onCancel={() => onChange(false)}
/>
<Text dimColor> (current: {String(value)})</Text>
</Box>
<Confirm
defaultValue={match(value)
.with(true, () => true)
.otherwise(() => false)}
focused={isFocused}
onSubmit={(submitted) => onChange(submitted)}
/>
))
.with('select', () => {
const selectOptions = buildSelectOptions(options)
return (
<Select
isDisabled={!isFocused}
focused={isFocused}
options={selectOptions}
onChange={(selected) => onChange(selected)}
/>
Expand All @@ -84,7 +83,7 @@ export function FieldControl({
return (
<Box flexDirection="column">
<MultiSelect
isDisabled={!isFocused}
focused={isFocused}
options={selectOptions}
defaultValue={defaultSelected}
onSubmit={(selectedValues) => onChange(selectedValues)}
Expand All @@ -96,7 +95,7 @@ export function FieldControl({
.with('json', () => (
<TextInput
defaultValue={stringifyJsonValue(value)}
isDisabled={!isFocused}
focused={isFocused}
onSubmit={(submitted) => onChange(parseJsonValue(submitted))}
/>
))
Expand Down Expand Up @@ -160,7 +159,9 @@ function parseNumericValue(input: string): number {
* @param options - The raw string options.
* @returns An array of label/value option objects.
*/
function buildSelectOptions(options: readonly string[] | undefined): Option[] {
function buildSelectOptions(
options: readonly string[] | undefined
): readonly PromptOption<string>[] {
if (options === undefined) {
return []
}
Expand Down
31 changes: 24 additions & 7 deletions packages/core/src/stories/viewer/components/help-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,29 @@ import type { ReactElement } from 'react'

const BROWSE_SHORTCUTS = [
{ key: 'Up/Down', description: 'Navigate story tree' },
{ key: 'Enter', description: 'Select story / expand-collapse group' },
{ key: 'i', description: 'Enter interactive mode' },
{ key: 'Enter', description: 'Select story' },
{ key: 'b', description: 'Toggle sidebar' },
{ key: 'r', description: 'Reset props to defaults' },
{ key: '?', description: 'Toggle help' },
{ key: 'q', description: 'Quit' },
] as const

const EDIT_SHORTCUTS = [
{ key: 'Tab', description: 'Cycle to next prop' },
{ key: 'Left/Right', description: 'Switch between prop fields' },
{ key: 'Esc', description: 'Back to story browser' },
{ key: 'b', description: 'Toggle sidebar' },
const PREVIEW_SHORTCUTS = [
{ key: 'Enter', description: 'Edit props' },
{ key: 'i', description: 'Enter interactive mode' },
{ key: 'r', description: 'Reset props to defaults' },
{ key: 'b', description: 'Toggle sidebar' },
{ key: 'Esc', description: 'Back to browse' },
{ key: '?', description: 'Toggle help' },
{ key: 'q', description: 'Quit' },
] as const

const EDIT_SHORTCUTS = [
{ key: 'Tab', description: 'Cycle to next prop' },
{ key: 'Left/Right', description: 'Switch between prop fields' },
{ key: 'Esc', description: 'Back to preview' },
] as const

const INTERACTIVE_SHORTCUTS = [{ key: 'Esc Esc', description: 'Exit interactive mode' }] as const

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -72,6 +77,18 @@ export function HelpOverlay({ onClose }: HelpOverlayProps): ReactElement {
</Box>
))}
<Text> </Text>
<Text bold color="blue">
Preview Mode
</Text>
{PREVIEW_SHORTCUTS.map((shortcut) => (
<Box key={shortcut.key} gap={2}>
<Box width={12}>
<Text color="cyan">{shortcut.key}</Text>
</Box>
<Text>{shortcut.description}</Text>
</Box>
))}
<Text> </Text>
<Text bold color="yellow">
Edit Mode
</Text>
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/stories/viewer/components/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface PreviewProps {
readonly errors: readonly FieldError[]
readonly onPropsChange: (name: string, value: unknown) => void
readonly isFocused: boolean
readonly editable?: boolean
readonly borderless?: boolean
readonly interactive?: boolean
}
Expand Down Expand Up @@ -75,6 +76,7 @@ export function Preview({
errors,
onPropsChange,
isFocused,
editable = false,
borderless = false,
interactive = false,
}: PreviewProps): ReactElement {
Expand Down Expand Up @@ -150,7 +152,7 @@ export function Preview({
<Box ref={contentRef} flexDirection="column" flexGrow={1}>
<ScrollArea height={Math.max(1, componentAreaHeight)}>
<ErrorBoundary key={context.displayName}>
<DecoratedComponent {...currentProps} />
<DecoratedComponent {...currentProps} focused={false} />
</ErrorBoundary>
</ScrollArea>
<Box height={propsAreaHeight} overflow="hidden" flexDirection="column">
Expand All @@ -159,7 +161,7 @@ export function Preview({
values={currentProps}
errors={errors}
onChange={onPropsChange}
isFocused={isFocused}
isFocused={editable}
/>
</Box>
</Box>
Expand Down
41 changes: 32 additions & 9 deletions packages/core/src/stories/viewer/components/status-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ export function StatusBar({ mode, hasSelection, isReloading }: StatusBarProps):
.with({ isReloading: false, mode: 'browse' }, () => (
<BrowseHints hasSelection={hasSelection} />
))
.with({ isReloading: false, mode: 'preview' }, () => <PreviewHints />)
.with({ isReloading: false, mode: 'edit' }, () => <EditHints />)
.with({ isReloading: false, mode: 'interactive' }, () => <InteractiveHints />)
.exhaustive()}
<Spacer />
{match(mode)
.with('interactive', () => null)
.with('browse', () => <QuitHint />)
.with('edit', () => <QuitHint />)
.exhaustive()}
.with('preview', () => <QuitHint />)
.otherwise(() => null)}
</Box>
)
}
Expand All @@ -74,12 +74,14 @@ function ModeIndicator({ mode }: { readonly mode: ViewerMode }): ReactElement {
bold
color={match(mode)
.with('browse', () => 'cyan' as const)
.with('preview', () => 'blue' as const)
.with('edit', () => 'yellow' as const)
.with('interactive', () => 'green' as const)
.exhaustive()}
>
{match(mode)
.with('browse', () => '● Browse')
.with('preview', () => '● Preview')
.with('edit', () => '● Edit')
.with('interactive', () => '● Interactive')
.exhaustive()}
Expand Down Expand Up @@ -123,29 +125,50 @@ function BrowseHints({ hasSelection }: { readonly hasSelection: boolean }): Reac
}

/**
* Render keyboard hints for edit mode.
* Render keyboard hints for preview mode.
*
* @private
* @returns A rendered hints element.
*/
function EditHints(): ReactElement {
function PreviewHints(): ReactElement {
return (
<Box>
<Text dimColor>tab/←→</Text>
<Text>: prop</Text>
<Text dimColor>enter</Text>
<Text>: edit props</Text>
<Text> </Text>
<Text dimColor>esc</Text>
<Text>: back</Text>
<Text dimColor>i</Text>
<Text>: interactive</Text>
<Text> </Text>
<Text dimColor>r</Text>
<Text>: reset</Text>
<Text> </Text>
<Text dimColor>esc</Text>
<Text>: back</Text>
<Text> </Text>
<Text dimColor>?</Text>
<Text>: help</Text>
</Box>
)
}

/**
* Render keyboard hints for edit mode.
*
* @private
* @returns A rendered hints element.
*/
function EditHints(): ReactElement {
return (
<Box>
<Text dimColor>tab/←→</Text>
<Text>: prop</Text>
<Text> </Text>
<Text dimColor>esc</Text>
<Text>: back</Text>
</Box>
)
}

/**
* Render keyboard hints for interactive mode.
*
Expand Down
29 changes: 0 additions & 29 deletions packages/core/src/stories/viewer/hooks/use-double-escape.ts

This file was deleted.

Loading
Loading