@nan0web/ui-cli 1.1.1 → 2.0.0
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/README.md +114 -207
- package/package.json +22 -12
- package/src/CLI.js +22 -30
- package/src/CLiMessage.js +2 -3
- package/src/Command.js +26 -24
- package/src/CommandError.js +3 -5
- package/src/CommandHelp.js +40 -36
- package/src/CommandMessage.js +56 -40
- package/src/CommandParser.js +27 -25
- package/src/InputAdapter.js +630 -90
- package/src/OutputAdapter.js +7 -8
- package/src/README.md.js +190 -316
- package/src/components/Alert.js +3 -6
- package/src/components/prompt/Autocomplete.js +12 -0
- package/src/components/prompt/Confirm.js +29 -0
- package/src/components/prompt/DateTime.js +26 -0
- package/src/components/prompt/Input.js +15 -0
- package/src/components/prompt/Mask.js +12 -0
- package/src/components/prompt/Multiselect.js +26 -0
- package/src/components/prompt/Next.js +8 -0
- package/src/components/prompt/Password.js +13 -0
- package/src/components/prompt/Pause.js +9 -0
- package/src/components/prompt/ProgressBar.js +16 -0
- package/src/components/prompt/Select.js +29 -0
- package/src/components/prompt/Slider.js +16 -0
- package/src/components/prompt/Spinner.js +29 -0
- package/src/components/prompt/Toggle.js +13 -0
- package/src/components/prompt/Tree.js +17 -0
- package/src/components/view/Alert.js +78 -0
- package/src/components/view/Badge.js +11 -0
- package/src/components/view/Nav.js +23 -0
- package/src/components/view/Table.js +12 -0
- package/src/components/view/Toast.js +9 -0
- package/src/core/Component.js +79 -0
- package/src/core/PropValidation.js +138 -0
- package/src/core/render.js +37 -0
- package/src/index.js +80 -41
- package/src/test/PlaygroundTest.js +37 -25
- package/src/test/index.js +2 -4
- package/src/ui/alert.js +58 -0
- package/src/ui/autocomplete.js +86 -0
- package/src/ui/badge.js +35 -0
- package/src/ui/confirm.js +49 -0
- package/src/ui/date-time.js +45 -0
- package/src/ui/form.js +120 -55
- package/src/ui/index.js +18 -4
- package/src/ui/input.js +79 -152
- package/src/ui/mask.js +132 -0
- package/src/ui/multiselect.js +59 -0
- package/src/ui/nav.js +74 -0
- package/src/ui/next.js +18 -13
- package/src/ui/progress.js +88 -0
- package/src/ui/select.js +49 -72
- package/src/ui/slider.js +154 -0
- package/src/ui/spinner.js +65 -0
- package/src/ui/table.js +163 -0
- package/src/ui/toast.js +34 -0
- package/src/ui/toggle.js +34 -0
- package/src/ui/tree.js +393 -0
- package/src/utils/parse.js +1 -1
- package/types/CLI.d.ts +5 -5
- package/types/CLiMessage.d.ts +1 -1
- package/types/Command.d.ts +2 -2
- package/types/CommandHelp.d.ts +3 -3
- package/types/CommandMessage.d.ts +8 -8
- package/types/CommandParser.d.ts +3 -3
- package/types/InputAdapter.d.ts +149 -15
- package/types/OutputAdapter.d.ts +1 -1
- package/types/README.md.d.ts +1 -1
- package/types/UiMessage.d.ts +31 -29
- package/types/components/prompt/Autocomplete.d.ts +6 -0
- package/types/components/prompt/Confirm.d.ts +6 -0
- package/types/components/prompt/DateTime.d.ts +6 -0
- package/types/components/prompt/Input.d.ts +6 -0
- package/types/components/prompt/Mask.d.ts +6 -0
- package/types/components/prompt/Multiselect.d.ts +6 -0
- package/types/components/prompt/Next.d.ts +6 -0
- package/types/components/prompt/Password.d.ts +6 -0
- package/types/components/prompt/Pause.d.ts +6 -0
- package/types/components/prompt/ProgressBar.d.ts +12 -0
- package/types/components/prompt/Select.d.ts +18 -0
- package/types/components/prompt/Slider.d.ts +6 -0
- package/types/components/prompt/Spinner.d.ts +21 -0
- package/types/components/prompt/Toggle.d.ts +6 -0
- package/types/components/prompt/Tree.d.ts +6 -0
- package/types/components/view/Alert.d.ts +21 -0
- package/types/components/view/Badge.d.ts +5 -0
- package/types/components/view/Nav.d.ts +15 -0
- package/types/components/view/Table.d.ts +10 -0
- package/types/components/view/Toast.d.ts +5 -0
- package/types/core/Component.d.ts +34 -0
- package/types/core/PropValidation.d.ts +48 -0
- package/types/core/render.d.ts +6 -0
- package/types/index.d.ts +47 -15
- package/types/test/PlaygroundTest.d.ts +12 -8
- package/types/test/index.d.ts +1 -1
- package/types/ui/alert.d.ts +14 -0
- package/types/ui/autocomplete.d.ts +20 -0
- package/types/ui/badge.d.ts +8 -0
- package/types/ui/confirm.d.ts +21 -0
- package/types/ui/date-time.d.ts +19 -0
- package/types/ui/form.d.ts +43 -12
- package/types/ui/index.d.ts +17 -2
- package/types/ui/input.d.ts +31 -74
- package/types/ui/mask.d.ts +29 -0
- package/types/ui/multiselect.d.ts +25 -0
- package/types/ui/nav.d.ts +27 -0
- package/types/ui/progress.d.ts +43 -0
- package/types/ui/select.d.ts +25 -64
- package/types/ui/slider.d.ts +23 -0
- package/types/ui/spinner.d.ts +28 -0
- package/types/ui/table.d.ts +28 -0
- package/types/ui/toast.d.ts +8 -0
- package/types/ui/toggle.d.ts +17 -0
- package/types/ui/tree.d.ts +48 -0
package/src/index.js
CHANGED
|
@@ -1,53 +1,92 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import Command from "./Command.js"
|
|
7
|
-
import CommandError from "./CommandError.js"
|
|
8
|
-
import CommandMessage from "./CommandMessage.js"
|
|
9
|
-
import CommandParser from "./CommandParser.js"
|
|
10
|
-
import CommandHelp from "./CommandHelp.js"
|
|
11
|
-
export { str2argv } from "./utils/parse.js"
|
|
1
|
+
import CLiInputAdapter from './InputAdapter.js'
|
|
2
|
+
import OutputAdapter from './OutputAdapter.js'
|
|
3
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
4
|
+
import CLI from './CLI.js'
|
|
5
|
+
import CommandParser from './CommandParser.js'
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from
|
|
7
|
+
// V2 Component Exports
|
|
8
|
+
import { render } from './core/render.js'
|
|
9
|
+
|
|
10
|
+
// Views
|
|
11
|
+
import { Alert } from './components/view/Alert.js'
|
|
12
|
+
import { Badge } from './components/view/Badge.js'
|
|
13
|
+
import { Table } from './components/view/Table.js'
|
|
14
|
+
import { Breadcrumbs, Tabs, Steps } from './components/view/Nav.js'
|
|
15
|
+
import { Toast } from './components/view/Toast.js'
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
// Prompts
|
|
18
|
+
import { Select } from './components/prompt/Select.js'
|
|
19
|
+
import { Input } from './components/prompt/Input.js'
|
|
20
|
+
import { Password } from './components/prompt/Password.js'
|
|
21
|
+
import { Confirm } from './components/prompt/Confirm.js'
|
|
22
|
+
import { Multiselect } from './components/prompt/Multiselect.js'
|
|
23
|
+
import { Mask } from './components/prompt/Mask.js'
|
|
24
|
+
import { Autocomplete } from './components/prompt/Autocomplete.js'
|
|
25
|
+
import { Slider } from './components/prompt/Slider.js'
|
|
26
|
+
import { Toggle } from './components/prompt/Toggle.js'
|
|
27
|
+
import { DateTime } from './components/prompt/DateTime.js'
|
|
28
|
+
import { Next } from './components/prompt/Next.js'
|
|
29
|
+
import { Pause } from './components/prompt/Pause.js'
|
|
30
|
+
import { Tree } from './components/prompt/Tree.js'
|
|
31
|
+
import { Spinner } from './components/prompt/Spinner.js'
|
|
32
|
+
import { ProgressBar } from './components/prompt/ProgressBar.js'
|
|
23
33
|
|
|
34
|
+
// Legacy utils still needed for internal logic or compat
|
|
35
|
+
export { createInput, ask, text } from './ui/input.js'
|
|
36
|
+
export { select } from './ui/select.js'
|
|
37
|
+
export { confirm } from './ui/confirm.js'
|
|
38
|
+
export { next } from './ui/next.js'
|
|
39
|
+
export { multiselect } from './ui/multiselect.js'
|
|
40
|
+
export { mask } from './ui/mask.js'
|
|
41
|
+
export { table } from './ui/table.js'
|
|
42
|
+
export { autocomplete } from './ui/autocomplete.js'
|
|
43
|
+
export { pause } from './ui/next.js'
|
|
44
|
+
export { alert } from './ui/alert.js'
|
|
45
|
+
export { badge } from './ui/badge.js'
|
|
46
|
+
export { toast } from './ui/toast.js'
|
|
47
|
+
export { spinner } from './ui/spinner.js'
|
|
48
|
+
export { progress } from './ui/progress.js'
|
|
49
|
+
export { breadcrumbs, tabs, steps } from './ui/nav.js'
|
|
50
|
+
export { tree } from './ui/tree.js'
|
|
51
|
+
export { datetime } from './ui/date-time.js'
|
|
52
|
+
export { toggle } from './ui/toggle.js'
|
|
53
|
+
export { slider } from './ui/slider.js'
|
|
54
|
+
|
|
55
|
+
// Public V2 API
|
|
24
56
|
export {
|
|
25
|
-
|
|
57
|
+
// Core
|
|
58
|
+
render,
|
|
26
59
|
CLiInputAdapter,
|
|
27
60
|
CancelError,
|
|
28
|
-
OutputAdapter,
|
|
29
61
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
62
|
+
// Components
|
|
63
|
+
Alert,
|
|
64
|
+
Badge,
|
|
65
|
+
Table,
|
|
66
|
+
Breadcrumbs,
|
|
67
|
+
Tabs,
|
|
68
|
+
Steps,
|
|
69
|
+
Toast,
|
|
70
|
+
Select,
|
|
71
|
+
Input,
|
|
72
|
+
Password,
|
|
73
|
+
Confirm,
|
|
74
|
+
Multiselect,
|
|
75
|
+
Mask,
|
|
76
|
+
Autocomplete,
|
|
77
|
+
Slider,
|
|
78
|
+
Toggle,
|
|
79
|
+
DateTime,
|
|
80
|
+
Next,
|
|
81
|
+
Pause,
|
|
82
|
+
Tree,
|
|
83
|
+
Spinner,
|
|
84
|
+
ProgressBar,
|
|
36
85
|
|
|
86
|
+
// Tools
|
|
87
|
+
CLI,
|
|
37
88
|
CommandParser,
|
|
38
|
-
|
|
89
|
+
OutputAdapter,
|
|
39
90
|
}
|
|
40
91
|
|
|
41
|
-
/* New public API */
|
|
42
|
-
export { generateForm } from "./ui/form.js"
|
|
43
|
-
|
|
44
|
-
export const renderers = new Map([
|
|
45
|
-
[
|
|
46
|
-
"UIProcess",
|
|
47
|
-
data => {
|
|
48
|
-
return `${data.title || "Process"}: ${data.status || "running"}`
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
])
|
|
52
|
-
|
|
53
92
|
export default CLiInputAdapter
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/* eslint-disable no-use-before-define */
|
|
11
|
-
import { spawn } from
|
|
12
|
-
import event, { EventContext } from
|
|
11
|
+
import { spawn } from 'node:child_process'
|
|
12
|
+
import event, { EventContext } from '@nan0web/event'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {object} PlaygroundTestConfig
|
|
16
16
|
* @property {NodeJS.ProcessEnv} env Environment variables for the child process.
|
|
17
|
-
* @property {{ includeDebugger?: boolean }} [config={}] Configuration options.
|
|
17
|
+
* @property {{ includeDebugger?: boolean, includeEmptyLines?: boolean, feedStdin?: boolean }} [config={}] Configuration options.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -33,7 +33,7 @@ export default class PlaygroundTest {
|
|
|
33
33
|
#bus
|
|
34
34
|
/**
|
|
35
35
|
* @param {NodeJS.ProcessEnv} env Environment variables for the child process.
|
|
36
|
-
* @param {{ includeDebugger?: boolean, includeEmptyLines?: boolean }} [config={}] Configuration options.
|
|
36
|
+
* @param {{ includeDebugger?: boolean, includeEmptyLines?: boolean, feedStdin?: boolean }} [config={}] Configuration options.
|
|
37
37
|
*/
|
|
38
38
|
constructor(env, config = {}) {
|
|
39
39
|
this.env = env
|
|
@@ -41,6 +41,7 @@ export default class PlaygroundTest {
|
|
|
41
41
|
/** @type {boolean} Include debugger lines in output (default: false). */
|
|
42
42
|
this.includeDebugger = config.includeDebugger ?? false
|
|
43
43
|
this.incldeEmptyLines = config.includeEmptyLines ?? false
|
|
44
|
+
this.feedStdin = config.feedStdin ?? true
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -66,18 +67,18 @@ export default class PlaygroundTest {
|
|
|
66
67
|
*/
|
|
67
68
|
filterDebugger(str) {
|
|
68
69
|
if (this.includeDebugger) return str
|
|
69
|
-
const words = [
|
|
70
|
+
const words = ['debugger', 'https://nodejs.org/en/docs/inspector']
|
|
70
71
|
return str
|
|
71
|
-
.split(
|
|
72
|
-
.filter(s => !words.some(w => s.toLowerCase().includes(w)))
|
|
73
|
-
.join(
|
|
72
|
+
.split('\n')
|
|
73
|
+
.filter((s) => !words.some((w) => s.toLowerCase().includes(w)))
|
|
74
|
+
.join('\n')
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
77
|
* Slice lines from stdout or stderr.
|
|
77
78
|
*/
|
|
78
79
|
slice(target, start, end) {
|
|
79
|
-
const txt =
|
|
80
|
-
return txt.split(
|
|
80
|
+
const txt = this.recentResult?.[target] ?? ''
|
|
81
|
+
return txt.split('\n').slice(start, end)
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
84
|
* Write the answer sequence to the child process **asynchronously**,
|
|
@@ -89,18 +90,20 @@ export default class PlaygroundTest {
|
|
|
89
90
|
const raw = this.env.PLAY_DEMO_SEQUENCE
|
|
90
91
|
if (!raw) return
|
|
91
92
|
|
|
92
|
-
const sequence = raw.split(
|
|
93
|
+
const sequence = raw.split(',').map((s) => s.trim())
|
|
94
|
+
// Allow empty strings to signify "Enter" (default)
|
|
93
95
|
if (sequence.length === 0) return
|
|
94
96
|
|
|
95
|
-
if (child.stdin) child.stdin.setDefaultEncoding(
|
|
97
|
+
if (child.stdin) child.stdin.setDefaultEncoding('utf-8')
|
|
96
98
|
|
|
97
99
|
const writeNext = (idx) => {
|
|
98
100
|
if (idx >= sequence.length) {
|
|
99
101
|
try {
|
|
100
102
|
child.stdin?.end()
|
|
101
|
-
} catch (_) {
|
|
103
|
+
} catch (_) {}
|
|
102
104
|
return
|
|
103
105
|
}
|
|
106
|
+
// Use 200ms delay for rock-solid stability across all platforms.
|
|
104
107
|
setTimeout(() => {
|
|
105
108
|
try {
|
|
106
109
|
if (!child.killed && child.stdin?.writable) {
|
|
@@ -108,7 +111,7 @@ export default class PlaygroundTest {
|
|
|
108
111
|
writeNext(idx + 1)
|
|
109
112
|
}
|
|
110
113
|
} catch (_) {
|
|
111
|
-
// Silently swallow EPIPE
|
|
114
|
+
// Silently swallow EPIPE
|
|
112
115
|
}
|
|
113
116
|
}, 200)
|
|
114
117
|
}
|
|
@@ -119,33 +122,42 @@ export default class PlaygroundTest {
|
|
|
119
122
|
*
|
|
120
123
|
* @param {string[]} [args=["play/main.js"]] Arguments passed to the node process.
|
|
121
124
|
*/
|
|
122
|
-
async run(args = [
|
|
123
|
-
|
|
125
|
+
async run(args = ['play/main.js']) {
|
|
126
|
+
// Optimization: if we have --demo or --lang in env equivalent, let's use them directly
|
|
127
|
+
// But for now, we follow the args passed.
|
|
128
|
+
const child = spawn(process.execPath, args, {
|
|
124
129
|
env: this.env,
|
|
125
|
-
stdio: [
|
|
130
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
131
|
})
|
|
127
132
|
|
|
128
|
-
this
|
|
133
|
+
if (this.feedStdin) {
|
|
134
|
+
this.#feedSequence(child)
|
|
135
|
+
}
|
|
129
136
|
|
|
130
|
-
let stdout =
|
|
137
|
+
let stdout = ''
|
|
131
138
|
for await (const chunk of child.stdout) {
|
|
132
139
|
stdout += chunk.toString()
|
|
133
|
-
this.emit(
|
|
140
|
+
await this.emit('stdout', { chunk })
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
let stderr =
|
|
143
|
+
let stderr = ''
|
|
137
144
|
for await (const chunk of child.stderr) {
|
|
138
145
|
const clean = this.filterDebugger(chunk.toString())
|
|
139
146
|
stderr += clean
|
|
140
|
-
this.emit(
|
|
147
|
+
this.emit('stderr', { chunk, clean })
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
const exitCode = await new Promise(resolve => child.on(
|
|
150
|
+
const exitCode = await new Promise((resolve) => child.on('close', resolve))
|
|
144
151
|
|
|
145
152
|
// Trim leading whitespace from every line – the test suite expects raw
|
|
146
153
|
// output without logger prefixes or indentation.
|
|
147
|
-
const normalize = txt =>
|
|
148
|
-
|
|
154
|
+
const normalize = (txt) =>
|
|
155
|
+
this.incldeEmptyLines
|
|
156
|
+
? txt
|
|
157
|
+
: txt
|
|
158
|
+
.split('\n')
|
|
159
|
+
.filter((row) => row.trim() !== '')
|
|
160
|
+
.join('\n')
|
|
149
161
|
|
|
150
162
|
this.recentResult = {
|
|
151
163
|
stdout: normalize(stdout),
|
package/src/test/index.js
CHANGED
package/src/ui/alert.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert component – displays a prominent message box.
|
|
3
|
+
*
|
|
4
|
+
* @module ui/alert
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Logger from '@nan0web/log'
|
|
8
|
+
import { beep } from './input.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders an alert box and optionally plays a sound.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} message - Message content.
|
|
14
|
+
* @param {'info'|'success'|'warning'|'error'} [variant='info']
|
|
15
|
+
* @param {Object} [options]
|
|
16
|
+
* @param {string} [options.title] - Optional title.
|
|
17
|
+
* @param {boolean} [options.sound=false] - Play beep sound.
|
|
18
|
+
* @returns {string} Styled message block.
|
|
19
|
+
*/
|
|
20
|
+
export function alert(message, variant = 'info', options = {}) {
|
|
21
|
+
const { title, sound = variant === 'error' || variant === 'warning' } = options
|
|
22
|
+
|
|
23
|
+
if (sound) beep()
|
|
24
|
+
|
|
25
|
+
const colors = {
|
|
26
|
+
info: Logger.CYAN,
|
|
27
|
+
success: Logger.GREEN,
|
|
28
|
+
warning: Logger.YELLOW,
|
|
29
|
+
error: Logger.RED,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const color = colors[variant] || Logger.WHITE
|
|
33
|
+
const icon =
|
|
34
|
+
{
|
|
35
|
+
info: 'ℹ',
|
|
36
|
+
success: '✔',
|
|
37
|
+
warning: '⚠',
|
|
38
|
+
error: '✖',
|
|
39
|
+
}[variant] || '•'
|
|
40
|
+
|
|
41
|
+
const lines = String(message || '').split('\n')
|
|
42
|
+
const maxLineLen = Math.max(title ? String(title).length + 4 : 0, ...lines.map((l) => l.length))
|
|
43
|
+
const len = Math.max(60, maxLineLen + 4)
|
|
44
|
+
const border = Logger.style('━'.repeat(len), { color })
|
|
45
|
+
|
|
46
|
+
let out = ''
|
|
47
|
+
out += `\n${border}\n`
|
|
48
|
+
if (title) {
|
|
49
|
+
out += Logger.style(` ${icon} ${title} `, { color }) + '\n'
|
|
50
|
+
out += Logger.style('─'.repeat(len) + '\n', { color: Logger.DIM })
|
|
51
|
+
}
|
|
52
|
+
lines.forEach((line) => {
|
|
53
|
+
out += ' ' + Logger.style(line.trim(), { color }) + '\n'
|
|
54
|
+
})
|
|
55
|
+
out += `${border}\n`
|
|
56
|
+
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autocomplete module - provides a searchable selection list.
|
|
3
|
+
*
|
|
4
|
+
* @module ui/autocomplete
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import prompts from 'prompts'
|
|
8
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
9
|
+
import Logger from '@nan0web/log'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Highlights the matching part of the text based on the query.
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @param {string} query
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function highlight(text, query) {
|
|
18
|
+
if (!query) return text
|
|
19
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
20
|
+
const regex = new RegExp(`(${escapedQuery})`, 'gi')
|
|
21
|
+
return text.replace(regex, `${Logger.MAGENTA}$1${Logger.RESET}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Renders a searchable selection list.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} input
|
|
28
|
+
* @param {string} [input.message] - Prompt message.
|
|
29
|
+
* @param {string} [input.title] - Alternative prompt title.
|
|
30
|
+
* @param {Array|Function} input.options - List of options or async function to fetch them.
|
|
31
|
+
* @param {number} [input.limit=10] - Max visible items.
|
|
32
|
+
* @returns {Promise<{index:number,value:any,cancelled:boolean}>}
|
|
33
|
+
*/
|
|
34
|
+
export async function autocomplete(input) {
|
|
35
|
+
const { message, title, options: initOptions, limit = 30 } = input
|
|
36
|
+
|
|
37
|
+
let choices = []
|
|
38
|
+
const fetch = async (query = '') => {
|
|
39
|
+
let currentOptions = typeof initOptions === 'function' ? await initOptions(query) : initOptions
|
|
40
|
+
|
|
41
|
+
// Normalize Map or Array
|
|
42
|
+
if (currentOptions instanceof Map) {
|
|
43
|
+
currentOptions = Array.from(currentOptions.entries()).map(([value, label]) => ({
|
|
44
|
+
label,
|
|
45
|
+
value,
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filtered = currentOptions.filter((el) => {
|
|
50
|
+
const label = typeof el === 'string' ? el : el.title || el.label || ''
|
|
51
|
+
return label.toLowerCase().includes(query.toLowerCase())
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return filtered.map((el) => {
|
|
55
|
+
const label = typeof el === 'string' ? el : el.title || el.label
|
|
56
|
+
const value = typeof el === 'string' ? el : el.value
|
|
57
|
+
return {
|
|
58
|
+
title: highlight(label, query),
|
|
59
|
+
value: value,
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
choices = await fetch('')
|
|
65
|
+
|
|
66
|
+
const response = await prompts(
|
|
67
|
+
{
|
|
68
|
+
type: 'autocomplete',
|
|
69
|
+
name: 'value',
|
|
70
|
+
message: message || title,
|
|
71
|
+
limit,
|
|
72
|
+
choices: choices,
|
|
73
|
+
suggest: (input, choices) => fetch(input),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
onCancel: () => {
|
|
77
|
+
throw new CancelError()
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const finalChoices = typeof initOptions === 'function' ? await fetch('') : choices
|
|
83
|
+
const index = finalChoices.findIndex((c) => c.value === response.value)
|
|
84
|
+
|
|
85
|
+
return { index, value: response.value, cancelled: false }
|
|
86
|
+
}
|
package/src/ui/badge.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Badge component – displays a small status label.
|
|
3
|
+
*
|
|
4
|
+
* @module ui/badge
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Logger from '@nan0web/log'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders a status badge.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} label - Text to display.
|
|
13
|
+
* @param {'info'|'success'|'warning'|'error'|'neutral'} [variant='neutral']
|
|
14
|
+
* @returns {string} Styled string.
|
|
15
|
+
*/
|
|
16
|
+
export function badge(label, variant = 'neutral') {
|
|
17
|
+
const colors = {
|
|
18
|
+
info: { bg: 'BLUE', fg: 'WHITE' }, // Use keys compatible with Logger lookup if possible, or Logger constants
|
|
19
|
+
success: { bg: 'GREEN', fg: 'BLACK' },
|
|
20
|
+
warning: { bg: 'YELLOW', fg: 'BLACK' },
|
|
21
|
+
error: { bg: 'RED', fg: 'WHITE' },
|
|
22
|
+
neutral: { bg: 'WHITE', fg: 'BLACK' },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Logger.style expects bgColor name or code.
|
|
26
|
+
// Logger.BG_BLUE is a code.
|
|
27
|
+
// But Logger.style implementation tries `Logger['BG_' + bgColor.toUpperCase()]`.
|
|
28
|
+
// So if I pass 'BLUE', it looks for BG_BLUE.
|
|
29
|
+
// If I pass Logger.BG_BLUE (the code), it uses it as raw.
|
|
30
|
+
|
|
31
|
+
// Let's use string keys 'BLUE', etc.
|
|
32
|
+
|
|
33
|
+
const style = colors[variant] || colors.neutral
|
|
34
|
+
return Logger.style(` ${label} `, { color: Logger[style.fg] || style.fg, bgColor: style.bg })
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirm module – simple Yes/No prompt.
|
|
3
|
+
* @module ui/confirm
|
|
4
|
+
*/
|
|
5
|
+
import prompts from 'prompts'
|
|
6
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
7
|
+
import { validateString, validateBoolean, validateFunction } from '../core/PropValidation.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} config
|
|
11
|
+
* @param {string} config.message
|
|
12
|
+
* @param {boolean} [config.initial=true] - Default value (true=Yes, false=No)
|
|
13
|
+
* @param {Function} [config.format] - Output value formatter
|
|
14
|
+
* @param {string} [config.active] - Label for "Yes" state
|
|
15
|
+
* @param {string} [config.inactive] - Label for "No" state
|
|
16
|
+
* @param {Function} [config.t] - Optional translation function.
|
|
17
|
+
* @returns {Promise<{value:boolean, cancelled:boolean}>}
|
|
18
|
+
*/
|
|
19
|
+
export async function confirm(config) {
|
|
20
|
+
// Validation
|
|
21
|
+
validateString(config.message, 'message', 'Confirm', true)
|
|
22
|
+
validateBoolean(config.initial, 'initial', 'Confirm')
|
|
23
|
+
validateFunction(config.format, 'format', 'Confirm')
|
|
24
|
+
validateString(config.active, 'active', 'Confirm')
|
|
25
|
+
validateString(config.inactive, 'inactive', 'Confirm')
|
|
26
|
+
validateFunction(config.t, 't', 'Confirm')
|
|
27
|
+
|
|
28
|
+
const { message, initial = true, format, t = (k) => k } = config
|
|
29
|
+
const active = config.active || t('yes')
|
|
30
|
+
const inactive = config.inactive || t('no')
|
|
31
|
+
|
|
32
|
+
const response = await prompts(
|
|
33
|
+
{
|
|
34
|
+
type: 'toggle',
|
|
35
|
+
name: 'value',
|
|
36
|
+
message,
|
|
37
|
+
initial,
|
|
38
|
+
format,
|
|
39
|
+
active,
|
|
40
|
+
inactive,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
onCancel: () => {
|
|
44
|
+
throw new CancelError()
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
return { value: response.value, cancelled: false }
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date/Time Picker module.
|
|
3
|
+
* @module ui/date-time
|
|
4
|
+
*/
|
|
5
|
+
import prompts from 'prompts'
|
|
6
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prompt user for a date or time.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} config
|
|
12
|
+
* @param {string} config.message
|
|
13
|
+
* @param {Date} [config.initial]
|
|
14
|
+
* @param {string} [config.mask] - Optional mask (e.g. 'YYYY-MM-DD HH:mm')
|
|
15
|
+
* @param {Function} [config.t] - Optional translation function.
|
|
16
|
+
* @returns {Promise<{value:Date, cancelled:boolean}>}
|
|
17
|
+
*/
|
|
18
|
+
export async function datetime(config) {
|
|
19
|
+
const { message, initial: rawInitial = new Date(), mask, t = (k) => k } = config
|
|
20
|
+
const initial = rawInitial instanceof Date ? rawInitial : new Date(rawInitial)
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const result = await prompts(
|
|
24
|
+
{
|
|
25
|
+
type: 'date',
|
|
26
|
+
name: 'value',
|
|
27
|
+
message: t(message),
|
|
28
|
+
initial,
|
|
29
|
+
mask: mask || undefined,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
onCancel: () => {
|
|
33
|
+
throw new CancelError()
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return { value: result.value, cancelled: false }
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof CancelError) {
|
|
41
|
+
return { value: initial, cancelled: true }
|
|
42
|
+
}
|
|
43
|
+
throw err
|
|
44
|
+
}
|
|
45
|
+
}
|