@nan0web/ui-cli 1.1.1 → 2.1.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 +153 -203
- package/bin/cli.js +11 -0
- package/bin/nan0cli.js +86 -0
- package/package.json +27 -13
- 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 +241 -312
- 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 +85 -40
- 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/components/Alert.js
CHANGED
|
@@ -10,12 +10,9 @@
|
|
|
10
10
|
* @throws {Error} If variant maps to undefined console method.
|
|
11
11
|
*/
|
|
12
12
|
export default function (input = {}) {
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} = input
|
|
17
|
-
const fn = variant === "success" ? "info" : variant
|
|
18
|
-
if (typeof this.console[fn] !== "function") {
|
|
13
|
+
const { variant = 'info', content = '' } = input
|
|
14
|
+
const fn = variant === 'success' ? 'info' : variant
|
|
15
|
+
if (typeof this.console[fn] !== 'function') {
|
|
19
16
|
throw new Error(`Undefined variant: ${variant}`)
|
|
20
17
|
}
|
|
21
18
|
this.console[fn](content)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { autocomplete as baseAutocomplete } from '../../ui/autocomplete.js'
|
|
3
|
+
|
|
4
|
+
export function Autocomplete(props) {
|
|
5
|
+
return createPrompt('Autocomplete', props, async (p) => {
|
|
6
|
+
return await baseAutocomplete({
|
|
7
|
+
message: p.message || p.title,
|
|
8
|
+
options: p.options,
|
|
9
|
+
limit: p.limit,
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { confirm as baseConfirm } from '../../ui/confirm.js'
|
|
3
|
+
import { validateString, validateBoolean } from '../../core/PropValidation.js'
|
|
4
|
+
|
|
5
|
+
export function Confirm(props) {
|
|
6
|
+
return createPrompt('Confirm', props, async (p) => {
|
|
7
|
+
// Validate props
|
|
8
|
+
validateString(p.message || p.children, 'message/children', 'Confirm', true)
|
|
9
|
+
validateBoolean(p.initial, 'initial', 'Confirm')
|
|
10
|
+
|
|
11
|
+
const yesLabel = p.t ? p.t('yes') : 'yes'
|
|
12
|
+
const noLabel = p.t ? p.t('no') : 'no'
|
|
13
|
+
|
|
14
|
+
const result = await baseConfirm({
|
|
15
|
+
message: p.message || p.children,
|
|
16
|
+
initial: p.initial,
|
|
17
|
+
format: (val) => (val ? yesLabel : noLabel),
|
|
18
|
+
active: yesLabel,
|
|
19
|
+
inactive: noLabel,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Restore boolean value from formatted string
|
|
23
|
+
if (typeof result.value === 'string') {
|
|
24
|
+
result.value = result.value === yesLabel
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { datetime as baseDateTime } from '../../ui/date-time.js'
|
|
3
|
+
import { validateDate, validateString, validateFunction } from '../../core/PropValidation.js'
|
|
4
|
+
|
|
5
|
+
export function DateTime(props) {
|
|
6
|
+
return createPrompt('DateTime', props, async (p) => {
|
|
7
|
+
// Validate props
|
|
8
|
+
validateString(p.message || p.label, 'message/label', 'DateTime', true)
|
|
9
|
+
validateDate(p.initial, 'initial', 'DateTime')
|
|
10
|
+
validateString(p.mask, 'mask', 'DateTime')
|
|
11
|
+
validateFunction(p.t, 't', 'DateTime')
|
|
12
|
+
|
|
13
|
+
// Auto-convert initial to Date if it's a string
|
|
14
|
+
let initial = p.initial
|
|
15
|
+
if (typeof initial === 'string') {
|
|
16
|
+
initial = new Date(initial)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return await baseDateTime({
|
|
20
|
+
message: p.message || p.label,
|
|
21
|
+
initial: initial,
|
|
22
|
+
mask: p.mask,
|
|
23
|
+
t: p.t,
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { text } from '../../ui/input.js'
|
|
3
|
+
|
|
4
|
+
export function Input(props) {
|
|
5
|
+
return createPrompt('Input', props, async (p) => {
|
|
6
|
+
const config = {
|
|
7
|
+
message: p.message || p.label,
|
|
8
|
+
initial: p.initial || p.defaultValue,
|
|
9
|
+
type: p.type || 'text',
|
|
10
|
+
validate: p.validate || p.validator,
|
|
11
|
+
format: p.format,
|
|
12
|
+
}
|
|
13
|
+
return await text(config)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { mask as baseMask } from '../../ui/mask.js'
|
|
3
|
+
|
|
4
|
+
export function Mask(props) {
|
|
5
|
+
return createPrompt('Mask', props, async (p) => {
|
|
6
|
+
return await baseMask({
|
|
7
|
+
message: p.message || p.label,
|
|
8
|
+
mask: p.mask,
|
|
9
|
+
placeholder: p.placeholder,
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { multiselect as baseMultiselect } from '../../ui/multiselect.js'
|
|
3
|
+
|
|
4
|
+
function getMultiselectInstructions(t) {
|
|
5
|
+
if (!t) return undefined
|
|
6
|
+
return (
|
|
7
|
+
`\n${t('Instructions')}:\n` +
|
|
8
|
+
` ↑/↓: ${t('Highlight option')}\n` +
|
|
9
|
+
` ←/→/[space]: ${t('Toggle selection')}\n` +
|
|
10
|
+
` a: ${t('Toggle all')}\n` +
|
|
11
|
+
` enter/return: ${t('Complete answer')}`
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Multiselect(props) {
|
|
16
|
+
return createPrompt('Multiselect', props, async (p) => {
|
|
17
|
+
return await baseMultiselect({
|
|
18
|
+
message: p.message || p.label,
|
|
19
|
+
options: p.options,
|
|
20
|
+
limit: p.limit,
|
|
21
|
+
initial: p.initial,
|
|
22
|
+
instructions: p.instructions !== undefined ? p.instructions : getMultiselectInstructions(p.t),
|
|
23
|
+
hint: p.hint || (p.t ? p.t('hint.multiselect') : undefined),
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { text } from '../../ui/input.js'
|
|
3
|
+
|
|
4
|
+
export function Password(props) {
|
|
5
|
+
return createPrompt('Password', props, async (p) => {
|
|
6
|
+
return await text({
|
|
7
|
+
message: p.message || p.label,
|
|
8
|
+
initial: p.initial,
|
|
9
|
+
type: 'password',
|
|
10
|
+
validate: p.validate || p.validator,
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { pause } from '../../ui/next.js'
|
|
3
|
+
|
|
4
|
+
export function Pause(props) {
|
|
5
|
+
if (typeof props === 'number') props = { ms: props }
|
|
6
|
+
return createPrompt('Pause', props, async (p) => {
|
|
7
|
+
return await pause(p.ms || 1000)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { progress as baseProgress } from '../../ui/progress.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ProgressBar Component.
|
|
6
|
+
* usage:
|
|
7
|
+
* const bar = await render(ProgressBar({ total: 100 }));
|
|
8
|
+
* bar.update(50);
|
|
9
|
+
*/
|
|
10
|
+
export function ProgressBar(props) {
|
|
11
|
+
return createPrompt('ProgressBar', props, async (p) => {
|
|
12
|
+
const bar = baseProgress(p)
|
|
13
|
+
// If action is provided, we could auto-run it, but progress bars usually strictly driven
|
|
14
|
+
return bar
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { select } from '../../ui/select.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Select Prompt Component.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {string} props.message - Question/Title.
|
|
9
|
+
* @param {Array} props.options - Options list.
|
|
10
|
+
* @param {number} [props.limit] - Max visible options.
|
|
11
|
+
*/
|
|
12
|
+
export function Select(props) {
|
|
13
|
+
return createPrompt('Select', props, async (p) => {
|
|
14
|
+
// Map props to legacy select config
|
|
15
|
+
// New API uses 'message', 'children'?, 'options'
|
|
16
|
+
// Legacy used 'title', 'prompt', 'options'
|
|
17
|
+
|
|
18
|
+
const config = {
|
|
19
|
+
title: p.title || p.message, // Support both
|
|
20
|
+
options: p.options || p.children, // Support children as options?
|
|
21
|
+
limit: p.limit,
|
|
22
|
+
hint: p.hint || (p.t ? p.t('hint.select') : undefined),
|
|
23
|
+
// Pass other potential legacy props
|
|
24
|
+
...p,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return await select(config)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { slider as baseSlider } from '../../ui/slider.js'
|
|
3
|
+
|
|
4
|
+
export function Slider(props) {
|
|
5
|
+
return createPrompt('Slider', props, async (p) => {
|
|
6
|
+
return await baseSlider({
|
|
7
|
+
message: p.message || p.label,
|
|
8
|
+
initial: p.initial,
|
|
9
|
+
min: p.min,
|
|
10
|
+
max: p.max,
|
|
11
|
+
step: p.step,
|
|
12
|
+
jump: p.jump,
|
|
13
|
+
t: p.t,
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { spinner as baseSpinner } from '../../ui/spinner.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spinner Component.
|
|
6
|
+
* Usage: await render(Spinner({ message: 'Loading...', action: promise }))
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} props
|
|
9
|
+
* @param {string} props.message - Main message.
|
|
10
|
+
* @param {Promise<any>} [props.action] - Async action to wait for.
|
|
11
|
+
* @param {string} [props.successMessage] - Message on success.
|
|
12
|
+
* @param {string} [props.errorMessage] - Message on error.
|
|
13
|
+
*/
|
|
14
|
+
export function Spinner(props) {
|
|
15
|
+
return createPrompt('Spinner', props, async (p) => {
|
|
16
|
+
const spin = baseSpinner(p.message)
|
|
17
|
+
if (p.action && p.action.then) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await p.action
|
|
20
|
+
spin.success(p.successMessage || 'Done')
|
|
21
|
+
return res
|
|
22
|
+
} catch (err) {
|
|
23
|
+
spin.error(p.errorMessage || 'Error')
|
|
24
|
+
throw err
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return spin // Return controller if no action (legacy behavior)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { toggle as baseToggle } from '../../ui/toggle.js'
|
|
3
|
+
|
|
4
|
+
export function Toggle(props) {
|
|
5
|
+
return createPrompt('Toggle', props, async (p) => {
|
|
6
|
+
return await baseToggle({
|
|
7
|
+
message: p.message || p.children,
|
|
8
|
+
initial: p.initial,
|
|
9
|
+
active: p.active,
|
|
10
|
+
inactive: p.inactive,
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { tree } from '../../ui/tree.js'
|
|
3
|
+
|
|
4
|
+
export function Tree(props) {
|
|
5
|
+
return createPrompt('Tree', props, async (p) => {
|
|
6
|
+
return await tree({
|
|
7
|
+
message: p.message || p.title,
|
|
8
|
+
mode: p.mode,
|
|
9
|
+
tree: p.tree || p.options || p.data,
|
|
10
|
+
loader: p.loader,
|
|
11
|
+
limit: p.limit,
|
|
12
|
+
initialExpanded: p.expanded,
|
|
13
|
+
multiselect: p.multiselect,
|
|
14
|
+
t: p.t,
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createView } from '../../core/Component.js'
|
|
2
|
+
import Logger from '@nan0web/log'
|
|
3
|
+
import { beep } from '../../ui/input.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Alert View Component.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} props
|
|
9
|
+
* @param {string} props.title - Title of the alert.
|
|
10
|
+
* @param {string} props.children - Message content.
|
|
11
|
+
* @param {'info'|'success'|'warning'|'error'} [props.variant='info'] - Style variant.
|
|
12
|
+
* @param {string} [props.message] - Alias for children (legacy support).
|
|
13
|
+
* @param {boolean} [props.sound] - Play sound (side-effect during toString is acceptable here as it invokes on print).
|
|
14
|
+
*/
|
|
15
|
+
export function Alert(props) {
|
|
16
|
+
// Normalize props (allow string argument as children shorthand)
|
|
17
|
+
const defaults = { title: '', children: '', variant: 'info' }
|
|
18
|
+
|
|
19
|
+
// If props is string, treat as children, otherwise merge with defaults
|
|
20
|
+
const safeProps =
|
|
21
|
+
typeof props === 'string' ? { ...defaults, children: props } : { ...defaults, ...props }
|
|
22
|
+
|
|
23
|
+
const { title, children, message = children, variant } = safeProps
|
|
24
|
+
|
|
25
|
+
return createView('Alert', props, () => {
|
|
26
|
+
// Logic extracted from old alert.js
|
|
27
|
+
const sound = props.sound || variant === 'error' || variant === 'warning'
|
|
28
|
+
|
|
29
|
+
// Note: beep() is technically a side effect.
|
|
30
|
+
// In "toString" pattern, side effects happen when string conversion receives focus.
|
|
31
|
+
// It's acceptable for sound, but we should be careful.
|
|
32
|
+
if (sound) beep()
|
|
33
|
+
|
|
34
|
+
const colors = {
|
|
35
|
+
info: Logger.CYAN,
|
|
36
|
+
success: Logger.GREEN,
|
|
37
|
+
warning: Logger.YELLOW,
|
|
38
|
+
error: Logger.RED,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const color = colors[variant] || Logger.WHITE
|
|
42
|
+
const icon =
|
|
43
|
+
{
|
|
44
|
+
info: 'ℹ',
|
|
45
|
+
success: '✔',
|
|
46
|
+
warning: '⚠',
|
|
47
|
+
error: '✖',
|
|
48
|
+
}[variant] || '•'
|
|
49
|
+
|
|
50
|
+
const msgStr = String(message || '')
|
|
51
|
+
const lines = msgStr.split('\n')
|
|
52
|
+
const paddedLines = lines.map((line) => ` ${line}`)
|
|
53
|
+
|
|
54
|
+
// Calculate max line length for border (at least 60 chars or based on content)
|
|
55
|
+
const contentLengths = [title ? title.length + 6 : 0, ...paddedLines.map((l) => l.length)]
|
|
56
|
+
const maxContentLen = Math.max(...contentLengths)
|
|
57
|
+
const len = Math.max(60, maxContentLen + 2)
|
|
58
|
+
|
|
59
|
+
const border = Logger.style('━'.repeat(len), { color })
|
|
60
|
+
let out = ''
|
|
61
|
+
|
|
62
|
+
out += `\n${border}\n`
|
|
63
|
+
if (title) {
|
|
64
|
+
out += Logger.style(` ${icon} ${title} `, { color }) + ' \n'
|
|
65
|
+
out += Logger.style('─'.repeat(len) + '\n', { color: Logger.DIM })
|
|
66
|
+
}
|
|
67
|
+
lines.forEach((line) => {
|
|
68
|
+
if (line.trim()) {
|
|
69
|
+
out += ' ' + Logger.style(line.trim(), { color }) + '\n'
|
|
70
|
+
} else {
|
|
71
|
+
out += '\n'
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
out += `${border}\n`
|
|
75
|
+
|
|
76
|
+
return out
|
|
77
|
+
})
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createView } from '../../core/Component.js'
|
|
2
|
+
import { badge as baseBadge } from '../../ui/badge.js'
|
|
3
|
+
|
|
4
|
+
export function Badge(props) {
|
|
5
|
+
if (typeof props === 'string') {
|
|
6
|
+
props = { label: props }
|
|
7
|
+
}
|
|
8
|
+
return createView('Badge', props, (p) => {
|
|
9
|
+
return baseBadge(p.label, p.variant)
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createView } from '../../core/Component.js'
|
|
2
|
+
import { breadcrumbs, tabs, steps } from '../../ui/nav.js'
|
|
3
|
+
|
|
4
|
+
export function Breadcrumbs(props) {
|
|
5
|
+
if (Array.isArray(props)) props = { items: props }
|
|
6
|
+
return createView('Breadcrumbs', props, (p) => {
|
|
7
|
+
return breadcrumbs(p.items, p.options)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Tabs(props) {
|
|
12
|
+
if (Array.isArray(props)) props = { items: props }
|
|
13
|
+
return createView('Tabs', props, (p) => {
|
|
14
|
+
return tabs(p.items, p.active)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Steps(props) {
|
|
19
|
+
if (Array.isArray(props)) props = { items: props }
|
|
20
|
+
return createView('Steps', props, (p) => {
|
|
21
|
+
return steps(p.items, p.current)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createPrompt } from '../../core/Component.js'
|
|
2
|
+
import { table as baseTable } from '../../ui/table.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Table Component.
|
|
6
|
+
* Can be static (display only) or interactive (filter/select).
|
|
7
|
+
*/
|
|
8
|
+
export function Table(props) {
|
|
9
|
+
return createPrompt('Table', props, async (p) => {
|
|
10
|
+
return await baseTable(p)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createView } from '../../core/Component.js'
|
|
2
|
+
import { toast as baseToast } from '../../ui/toast.js'
|
|
3
|
+
|
|
4
|
+
export function Toast(props) {
|
|
5
|
+
if (typeof props === 'string') props = { message: props, variant: 'info' }
|
|
6
|
+
return createView('Toast', props, (p) => {
|
|
7
|
+
return baseToast(p.message, p.variant)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Component logic.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract for View (static) and Prompt (interactive) components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const ComponentSymbol = Symbol.for('ui.component')
|
|
8
|
+
export const PromptSymbol = Symbol.for('ui.prompt')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a Static View Component.
|
|
12
|
+
* These components are synchronous and can be stringified directly.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} displayName - Name of the component (e.g. 'Alert').
|
|
15
|
+
* @param {any} props - Props passed to the component.
|
|
16
|
+
* @param {Function} formatFn - Pure function (props) => string.
|
|
17
|
+
*/
|
|
18
|
+
export function createView(displayName, props, formatFn) {
|
|
19
|
+
const component = {
|
|
20
|
+
$$typeof: ComponentSymbol,
|
|
21
|
+
type: displayName,
|
|
22
|
+
props,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Magic: toString calls the formatter
|
|
26
|
+
// We also implement nodejs.util.inspect.custom so console.log works directly
|
|
27
|
+
const toString = () => {
|
|
28
|
+
try {
|
|
29
|
+
return formatFn(props)
|
|
30
|
+
} catch (originalError) {
|
|
31
|
+
const e = /** @type {Error} */ (originalError)
|
|
32
|
+
return `[${displayName} Error: ${e.message}]`
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Object.defineProperty(component, 'toString', {
|
|
37
|
+
value: toString,
|
|
38
|
+
enumerable: false,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
Object.defineProperty(component, Symbol.for('nodejs.util.inspect.custom'), {
|
|
42
|
+
value: () => toString(),
|
|
43
|
+
enumerable: false,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return component
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates an Interactive Prompt Component.
|
|
51
|
+
* These components are asynchronous and require `render()` or `await` handling.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} displayName - Name of the component.
|
|
54
|
+
* @param {any} props - Props.
|
|
55
|
+
* @param {Function} executorFn - Async function (props) => Promise<result>.
|
|
56
|
+
*/
|
|
57
|
+
export function createPrompt(displayName, props, executorFn) {
|
|
58
|
+
const component = {
|
|
59
|
+
$$typeof: PromptSymbol,
|
|
60
|
+
type: displayName,
|
|
61
|
+
props,
|
|
62
|
+
execute: () => executorFn(props),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Prompts cannot be stringified meaningfully
|
|
66
|
+
const toString = () => `[Prompt: ${displayName}]`
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(component, 'toString', {
|
|
69
|
+
value: toString,
|
|
70
|
+
enumerable: false,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
Object.defineProperty(component, Symbol.for('nodejs.util.inspect.custom'), {
|
|
74
|
+
value: toString,
|
|
75
|
+
enumerable: false,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return component
|
|
79
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop validation utilities for UI components.
|
|
3
|
+
* @module core/PropValidation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates that a value is a valid Date object or a parseable date string.
|
|
8
|
+
* @param {any} value
|
|
9
|
+
* @param {string} propName
|
|
10
|
+
* @param {string} componentName
|
|
11
|
+
* @throws {TypeError} if validation fails
|
|
12
|
+
*/
|
|
13
|
+
export function validateDate(value, propName, componentName) {
|
|
14
|
+
if (value === undefined || value === null) {
|
|
15
|
+
return // Optional prop
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (value instanceof Date) {
|
|
19
|
+
if (isNaN(value.getTime())) {
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
`[${componentName}] Invalid Date object for prop "${propName}". Date is invalid.`
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
const parsed = new Date(value)
|
|
29
|
+
if (isNaN(parsed.getTime())) {
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
`[${componentName}] Invalid date string for prop "${propName}": "${value}". ` +
|
|
32
|
+
`Expected a valid ISO date string (e.g., "2026-02-05" or "2026-02-05T10:30:00").`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new TypeError(
|
|
39
|
+
`[${componentName}] Invalid type for prop "${propName}". ` +
|
|
40
|
+
`Expected Date object or date string, got ${typeof value}.`
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates that a value is a string.
|
|
46
|
+
* @param {any} value
|
|
47
|
+
* @param {string} propName
|
|
48
|
+
* @param {string} componentName
|
|
49
|
+
* @param {boolean} required
|
|
50
|
+
* @throws {TypeError} if validation fails
|
|
51
|
+
*/
|
|
52
|
+
export function validateString(value, propName, componentName, required = false) {
|
|
53
|
+
if (value === undefined || value === null) {
|
|
54
|
+
if (required) {
|
|
55
|
+
throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
|
|
56
|
+
}
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof value !== 'string') {
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
`[${componentName}] Invalid type for prop "${propName}". ` +
|
|
63
|
+
`Expected string, got ${typeof value}.`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates that a value is a function.
|
|
70
|
+
* @param {any} value
|
|
71
|
+
* @param {string} propName
|
|
72
|
+
* @param {string} componentName
|
|
73
|
+
* @param {boolean} required
|
|
74
|
+
* @throws {TypeError} if validation fails
|
|
75
|
+
*/
|
|
76
|
+
export function validateFunction(value, propName, componentName, required = false) {
|
|
77
|
+
if (value === undefined || value === null) {
|
|
78
|
+
if (required) {
|
|
79
|
+
throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
|
|
80
|
+
}
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof value !== 'function') {
|
|
85
|
+
throw new TypeError(
|
|
86
|
+
`[${componentName}] Invalid type for prop "${propName}". ` +
|
|
87
|
+
`Expected function, got ${typeof value}.`
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validates that a value is a boolean.
|
|
94
|
+
* @param {any} value
|
|
95
|
+
* @param {string} propName
|
|
96
|
+
* @param {string} componentName
|
|
97
|
+
* @param {boolean} required
|
|
98
|
+
* @throws {TypeError} if validation fails
|
|
99
|
+
*/
|
|
100
|
+
export function validateBoolean(value, propName, componentName, required = false) {
|
|
101
|
+
if (value === undefined || value === null) {
|
|
102
|
+
if (required) {
|
|
103
|
+
throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof value !== 'boolean') {
|
|
109
|
+
throw new TypeError(
|
|
110
|
+
`[${componentName}] Invalid type for prop "${propName}". ` +
|
|
111
|
+
`Expected boolean, got ${typeof value}.`
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validates that a value is a number.
|
|
118
|
+
* @param {any} value
|
|
119
|
+
* @param {string} propName
|
|
120
|
+
* @param {string} componentName
|
|
121
|
+
* @param {boolean} required
|
|
122
|
+
* @throws {TypeError} if validation fails
|
|
123
|
+
*/
|
|
124
|
+
export function validateNumber(value, propName, componentName, required = false) {
|
|
125
|
+
if (value === undefined || value === null) {
|
|
126
|
+
if (required) {
|
|
127
|
+
throw new TypeError(`[${componentName}] Required prop "${propName}" is missing.`)
|
|
128
|
+
}
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
133
|
+
throw new TypeError(
|
|
134
|
+
`[${componentName}] Invalid type for prop "${propName}". ` +
|
|
135
|
+
`Expected number, got ${typeof value}.`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|