agent-facets 0.3.0 → 0.3.3
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/bin/facet +1 -1
- package/package.json +16 -37
- package/{scripts/postinstall.mjs → postinstall.mjs} +1 -1
- package/.package.json.bak +0 -45
- package/.turbo/turbo-build.log +0 -3
- package/CHANGELOG.md +0 -95
- package/bunfig.toml +0 -2
- package/dist/facet +0 -0
- package/src/__tests__/cli.test.ts +0 -195
- package/src/__tests__/create-build.test.ts +0 -227
- package/src/__tests__/edit-integration.test.ts +0 -171
- package/src/__tests__/launcher.test.ts +0 -106
- package/src/__tests__/postinstall.test.ts +0 -196
- package/src/__tests__/resolve-dir.test.ts +0 -95
- package/src/commands/build.ts +0 -58
- package/src/commands/create/index.ts +0 -76
- package/src/commands/create/types.ts +0 -9
- package/src/commands/create/wizard.tsx +0 -75
- package/src/commands/create-scaffold.ts +0 -184
- package/src/commands/edit/index.ts +0 -144
- package/src/commands/edit/wizard.tsx +0 -74
- package/src/commands/resolve-dir.ts +0 -98
- package/src/commands.ts +0 -40
- package/src/help.ts +0 -43
- package/src/index.ts +0 -10
- package/src/run.ts +0 -82
- package/src/suggest.ts +0 -35
- package/src/tui/components/asset-description.tsx +0 -17
- package/src/tui/components/asset-field-picker.tsx +0 -78
- package/src/tui/components/asset-inline-input.tsx +0 -91
- package/src/tui/components/asset-item.tsx +0 -44
- package/src/tui/components/asset-section.tsx +0 -191
- package/src/tui/components/button.tsx +0 -92
- package/src/tui/components/editable-field.tsx +0 -172
- package/src/tui/components/exit-toast.tsx +0 -20
- package/src/tui/components/reconciliation-item.tsx +0 -129
- package/src/tui/components/stage-row.tsx +0 -45
- package/src/tui/components/version-selector.tsx +0 -79
- package/src/tui/context/focus-mode-context.ts +0 -36
- package/src/tui/context/focus-order-context.ts +0 -68
- package/src/tui/context/form-state-context.ts +0 -260
- package/src/tui/editor.ts +0 -40
- package/src/tui/gradient.ts +0 -1
- package/src/tui/hooks/use-exit-keys.ts +0 -75
- package/src/tui/hooks/use-navigation-keys.ts +0 -34
- package/src/tui/layouts/wizard-layout.tsx +0 -41
- package/src/tui/theme.ts +0 -1
- package/src/tui/views/build/build-view.tsx +0 -152
- package/src/tui/views/create/confirm-view.tsx +0 -74
- package/src/tui/views/create/create-view.tsx +0 -158
- package/src/tui/views/create/wizard.tsx +0 -97
- package/src/tui/views/edit/edit-confirm-view.tsx +0 -93
- package/src/tui/views/edit/edit-types.ts +0 -34
- package/src/tui/views/edit/edit-view.tsx +0 -140
- package/src/tui/views/edit/manifest-to-form.ts +0 -38
- package/src/tui/views/edit/reconciliation-view.tsx +0 -170
- package/src/tui/views/edit/use-edit-session.ts +0 -125
- package/src/tui/views/edit/wizard.tsx +0 -129
- package/src/version.ts +0 -3
- package/tsconfig.json +0 -4
package/src/help.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Command } from './commands.ts'
|
|
2
|
-
import { version } from './version.ts'
|
|
3
|
-
|
|
4
|
-
export function printGlobalHelp(commands: Record<string, Command>): void {
|
|
5
|
-
const entries = Object.values(commands)
|
|
6
|
-
const maxNameLength = Math.max(...entries.map((c) => c.name.length))
|
|
7
|
-
|
|
8
|
-
const lines = [
|
|
9
|
-
`facet v${version}`,
|
|
10
|
-
'',
|
|
11
|
-
'Usage: facet <command> [options]',
|
|
12
|
-
'',
|
|
13
|
-
'Commands:',
|
|
14
|
-
...entries.map((c) => ` ${c.name.padEnd(maxNameLength + 2)}${c.description}`),
|
|
15
|
-
'',
|
|
16
|
-
'Options:',
|
|
17
|
-
' --help Show help',
|
|
18
|
-
' --version Show version',
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
console.log(lines.join('\n'))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function printCommandHelp(command: Command): void {
|
|
25
|
-
const usage = command.usage ? ` ${command.usage}` : ''
|
|
26
|
-
|
|
27
|
-
const lines = [`Usage: facet ${command.name}${usage} [options]`, '', ` ${command.description}`, '', 'Options:']
|
|
28
|
-
|
|
29
|
-
if (command.flags) {
|
|
30
|
-
const flagEntries = Object.entries(command.flags)
|
|
31
|
-
const maxFlagLength = Math.max(...flagEntries.map(([name]) => `--${name}`.length), '--help'.length)
|
|
32
|
-
|
|
33
|
-
for (const [name, def] of flagEntries) {
|
|
34
|
-
lines.push(` ${`--${name}`.padEnd(maxFlagLength + 4)}${def.description}`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
lines.push(` ${'--help'.padEnd(maxFlagLength + 4)}Show help`)
|
|
38
|
-
} else {
|
|
39
|
-
lines.push(' --help Show help')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log(lines.join('\n'))
|
|
43
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { commands } from './commands.ts'
|
|
2
|
-
import { run } from './run.ts'
|
|
3
|
-
|
|
4
|
-
try {
|
|
5
|
-
const code = await run(process.argv.slice(2), commands)
|
|
6
|
-
process.exit(code)
|
|
7
|
-
} catch (error) {
|
|
8
|
-
console.error(error instanceof Error ? error.message : 'An unexpected error occurred.')
|
|
9
|
-
process.exit(2)
|
|
10
|
-
}
|
package/src/run.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { parse } from '@bomb.sh/args'
|
|
2
|
-
import type { Command } from './commands.ts'
|
|
3
|
-
import { printCommandHelp, printGlobalHelp } from './help.ts'
|
|
4
|
-
import { findClosestCommand } from './suggest.ts'
|
|
5
|
-
import { version } from './version.ts'
|
|
6
|
-
|
|
7
|
-
export async function run(argv: string[], commands: Record<string, Command>): Promise<number> {
|
|
8
|
-
const args = parse(argv, {
|
|
9
|
-
boolean: ['help', 'version'],
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
if (args.version) {
|
|
13
|
-
console.log(version)
|
|
14
|
-
return 0
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const commandName = String(args._[0] ?? '')
|
|
18
|
-
|
|
19
|
-
// No command given — show global help
|
|
20
|
-
if (!commandName) {
|
|
21
|
-
printGlobalHelp(commands)
|
|
22
|
-
return 0
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Explicit `help` command: `facets help` or `facets help build`
|
|
26
|
-
if (commandName === 'help') {
|
|
27
|
-
const subCommandName = String(args._[1] ?? '')
|
|
28
|
-
const subCommand = subCommandName ? commands[subCommandName] : undefined
|
|
29
|
-
if (subCommand) {
|
|
30
|
-
printCommandHelp(subCommand)
|
|
31
|
-
} else {
|
|
32
|
-
printGlobalHelp(commands)
|
|
33
|
-
}
|
|
34
|
-
return 0
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const command = commands[commandName]
|
|
38
|
-
|
|
39
|
-
if (!command) {
|
|
40
|
-
const suggestion = findClosestCommand(commandName, Object.keys(commands))
|
|
41
|
-
const message = suggestion
|
|
42
|
-
? `Unknown command "${commandName}". Did you mean "${suggestion}"?`
|
|
43
|
-
: `Unknown command "${commandName}".`
|
|
44
|
-
console.error(message)
|
|
45
|
-
return 1
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Per-command help: `facets build --help`
|
|
49
|
-
if (args.help) {
|
|
50
|
-
printCommandHelp(command)
|
|
51
|
-
return 0
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Build per-command flag parsing config
|
|
55
|
-
const booleanFlags: string[] = []
|
|
56
|
-
const stringFlags: string[] = []
|
|
57
|
-
|
|
58
|
-
if (command.flags) {
|
|
59
|
-
for (const [name, def] of Object.entries(command.flags)) {
|
|
60
|
-
if (def.type === 'boolean') booleanFlags.push(name)
|
|
61
|
-
else if (def.type === 'string') stringFlags.push(name)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Parse with per-command config
|
|
66
|
-
const parsed = parse(argv.slice(1), {
|
|
67
|
-
boolean: booleanFlags,
|
|
68
|
-
string: stringFlags,
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// Build positional args and flags
|
|
72
|
-
const positionalArgs = parsed._.map(String)
|
|
73
|
-
const flags: Record<string, unknown> = {}
|
|
74
|
-
|
|
75
|
-
for (const name of [...booleanFlags, ...stringFlags]) {
|
|
76
|
-
if (parsed[name] !== undefined) {
|
|
77
|
-
flags[name] = parsed[name]
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return command.run(positionalArgs, flags)
|
|
82
|
-
}
|
package/src/suggest.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
function levenshtein(a: string, b: string): number {
|
|
2
|
-
const m = a.length
|
|
3
|
-
const n = b.length
|
|
4
|
-
const row: number[] = []
|
|
5
|
-
|
|
6
|
-
for (let j = 0; j <= n; j++) row.push(j)
|
|
7
|
-
|
|
8
|
-
for (let i = 1; i <= m; i++) {
|
|
9
|
-
let prev = row[0] ?? 0
|
|
10
|
-
row[0] = i
|
|
11
|
-
for (let j = 1; j <= n; j++) {
|
|
12
|
-
const current = row[j] ?? 0
|
|
13
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
14
|
-
row[j] = Math.min(current + 1, (row[j - 1] ?? 0) + 1, prev + cost)
|
|
15
|
-
prev = current
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return row[n] ?? 0
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function findClosestCommand(input: string, commandNames: string[]): string | undefined {
|
|
23
|
-
let best: string | undefined
|
|
24
|
-
let bestDistance = Infinity
|
|
25
|
-
|
|
26
|
-
for (const name of commandNames) {
|
|
27
|
-
const distance = levenshtein(input, name)
|
|
28
|
-
if (distance < bestDistance) {
|
|
29
|
-
bestDistance = distance
|
|
30
|
-
best = name
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return bestDistance <= 3 ? best : undefined
|
|
35
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
|
|
3
|
-
export function truncateDescription(text: string, maxLen = 50): string {
|
|
4
|
-
const lines = text.split(/\r?\n/)
|
|
5
|
-
const firstLine = lines[0] ?? text
|
|
6
|
-
const hasMore = lines.length > 1
|
|
7
|
-
if (firstLine.length <= maxLen) return hasMore ? `${firstLine}...` : firstLine
|
|
8
|
-
return `${firstLine.slice(0, maxLen)}...`
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function AssetDescription({ description }: { description: string }) {
|
|
12
|
-
return (
|
|
13
|
-
<Box marginLeft={4}>
|
|
14
|
-
<Text dimColor>{truncateDescription(description)}</Text>
|
|
15
|
-
</Box>
|
|
16
|
-
)
|
|
17
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
3
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
4
|
-
import { THEME } from '../theme.ts'
|
|
5
|
-
|
|
6
|
-
export type AssetField = 'name' | 'description'
|
|
7
|
-
|
|
8
|
-
export function AssetFieldPicker({
|
|
9
|
-
name,
|
|
10
|
-
description,
|
|
11
|
-
initialField,
|
|
12
|
-
onChoose,
|
|
13
|
-
onCancel,
|
|
14
|
-
}: {
|
|
15
|
-
name: string
|
|
16
|
-
description: string
|
|
17
|
-
initialField?: AssetField
|
|
18
|
-
onChoose: (field: AssetField) => void
|
|
19
|
-
onCancel: () => void
|
|
20
|
-
}) {
|
|
21
|
-
const { setMode } = useFocusMode()
|
|
22
|
-
const [field, setField] = useState<AssetField>(initialField ?? 'name')
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
setMode('field-revision')
|
|
26
|
-
return () => setMode('form-navigation')
|
|
27
|
-
}, [setMode])
|
|
28
|
-
|
|
29
|
-
useInput((_input, key) => {
|
|
30
|
-
if (key.upArrow) setField('name')
|
|
31
|
-
if (key.downArrow) setField('description')
|
|
32
|
-
if (key.escape) onCancel()
|
|
33
|
-
if (key.return) onChoose(field)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<Box flexDirection="column">
|
|
38
|
-
<Box gap={1} marginLeft={2}>
|
|
39
|
-
{field === 'name' ? (
|
|
40
|
-
<>
|
|
41
|
-
<Text color={THEME.primary} bold>
|
|
42
|
-
▸
|
|
43
|
-
</Text>
|
|
44
|
-
<Text color={THEME.primary}>{name}</Text>
|
|
45
|
-
<Text color={THEME.hint}>
|
|
46
|
-
<Text color={THEME.keyword}>↑↓</Text> select · <Text color={THEME.keyword}>Enter</Text> edit ·{' '}
|
|
47
|
-
<Text color={THEME.keyword}>Esc</Text> back
|
|
48
|
-
</Text>
|
|
49
|
-
</>
|
|
50
|
-
) : (
|
|
51
|
-
<>
|
|
52
|
-
<Text color={THEME.success}>•</Text>
|
|
53
|
-
<Text>{name}</Text>
|
|
54
|
-
</>
|
|
55
|
-
)}
|
|
56
|
-
</Box>
|
|
57
|
-
<Box gap={1} marginLeft={2}>
|
|
58
|
-
{field === 'description' ? (
|
|
59
|
-
<>
|
|
60
|
-
<Text color={THEME.primary} bold>
|
|
61
|
-
▸
|
|
62
|
-
</Text>
|
|
63
|
-
<Text color={THEME.primary}>{description}</Text>
|
|
64
|
-
<Text color={THEME.hint}>
|
|
65
|
-
<Text color={THEME.keyword}>↑↓</Text> select · <Text color={THEME.keyword}>Enter</Text> edit ·{' '}
|
|
66
|
-
<Text color={THEME.keyword}>Esc</Text> back
|
|
67
|
-
</Text>
|
|
68
|
-
</>
|
|
69
|
-
) : (
|
|
70
|
-
<>
|
|
71
|
-
<Text> </Text>
|
|
72
|
-
<Text dimColor>{description}</Text>
|
|
73
|
-
</>
|
|
74
|
-
)}
|
|
75
|
-
</Box>
|
|
76
|
-
</Box>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import TextInput from 'ink-text-input'
|
|
3
|
-
import { THEME } from '../theme.ts'
|
|
4
|
-
|
|
5
|
-
export function AssetInlineInput({
|
|
6
|
-
value,
|
|
7
|
-
placeholder,
|
|
8
|
-
error,
|
|
9
|
-
isFocused,
|
|
10
|
-
onChange,
|
|
11
|
-
validate,
|
|
12
|
-
onError,
|
|
13
|
-
onSubmit,
|
|
14
|
-
onCancel,
|
|
15
|
-
onDownArrow,
|
|
16
|
-
}: {
|
|
17
|
-
id: string
|
|
18
|
-
value: string
|
|
19
|
-
placeholder?: string
|
|
20
|
-
error: string
|
|
21
|
-
isFocused: boolean
|
|
22
|
-
onChange: (value: string) => void
|
|
23
|
-
validate?: (value: string) => string | undefined
|
|
24
|
-
onError: (error: string) => void
|
|
25
|
-
onSubmit: (name: string) => void
|
|
26
|
-
onCancel: () => void
|
|
27
|
-
onDownArrow?: () => void
|
|
28
|
-
}) {
|
|
29
|
-
useInput(
|
|
30
|
-
(_input, key) => {
|
|
31
|
-
if (!isFocused) return
|
|
32
|
-
if (key.return) {
|
|
33
|
-
const name = value || placeholder || ''
|
|
34
|
-
if (!name) {
|
|
35
|
-
onCancel()
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
if (validate) {
|
|
39
|
-
const err = validate(name)
|
|
40
|
-
if (err) {
|
|
41
|
-
onError(err)
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
onSubmit(name)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
if (key.escape) {
|
|
49
|
-
onCancel()
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
if (key.downArrow && onDownArrow) {
|
|
53
|
-
onDownArrow()
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
if (key.tab && !value && placeholder) {
|
|
57
|
-
onChange(placeholder)
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
{ isActive: isFocused },
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<Box marginLeft={2} gap={1}>
|
|
66
|
-
<Text color={THEME.tertiary}>{'>'}</Text>
|
|
67
|
-
<TextInput value={value} onChange={onChange} placeholder={placeholder} focus={isFocused} />
|
|
68
|
-
{error ? (
|
|
69
|
-
<Text color={THEME.warning}>· {error}</Text>
|
|
70
|
-
) : value ? (
|
|
71
|
-
<Text color={THEME.hint}>
|
|
72
|
-
· <Text color={THEME.keyword}>Enter</Text> save ·{' '}
|
|
73
|
-
{onDownArrow && (
|
|
74
|
-
<>
|
|
75
|
-
<Text color={THEME.keyword}>↓</Text> description ·{' '}
|
|
76
|
-
</>
|
|
77
|
-
)}
|
|
78
|
-
<Text color={THEME.keyword}>Esc</Text> revert
|
|
79
|
-
</Text>
|
|
80
|
-
) : placeholder ? (
|
|
81
|
-
<Text color={THEME.hint}>
|
|
82
|
-
· <Text color={THEME.keyword}>Tab</Text> to fill · <Text color={THEME.keyword}>Escape</Text> to cancel
|
|
83
|
-
</Text>
|
|
84
|
-
) : (
|
|
85
|
-
<Text color={THEME.hint}>
|
|
86
|
-
· <Text color={THEME.keyword}>Enter</Text> to add · <Text color={THEME.keyword}>Escape</Text> to cancel
|
|
87
|
-
</Text>
|
|
88
|
-
)}
|
|
89
|
-
</Box>
|
|
90
|
-
)
|
|
91
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import { THEME } from '../theme.ts'
|
|
3
|
-
|
|
4
|
-
export function AssetItem({
|
|
5
|
-
name,
|
|
6
|
-
isFocused,
|
|
7
|
-
onEdit,
|
|
8
|
-
onRemove,
|
|
9
|
-
}: {
|
|
10
|
-
id: string
|
|
11
|
-
name: string
|
|
12
|
-
isFocused: boolean
|
|
13
|
-
onEdit: () => void
|
|
14
|
-
onRemove: () => void
|
|
15
|
-
}) {
|
|
16
|
-
useInput(
|
|
17
|
-
(_input, key) => {
|
|
18
|
-
if (key.return) onEdit()
|
|
19
|
-
if (key.delete || key.backspace) onRemove()
|
|
20
|
-
},
|
|
21
|
-
{ isActive: isFocused },
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<Box gap={1} marginLeft={2}>
|
|
26
|
-
{isFocused ? (
|
|
27
|
-
<>
|
|
28
|
-
<Text color={THEME.primary} bold>
|
|
29
|
-
▸
|
|
30
|
-
</Text>
|
|
31
|
-
<Text color={THEME.primary}>{name}</Text>
|
|
32
|
-
<Text color={THEME.hint}>
|
|
33
|
-
<Text color={THEME.keyword}>Enter</Text> edit name · <Text color={THEME.keyword}>Del</Text> remove
|
|
34
|
-
</Text>
|
|
35
|
-
</>
|
|
36
|
-
) : (
|
|
37
|
-
<>
|
|
38
|
-
<Text color={THEME.success}>•</Text>
|
|
39
|
-
<Text>{name}</Text>
|
|
40
|
-
</>
|
|
41
|
-
)}
|
|
42
|
-
</Box>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
import { useState } from 'react'
|
|
3
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
4
|
-
import { useFocusOrder } from '../context/focus-order-context.ts'
|
|
5
|
-
import type { AssetSectionKey } from '../context/form-state-context.ts'
|
|
6
|
-
import { useFormState } from '../context/form-state-context.ts'
|
|
7
|
-
import { AssetDescription, truncateDescription } from './asset-description.tsx'
|
|
8
|
-
import type { AssetField } from './asset-field-picker.tsx'
|
|
9
|
-
import { AssetFieldPicker } from './asset-field-picker.tsx'
|
|
10
|
-
import { AssetInlineInput } from './asset-inline-input.tsx'
|
|
11
|
-
import { AssetItem } from './asset-item.tsx'
|
|
12
|
-
import { Button } from './button.tsx'
|
|
13
|
-
|
|
14
|
-
export function AssetSection({
|
|
15
|
-
section,
|
|
16
|
-
label,
|
|
17
|
-
defaultName,
|
|
18
|
-
dimmed,
|
|
19
|
-
validate,
|
|
20
|
-
onEditDescription,
|
|
21
|
-
}: {
|
|
22
|
-
section: AssetSectionKey
|
|
23
|
-
label: string
|
|
24
|
-
defaultName?: string
|
|
25
|
-
dimmed?: boolean
|
|
26
|
-
validate?: (value: string) => string | undefined
|
|
27
|
-
onEditDescription?: (section: AssetSectionKey, name: string) => void
|
|
28
|
-
}) {
|
|
29
|
-
const { form, addAsset, removeAsset, renameAsset, setAssetAdding, setAssetEditing } = useFormState()
|
|
30
|
-
const { items, descriptions, editing, adding } = form.assets[section]
|
|
31
|
-
const { setMode } = useFocusMode()
|
|
32
|
-
const { focusedId, focus } = useFocusOrder()
|
|
33
|
-
const [inputValue, setInputValue] = useState('')
|
|
34
|
-
const [error, setError] = useState('')
|
|
35
|
-
const [selectedItem, setSelectedItem] = useState<string | null>(null)
|
|
36
|
-
|
|
37
|
-
const startAdding = () => {
|
|
38
|
-
setAssetAdding(section, true)
|
|
39
|
-
setInputValue('')
|
|
40
|
-
setError('')
|
|
41
|
-
setMode('field-revision')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const startEditing = (name: string) => {
|
|
45
|
-
setAssetEditing(section, name)
|
|
46
|
-
setInputValue(name)
|
|
47
|
-
setError('')
|
|
48
|
-
setMode('field-revision')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const closeInput = (focusTarget?: string | false) => {
|
|
52
|
-
setAssetAdding(section, false)
|
|
53
|
-
setAssetEditing(section, undefined)
|
|
54
|
-
setInputValue('')
|
|
55
|
-
setError('')
|
|
56
|
-
if (focusTarget !== false) {
|
|
57
|
-
setMode('form-navigation')
|
|
58
|
-
focus(focusTarget ?? `add-${section}`)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const handleFieldChoice = (name: string, field: AssetField) => {
|
|
63
|
-
setSelectedItem(null)
|
|
64
|
-
if (field === 'name') {
|
|
65
|
-
startEditing(name)
|
|
66
|
-
} else {
|
|
67
|
-
onEditDescription?.(section, name)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const handleRemove = (name: string) => {
|
|
72
|
-
const index = items.indexOf(name)
|
|
73
|
-
removeAsset(section, name)
|
|
74
|
-
|
|
75
|
-
if (index < items.length - 1) {
|
|
76
|
-
focus(`item-${section}-${index}`)
|
|
77
|
-
} else if (index > 0) {
|
|
78
|
-
focus(`item-${section}-${index - 1}`)
|
|
79
|
-
} else {
|
|
80
|
-
focus(`add-${section}`)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<Box flexDirection="column" gap={0}>
|
|
86
|
-
<Box gap={1}>
|
|
87
|
-
<Text bold dimColor={dimmed}>
|
|
88
|
-
{label}
|
|
89
|
-
</Text>
|
|
90
|
-
{items.length === 0 && !adding && <Text dimColor>(none)</Text>}
|
|
91
|
-
</Box>
|
|
92
|
-
|
|
93
|
-
{items.map((item, i) => {
|
|
94
|
-
const itemId = `item-${section}-${i}`
|
|
95
|
-
const isFocusedItem = focusedId === itemId
|
|
96
|
-
const description = descriptions[item] ?? `A ${item} ${section}`
|
|
97
|
-
|
|
98
|
-
// Field picker (entered via ↓ during name editing)
|
|
99
|
-
if (selectedItem === item) {
|
|
100
|
-
return (
|
|
101
|
-
<AssetFieldPicker
|
|
102
|
-
key={itemId}
|
|
103
|
-
name={item}
|
|
104
|
-
description={truncateDescription(description)}
|
|
105
|
-
initialField="description"
|
|
106
|
-
onChoose={(field) => handleFieldChoice(item, field)}
|
|
107
|
-
onCancel={() => {
|
|
108
|
-
setSelectedItem(null)
|
|
109
|
-
setMode('form-navigation')
|
|
110
|
-
focus(itemId)
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Inline name editing
|
|
117
|
-
if (editing === item) {
|
|
118
|
-
return (
|
|
119
|
-
<Box key={itemId} flexDirection="column">
|
|
120
|
-
<AssetInlineInput
|
|
121
|
-
id={itemId}
|
|
122
|
-
value={inputValue}
|
|
123
|
-
placeholder={item}
|
|
124
|
-
error={error}
|
|
125
|
-
isFocused={isFocusedItem}
|
|
126
|
-
onChange={setInputValue}
|
|
127
|
-
validate={validate}
|
|
128
|
-
onError={setError}
|
|
129
|
-
onSubmit={(newName) => {
|
|
130
|
-
renameAsset(section, item, newName)
|
|
131
|
-
closeInput(itemId)
|
|
132
|
-
}}
|
|
133
|
-
onCancel={() => closeInput(itemId)}
|
|
134
|
-
onDownArrow={() => {
|
|
135
|
-
closeInput(false)
|
|
136
|
-
setSelectedItem(item)
|
|
137
|
-
}}
|
|
138
|
-
/>
|
|
139
|
-
<AssetDescription description={description} />
|
|
140
|
-
</Box>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Normal display (level 1)
|
|
145
|
-
return (
|
|
146
|
-
<Box key={itemId} flexDirection="column">
|
|
147
|
-
<AssetItem
|
|
148
|
-
id={itemId}
|
|
149
|
-
name={item}
|
|
150
|
-
isFocused={isFocusedItem}
|
|
151
|
-
onEdit={() => startEditing(item)}
|
|
152
|
-
onRemove={() => handleRemove(item)}
|
|
153
|
-
/>
|
|
154
|
-
<AssetDescription description={description} />
|
|
155
|
-
</Box>
|
|
156
|
-
)
|
|
157
|
-
})}
|
|
158
|
-
|
|
159
|
-
{adding ? (
|
|
160
|
-
<AssetInlineInput
|
|
161
|
-
id={`add-${section}`}
|
|
162
|
-
value={inputValue}
|
|
163
|
-
placeholder={defaultName}
|
|
164
|
-
error={error}
|
|
165
|
-
isFocused={focusedId === `add-${section}`}
|
|
166
|
-
onChange={setInputValue}
|
|
167
|
-
validate={validate}
|
|
168
|
-
onError={setError}
|
|
169
|
-
onSubmit={(name) => {
|
|
170
|
-
addAsset(section, name)
|
|
171
|
-
closeInput()
|
|
172
|
-
}}
|
|
173
|
-
onCancel={closeInput}
|
|
174
|
-
/>
|
|
175
|
-
) : (
|
|
176
|
-
<Box marginLeft={2}>
|
|
177
|
-
<Button
|
|
178
|
-
id={`add-${section}`}
|
|
179
|
-
label="+ Add"
|
|
180
|
-
hint={
|
|
181
|
-
<Text dimColor>
|
|
182
|
-
<Text>Enter</Text> to add
|
|
183
|
-
</Text>
|
|
184
|
-
}
|
|
185
|
-
onPress={startAdding}
|
|
186
|
-
/>
|
|
187
|
-
</Box>
|
|
188
|
-
)}
|
|
189
|
-
</Box>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import Gradient from 'ink-gradient'
|
|
3
|
-
import type { ReactNode } from 'react'
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
5
|
-
import { useFocusOrder } from '../context/focus-order-context.ts'
|
|
6
|
-
import { GRADIENT_STOPS, getAnimatedGradient } from '../gradient.ts'
|
|
7
|
-
import { THEME } from '../theme.ts'
|
|
8
|
-
|
|
9
|
-
const ANIMATION_INTERVAL_MS = 75
|
|
10
|
-
|
|
11
|
-
export function Button({
|
|
12
|
-
id,
|
|
13
|
-
label,
|
|
14
|
-
hint,
|
|
15
|
-
onPress,
|
|
16
|
-
disabled,
|
|
17
|
-
color,
|
|
18
|
-
gradient: showGradient,
|
|
19
|
-
animateGradient,
|
|
20
|
-
}: {
|
|
21
|
-
id: string
|
|
22
|
-
label: string
|
|
23
|
-
hint?: ReactNode
|
|
24
|
-
onPress: () => void
|
|
25
|
-
disabled?: boolean
|
|
26
|
-
color?: string
|
|
27
|
-
autoFocus?: boolean
|
|
28
|
-
gradient?: boolean
|
|
29
|
-
animateGradient?: boolean
|
|
30
|
-
}) {
|
|
31
|
-
const { focusedId } = useFocusOrder()
|
|
32
|
-
const isFocused = focusedId === id && !disabled
|
|
33
|
-
const [offset, setOffset] = useState(0)
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!animateGradient) return
|
|
37
|
-
const interval = setInterval(() => {
|
|
38
|
-
setOffset((prev) => (prev + 1) % GRADIENT_STOPS.length)
|
|
39
|
-
}, ANIMATION_INTERVAL_MS)
|
|
40
|
-
return () => clearInterval(interval)
|
|
41
|
-
}, [animateGradient])
|
|
42
|
-
|
|
43
|
-
useInput(
|
|
44
|
-
(_input, key) => {
|
|
45
|
-
if (key.return) {
|
|
46
|
-
onPress()
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{ isActive: isFocused },
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
const prefix = isFocused ? '▸ ' : ' '
|
|
53
|
-
|
|
54
|
-
const focusHint = isFocused && hint ? <Text> {hint}</Text> : null
|
|
55
|
-
|
|
56
|
-
if (disabled) {
|
|
57
|
-
return (
|
|
58
|
-
<Box gap={0}>
|
|
59
|
-
<Text color="gray" dimColor>
|
|
60
|
-
{prefix}
|
|
61
|
-
{label}
|
|
62
|
-
</Text>
|
|
63
|
-
</Box>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (showGradient) {
|
|
68
|
-
const colors = animateGradient ? getAnimatedGradient(offset) : [...THEME.gradient]
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<Box gap={0}>
|
|
72
|
-
<Gradient colors={colors}>
|
|
73
|
-
<Text bold>
|
|
74
|
-
{prefix}
|
|
75
|
-
{label}
|
|
76
|
-
</Text>
|
|
77
|
-
</Gradient>
|
|
78
|
-
{focusHint}
|
|
79
|
-
</Box>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Box gap={0}>
|
|
85
|
-
<Text color={isFocused ? (color ?? THEME.primary) : undefined} bold={isFocused}>
|
|
86
|
-
{prefix}
|
|
87
|
-
{label}
|
|
88
|
-
</Text>
|
|
89
|
-
{focusHint}
|
|
90
|
-
</Box>
|
|
91
|
-
)
|
|
92
|
-
}
|