@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/ui/slider.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slider module – numeric range selection with a visual bar and Shift-jumps.
|
|
3
|
+
* @module ui/slider
|
|
4
|
+
*/
|
|
5
|
+
import NumberPrompt from 'prompts/lib/elements/number.js'
|
|
6
|
+
import prompts from 'prompts'
|
|
7
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
8
|
+
import { validateString, validateNumber, validateFunction } from '../core/PropValidation.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom SliderPrompt that adds visual bar and Shift+Up/Down jumps.
|
|
12
|
+
*/
|
|
13
|
+
class SliderPrompt extends NumberPrompt {
|
|
14
|
+
/** @param {any} opts */
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
super(opts)
|
|
17
|
+
this.jump = opts.jump || 10
|
|
18
|
+
/** @type {boolean} */
|
|
19
|
+
this.shift = false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
up() {
|
|
23
|
+
const self = /** @type {any} */ (this)
|
|
24
|
+
const max = self.max ?? 100
|
|
25
|
+
const step = self.increment ?? 1
|
|
26
|
+
self.value = Math.min(max, self.value + (this.shift ? this.jump : step))
|
|
27
|
+
this.render()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
down() {
|
|
31
|
+
const self = /** @type {any} */ (this)
|
|
32
|
+
const min = self.min ?? 0
|
|
33
|
+
const step = self.increment ?? 1
|
|
34
|
+
self.value = Math.max(min, self.value - (this.shift ? this.jump : step))
|
|
35
|
+
this.render()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} key
|
|
40
|
+
* @param {any} keypress
|
|
41
|
+
*/
|
|
42
|
+
_(key, keypress) {
|
|
43
|
+
this.shift = !!keypress?.shift
|
|
44
|
+
if (key === '+' || key === '=') {
|
|
45
|
+
this.up()
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (key === '-' || key === '_') {
|
|
49
|
+
this.down()
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (super._) super._(key, keypress)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render() {
|
|
56
|
+
const self = /** @type {any} */ (this)
|
|
57
|
+
if (self.closed || self.aborted) return
|
|
58
|
+
|
|
59
|
+
const min = self.min ?? 0
|
|
60
|
+
const max = self.max ?? 100
|
|
61
|
+
const width = 20
|
|
62
|
+
const range = max - min || 1
|
|
63
|
+
const percent = Math.max(0, Math.min(1, (self.value - min) / range))
|
|
64
|
+
const filled = Math.round(width * percent)
|
|
65
|
+
const bar = '━'.repeat(filled) + '─'.repeat(width - filled)
|
|
66
|
+
const label = self.msg || self.message || ''
|
|
67
|
+
const val = self.value
|
|
68
|
+
|
|
69
|
+
const out = self.out || process.stdout
|
|
70
|
+
// Clear prompt area and redraw
|
|
71
|
+
out.write('\r\x1B[K')
|
|
72
|
+
out.write(`${label} [${bar}] ${val}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {Object} config
|
|
78
|
+
* @param {string} config.message
|
|
79
|
+
* @param {number} [config.initial]
|
|
80
|
+
* @param {number} [config.min=0]
|
|
81
|
+
* @param {number} [config.max=100]
|
|
82
|
+
* @param {number} [config.step=1]
|
|
83
|
+
* @param {number} [config.jump]
|
|
84
|
+
* @param {Function} [config.t] - Optional translation function.
|
|
85
|
+
* @returns {Promise<{value:number, cancelled:boolean}>}
|
|
86
|
+
*/
|
|
87
|
+
export async function slider(config) {
|
|
88
|
+
validateString(config.message, 'message', 'Slider', true)
|
|
89
|
+
validateNumber(config.initial, 'initial', 'Slider')
|
|
90
|
+
validateNumber(config.min, 'min', 'Slider')
|
|
91
|
+
validateNumber(config.max, 'max', 'Slider')
|
|
92
|
+
validateNumber(config.step, 'step', 'Slider')
|
|
93
|
+
validateFunction(config.t, 't', 'Slider')
|
|
94
|
+
|
|
95
|
+
const { message, initial, min = 0, max = 100, step = 1, t = (k) => k } = config
|
|
96
|
+
const range = max - min
|
|
97
|
+
const jump = config.jump || Math.max(step, Math.round(range / 10))
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const isTest = process.env.NODE_TEST_CONTEXT || process.env.PLAY_DEMO_SEQUENCE
|
|
101
|
+
|
|
102
|
+
if (isTest) {
|
|
103
|
+
// In tests, fallback to a simple text-based number input to avoid complex TTY interactions
|
|
104
|
+
const res = await prompts(
|
|
105
|
+
{
|
|
106
|
+
type: 'text',
|
|
107
|
+
name: 'value',
|
|
108
|
+
message: t(message),
|
|
109
|
+
initial: String(initial ?? min),
|
|
110
|
+
validate: (v) => {
|
|
111
|
+
const n = Number(v)
|
|
112
|
+
return (!isNaN(n) && n >= min && n <= max) || `Enter number ${min}-${max}`
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
onCancel: () => {
|
|
117
|
+
throw new CancelError()
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
return { value: Number(res.value), cancelled: res.value === undefined }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// For interactive TTY, use the built-in number prompt (works reliably)
|
|
125
|
+
const res = await prompts(
|
|
126
|
+
{
|
|
127
|
+
type: 'number',
|
|
128
|
+
name: 'value',
|
|
129
|
+
message: t(message),
|
|
130
|
+
initial: initial ?? min,
|
|
131
|
+
min,
|
|
132
|
+
max,
|
|
133
|
+
increment: step,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
onCancel: () => {
|
|
137
|
+
throw new CancelError()
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
if (res.value === undefined) {
|
|
142
|
+
return { value: initial ?? min, cancelled: true }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { value: res.value, cancelled: false }
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const error = /** @type {any} */ (err)
|
|
148
|
+
if (error instanceof CancelError || error.message === 'canceled') {
|
|
149
|
+
// prompts throws 'canceled' sometimes
|
|
150
|
+
throw new CancelError()
|
|
151
|
+
}
|
|
152
|
+
throw error
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spinner module – loading indicators.
|
|
3
|
+
* @module ui/spinner
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Visual spinner for async operations.
|
|
8
|
+
*/
|
|
9
|
+
export class Spinner {
|
|
10
|
+
static FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} [message]
|
|
14
|
+
*/
|
|
15
|
+
constructor(message) {
|
|
16
|
+
this.message = message || ''
|
|
17
|
+
this.frameIndex = 0
|
|
18
|
+
this.interval = null
|
|
19
|
+
this.startTime = Date.now()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start() {
|
|
23
|
+
this.startTime = Date.now()
|
|
24
|
+
this.interval = setInterval(() => {
|
|
25
|
+
const frame = Spinner.FRAMES[this.frameIndex]
|
|
26
|
+
this.frameIndex = (this.frameIndex + 1) % Spinner.FRAMES.length
|
|
27
|
+
|
|
28
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000)
|
|
29
|
+
const timeStr = `${Math.floor(elapsed / 60)
|
|
30
|
+
.toString()
|
|
31
|
+
.padStart(2, '0')}:${(elapsed % 60).toString().padStart(2, '0')}`
|
|
32
|
+
|
|
33
|
+
process.stdout.write(`\r${frame} ${this.message} [${timeStr}]`)
|
|
34
|
+
}, 80)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
stop(status = '') {
|
|
38
|
+
if (this.interval) {
|
|
39
|
+
clearInterval(this.interval)
|
|
40
|
+
this.interval = null
|
|
41
|
+
process.stdout.write(`\r${status} ${this.message}\x1b[K\n`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
success(msg) {
|
|
46
|
+
if (msg) this.message = msg
|
|
47
|
+
this.stop('✔')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
error(msg) {
|
|
51
|
+
if (msg) this.message = msg
|
|
52
|
+
this.stop('✖')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Functional helper for spinner.
|
|
58
|
+
* @param {string} message
|
|
59
|
+
* @returns {Spinner}
|
|
60
|
+
*/
|
|
61
|
+
export function spinner(message) {
|
|
62
|
+
const s = new Spinner(message)
|
|
63
|
+
s.start()
|
|
64
|
+
return s
|
|
65
|
+
}
|
package/src/ui/table.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table module – provides interactive tables with filtering and selection.
|
|
3
|
+
*
|
|
4
|
+
* @module ui/table
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Logger from '@nan0web/log'
|
|
8
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
9
|
+
import { text } from './input.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Highlights matches in text.
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @param {string} query
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function highlight(text, query) {
|
|
18
|
+
if (!query) return String(text)
|
|
19
|
+
const str = String(text)
|
|
20
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
21
|
+
const regex = new RegExp(`(${escapedQuery})`, 'gi')
|
|
22
|
+
return str.replace(regex, `${Logger.MAGENTA}$1${Logger.RESET}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Renders an interactive table.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} input
|
|
29
|
+
* @param {Array<Object>} input.data - Data to display.
|
|
30
|
+
* @param {Array<string>} [input.columns] - Columns to include.
|
|
31
|
+
* @param {string} [input.title] - Table title.
|
|
32
|
+
* @param {boolean} [input.interactive=true] - Whether to allow filtering.
|
|
33
|
+
* @param {boolean} [input.instant=false] - Whether to use instant search (char-by-char).
|
|
34
|
+
* @param {(val:string)=>string} [input.t] - Translation function.
|
|
35
|
+
* @param {Logger} [input.logger] - Logger instance.
|
|
36
|
+
* @param {Function} [input.prompt] - Prompt function.
|
|
37
|
+
* @returns {Promise<{value:any, cancelled:boolean}>} Selected row (if interactive) or last state.
|
|
38
|
+
*/
|
|
39
|
+
export async function table(input) {
|
|
40
|
+
const {
|
|
41
|
+
data: rawData,
|
|
42
|
+
columns = [],
|
|
43
|
+
title,
|
|
44
|
+
interactive = true,
|
|
45
|
+
instant = false,
|
|
46
|
+
t = (k) => k,
|
|
47
|
+
} = input
|
|
48
|
+
|
|
49
|
+
const logger = input.logger || new Logger()
|
|
50
|
+
|
|
51
|
+
if (!interactive) {
|
|
52
|
+
if (title) logger.info(title)
|
|
53
|
+
logger.table(rawData, columns)
|
|
54
|
+
return { value: rawData, cancelled: false }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let query = ''
|
|
58
|
+
|
|
59
|
+
if (instant && process.stdin.isTTY) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const render = () => {
|
|
62
|
+
const filteredData = rawData.filter((row) => {
|
|
63
|
+
if (!query) return true
|
|
64
|
+
return Object.values(row).some((val) =>
|
|
65
|
+
String(val).toLowerCase().includes(query.toLowerCase())
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
const displayData = filteredData.map((row) => {
|
|
69
|
+
const highlightedRow = {}
|
|
70
|
+
for (const key in row) {
|
|
71
|
+
highlightedRow[key] = highlight(row[key], query)
|
|
72
|
+
}
|
|
73
|
+
return highlightedRow
|
|
74
|
+
})
|
|
75
|
+
const displayColumns = columns.map((c) => highlight(c, query))
|
|
76
|
+
|
|
77
|
+
logger.clear()
|
|
78
|
+
const infoMsg = title
|
|
79
|
+
? `${title} (${t('filter')}: "${query}")`
|
|
80
|
+
: `(${t('filter')}: "${query}")`
|
|
81
|
+
logger.info(infoMsg)
|
|
82
|
+
logger.table(displayData, displayColumns)
|
|
83
|
+
process.stdout.write('> ' + query)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render()
|
|
87
|
+
|
|
88
|
+
const onData = (chunk) => {
|
|
89
|
+
const char = chunk.toString()
|
|
90
|
+
if (char === '\r' || char === '\n') {
|
|
91
|
+
// Enter
|
|
92
|
+
cleanup()
|
|
93
|
+
resolve({ value: query, cancelled: false })
|
|
94
|
+
} else if (char === '\u0003' || char === '\u001b') {
|
|
95
|
+
// Ctrl+C or Esc
|
|
96
|
+
cleanup()
|
|
97
|
+
resolve({ value: query, cancelled: true })
|
|
98
|
+
} else if (char === '\u007f') {
|
|
99
|
+
// Backspace
|
|
100
|
+
query = query.slice(0, -1)
|
|
101
|
+
render()
|
|
102
|
+
} else {
|
|
103
|
+
// Only add printable characters
|
|
104
|
+
if (char.length === 1 && char.charCodeAt(0) >= 32) {
|
|
105
|
+
query += char
|
|
106
|
+
render()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cleanup = () => {
|
|
112
|
+
process.stdin.off('data', onData)
|
|
113
|
+
process.stdin.setRawMode(false)
|
|
114
|
+
process.stdin.pause()
|
|
115
|
+
process.stdout.write('\n')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
process.stdin.setRawMode(true)
|
|
119
|
+
process.stdin.resume()
|
|
120
|
+
process.stdin.on('data', onData)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Default loop (Enter based)
|
|
125
|
+
// eslint-disable-next-line no-constant-condition
|
|
126
|
+
while (true) {
|
|
127
|
+
const filteredData = rawData.filter((row) => {
|
|
128
|
+
if (!query) return true
|
|
129
|
+
return Object.values(row).some((val) =>
|
|
130
|
+
String(val).toLowerCase().includes(query.toLowerCase())
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const displayData = filteredData.map((row) => {
|
|
135
|
+
const highlightedRow = {}
|
|
136
|
+
for (const key in row) {
|
|
137
|
+
highlightedRow[key] = highlight(row[key], query)
|
|
138
|
+
}
|
|
139
|
+
return highlightedRow
|
|
140
|
+
})
|
|
141
|
+
const displayColumns = columns.map((c) => highlight(c, query))
|
|
142
|
+
|
|
143
|
+
logger.clear()
|
|
144
|
+
if (title) logger.info(`${title} (${t('filter')}: "${query || t('none')}")`)
|
|
145
|
+
logger.table(displayData, displayColumns)
|
|
146
|
+
|
|
147
|
+
const promptFn = input.prompt || text
|
|
148
|
+
const res = await promptFn({
|
|
149
|
+
message: t('table.filter_prompt'),
|
|
150
|
+
initial: query,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (res.cancelled || res.value === '::exit') {
|
|
154
|
+
return { value: filteredData, cancelled: res.cancelled }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (res.value === '::clear') {
|
|
158
|
+
query = ''
|
|
159
|
+
} else {
|
|
160
|
+
query = res.value
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/ui/toast.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast component – displays a brief notification.
|
|
3
|
+
*
|
|
4
|
+
* @module ui/toast
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Logger from '@nan0web/log'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders a toast message.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} message - Message content.
|
|
13
|
+
* @param {'info'|'success'|'warning'|'error'} [variant='info']
|
|
14
|
+
* @returns {string} Styled string.
|
|
15
|
+
*/
|
|
16
|
+
export function toast(message, variant = 'info') {
|
|
17
|
+
const icons = {
|
|
18
|
+
info: 'ℹ',
|
|
19
|
+
success: '✔',
|
|
20
|
+
warning: '⚠',
|
|
21
|
+
error: '✖',
|
|
22
|
+
}
|
|
23
|
+
const colors = {
|
|
24
|
+
info: Logger.BLUE,
|
|
25
|
+
success: Logger.GREEN,
|
|
26
|
+
warning: Logger.YELLOW,
|
|
27
|
+
error: Logger.RED,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const icon = icons[variant] || '•'
|
|
31
|
+
const color = colors[variant] || Logger.WHITE
|
|
32
|
+
|
|
33
|
+
return Logger.style(`${icon} ${message}`, { color })
|
|
34
|
+
}
|
package/src/ui/toggle.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggle module – a more visual boolean switch.
|
|
3
|
+
* @module ui/toggle
|
|
4
|
+
*/
|
|
5
|
+
import prompts from 'prompts'
|
|
6
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} config
|
|
10
|
+
* @param {string} config.message
|
|
11
|
+
* @param {boolean} [config.initial=false]
|
|
12
|
+
* @param {string} [config.active='yes']
|
|
13
|
+
* @param {string} [config.inactive='no']
|
|
14
|
+
* @returns {Promise<{value:boolean, cancelled:boolean}>}
|
|
15
|
+
*/
|
|
16
|
+
export async function toggle(config) {
|
|
17
|
+
const { message, initial = false, active = 'yes', inactive = 'no' } = config
|
|
18
|
+
const response = await prompts(
|
|
19
|
+
{
|
|
20
|
+
type: 'toggle',
|
|
21
|
+
name: 'value',
|
|
22
|
+
message,
|
|
23
|
+
initial,
|
|
24
|
+
active,
|
|
25
|
+
inactive,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
onCancel: () => {
|
|
29
|
+
throw new CancelError()
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
return { value: response.value, cancelled: false }
|
|
34
|
+
}
|