@nan0web/ui 1.0.0 → 1.0.2
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 +7 -1
- package/package.json +6 -1
- package/src/README.md.js +23 -0
- package/src/core/Error/CancelError.js +6 -0
- package/src/core/InputAdapter.js +21 -3
- package/types/core/Error/CancelError.d.ts +3 -0
- package/types/core/InputAdapter.d.ts +18 -2
- package/.datasets/README.dataset.jsonl +0 -12
- package/.editorconfig +0 -20
- package/CONTRIBUTING.md +0 -42
- package/docs/uk/README.md +0 -240
- package/playground/User.js +0 -52
- package/playground/currency.exchange.js +0 -48
- package/playground/i18n/index.js +0 -21
- package/playground/i18n/uk.js +0 -53
- package/playground/language.form.js +0 -25
- package/playground/main.js +0 -72
- package/playground/registration.form.js +0 -58
- package/playground/topup.telephone.js +0 -62
- package/src/App/User/UserApp.test.js +0 -56
- package/src/App/User/UserUI.test.js +0 -51
- package/src/Frame/Frame.test.js +0 -429
- package/src/View/View.test.js +0 -77
- package/src/core/Form/Form.test.js +0 -116
- package/src/core/Form/Input.test.js +0 -58
- package/src/core/Form/Message.test.js +0 -54
- package/src/core/InputAdapter.test.js +0 -35
- package/src/core/Message/InputMessage.test.js +0 -45
- package/src/core/Message/Message.test.js +0 -58
- package/src/core/Message/OutputMessage.test.js +0 -61
- package/src/core/OutputAdapter.test.js +0 -35
- package/src/core/Stream.test.js +0 -78
- package/src/index.test.js +0 -14
- package/stories/App/AppView.js +0 -15
- package/stories/App/AppView.test.js +0 -22
- package/stories/App/RenderOptions.js +0 -14
- package/stories/nodejs/interface.test.js +0 -27
- package/system.md +0 -187
- package/system1.md +0 -137
- package/task.md +0 -181
- package/tsconfig.json +0 -23
- package/vitest.config.js +0 -26
package/playground/i18n/uk.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ukrainian translations for the playground.
|
|
3
|
-
*
|
|
4
|
-
* The object keys are English identifiers, the values are Ukrainian strings.
|
|
5
|
-
*
|
|
6
|
-
* Example usage:
|
|
7
|
-
* import vocabMap from "./i18n/index.js"
|
|
8
|
-
* const vocab = vocabMap("uk")
|
|
9
|
-
* const t = createT(vocab)
|
|
10
|
-
* console.log(t("Registration Form")) // → "Форма реєстрації"
|
|
11
|
-
* console.log(vocab["Registration Form"]) // → "Форма реєстрації"
|
|
12
|
-
*/
|
|
13
|
-
export default {
|
|
14
|
-
"Select demo:": "Оберіть демонстрацію:",
|
|
15
|
-
"Registration Form": "Форма реєстрації",
|
|
16
|
-
"Currency Exchange": "Обмін валют",
|
|
17
|
-
"Top‑up Telephone": "Поповнення телефону",
|
|
18
|
-
"Enter number (or 0 to quit):": "Введіть номер (або 0 для виходу):",
|
|
19
|
-
"Good‑bye.": "До побачення.",
|
|
20
|
-
"Invalid choice.": "Невірний вибір.",
|
|
21
|
-
"Username": "Ім'я користувача",
|
|
22
|
-
"Password": "Пароль",
|
|
23
|
-
"Confirm Password": "Підтвердження пароля",
|
|
24
|
-
"Email or Telephone": "Електронна пошта або телефон",
|
|
25
|
-
"Press ENTER to submit, type 0 to cancel": "Натисніть ENTER для відправки, введіть 0 для скасування",
|
|
26
|
-
"Cancelled.": "Скасовано.",
|
|
27
|
-
"Form submitted successfully!": "Форма успішно надіслана!",
|
|
28
|
-
"Phone Number": "Номер телефону",
|
|
29
|
-
"Top‑up Amount": "Сума поповнення",
|
|
30
|
-
"Select currency": "Виберіть валюту",
|
|
31
|
-
"Currency options:": "Валютні опції:",
|
|
32
|
-
"Invalid choice.": "Неправильний вибір.",
|
|
33
|
-
"Phone number is required.": "Необхідно вказати номер телефону.",
|
|
34
|
-
"Invalid phone number. Use digits only, 7‑15 characters.": "Неправильний номер телефону. Використовуйте лише цифри, 7-15 символів.",
|
|
35
|
-
"Invalid amount.": "Неправильна сума.",
|
|
36
|
-
"Top‑up of": "Поповнення на",
|
|
37
|
-
"to": "до",
|
|
38
|
-
"scheduled.": "заплановано.",
|
|
39
|
-
"options:": "опції:",
|
|
40
|
-
"Select": "Зробіть вибір",
|
|
41
|
-
"From Currency": "З якої валюти міняємо",
|
|
42
|
-
"To Currency": "У яку валюту",
|
|
43
|
-
"Amount": "Сума",
|
|
44
|
-
"Invalid choice, try again.": "Неправильний вибір, спробуйте знову.",
|
|
45
|
-
"Password (min 4 chars)": "Пароль (мінімум 4 символи)",
|
|
46
|
-
"Passwords do not match. Try again.": "Паролі не збігаються. Спробуйте знову.",
|
|
47
|
-
"[me]": "[Я]",
|
|
48
|
-
"[me invalid]": "[Я помилився]",
|
|
49
|
-
"! Invalid choice.": "! Неправильний вибір.",
|
|
50
|
-
"⨉ Cancelled.": "⨉ Скасовано.",
|
|
51
|
-
"Select a language": "Виберіть мову",
|
|
52
|
-
"Language Selector": "Вибір мови",
|
|
53
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple currency exchange console tool.
|
|
3
|
-
*
|
|
4
|
-
* Prompts for:
|
|
5
|
-
* - From currency (options)
|
|
6
|
-
* - To currency (options)
|
|
7
|
-
* - Amount (number)
|
|
8
|
-
*
|
|
9
|
-
* Controls:
|
|
10
|
-
* - Input "0" on any prompt to cancel.
|
|
11
|
-
* - Empty input on final confirmation submits.
|
|
12
|
-
*/
|
|
13
|
-
import { localesMap } from "./i18n/index.js"
|
|
14
|
-
import { select } from "@nan0web/ui-cli"
|
|
15
|
-
|
|
16
|
-
/** Main exchange flow */
|
|
17
|
-
export async function runLanguage(t, ask, console, prompt, invalidPrompt) {
|
|
18
|
-
const lang = await select({
|
|
19
|
-
title: "\n=== " + t("Language Selector") + " ===",
|
|
20
|
-
prompt, invalidPrompt, console,
|
|
21
|
-
options: Array.from(localesMap.keys()),
|
|
22
|
-
})
|
|
23
|
-
console.success(`\n${lang.value}\n`)
|
|
24
|
-
return lang.value
|
|
25
|
-
}
|
package/playground/main.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playground entry point.
|
|
3
|
-
*
|
|
4
|
-
* Choose which demo to run:
|
|
5
|
-
* 1) Select a language
|
|
6
|
-
* 2) Registration Form
|
|
7
|
-
* 3) Currency Exchange
|
|
8
|
-
* 4) Top‑up Telephone
|
|
9
|
-
*
|
|
10
|
-
* Type 0 to exit.
|
|
11
|
-
*/
|
|
12
|
-
import { argv } from "node:process"
|
|
13
|
-
import Logger from "@nan0web/log"
|
|
14
|
-
import { runLanguage } from "./language.form.js"
|
|
15
|
-
import { runRegistration } from "./registration.form.js"
|
|
16
|
-
import { runExchange } from "./currency.exchange.js"
|
|
17
|
-
import { runTopup } from "./topup.telephone.js"
|
|
18
|
-
import createT, { detectLocale } from "./i18n/index.js"
|
|
19
|
-
import createInput, { CancelError, select } from "./ui/index.js"
|
|
20
|
-
|
|
21
|
-
const console = new Logger(Logger.detectLevel(argv))
|
|
22
|
-
let t = createT(detectLocale(argv))
|
|
23
|
-
const ask = createInput(["0"])
|
|
24
|
-
const menuOptions = [
|
|
25
|
-
"Select a language", // t("Select a language")
|
|
26
|
-
"Registration Form", // t("Registration Form")
|
|
27
|
-
"Currency Exchange", // t("Currency Exchange")
|
|
28
|
-
"Top‑up Telephone", // t("Top‑up Telephone")
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
async function main() {
|
|
32
|
-
console.info(Logger.style(Logger.LOGO, { color: "magenta" }))
|
|
33
|
-
|
|
34
|
-
while (true) {
|
|
35
|
-
try {
|
|
36
|
-
const prompt = t("[me]") + ": "
|
|
37
|
-
const invalidPrompt = Logger.style(t("[me invalid]", t("[me]")), { bgColor: "yellow" }) + ": "
|
|
38
|
-
const choice = await select({
|
|
39
|
-
title: t("Select demo:"),
|
|
40
|
-
prompt: t("[me]") + ": ",
|
|
41
|
-
invalidPrompt: Logger.style(t("[me]"), { bgColor: "yellow" }) + ": ",
|
|
42
|
-
options: menuOptions.map(t),
|
|
43
|
-
console,
|
|
44
|
-
})
|
|
45
|
-
switch (choice.index) {
|
|
46
|
-
case 0:
|
|
47
|
-
const lang = await runLanguage(t, ask, console, prompt, invalidPrompt)
|
|
48
|
-
t = createT(lang)
|
|
49
|
-
break
|
|
50
|
-
case 1:
|
|
51
|
-
await runRegistration(t, ask, console, prompt, invalidPrompt)
|
|
52
|
-
break
|
|
53
|
-
case 2:
|
|
54
|
-
await runExchange(t, ask, console, prompt, invalidPrompt)
|
|
55
|
-
break
|
|
56
|
-
case 3:
|
|
57
|
-
await runTopup(t, ask, console, prompt, invalidPrompt)
|
|
58
|
-
break
|
|
59
|
-
default:
|
|
60
|
-
console.warn(t("! Invalid choice."))
|
|
61
|
-
}
|
|
62
|
-
} catch (err) {
|
|
63
|
-
if (err instanceof CancelError) {
|
|
64
|
-
console.warn("\n" + t("⨉ Cancelled."))
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
throw err
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
main()
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple registration form using stdin.
|
|
3
|
-
*
|
|
4
|
-
* Fields: username, password, confirm, emailOrTel
|
|
5
|
-
*
|
|
6
|
-
* Controls:
|
|
7
|
-
* - Input "0" -> cancel (onCancel)
|
|
8
|
-
* - Empty input on final prompt -> submit (onSubmit)
|
|
9
|
-
*/
|
|
10
|
-
import { CancelError } from "@nan0web/ui-cli"
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Main registration flow
|
|
14
|
-
* @todo add proper jsdoc to make autocomplete work for input as result of ask, console, t.
|
|
15
|
-
*/
|
|
16
|
-
export async function runRegistration(t, ask, console) {
|
|
17
|
-
console.info("\n=== " + t("Registration Form") + " ===")
|
|
18
|
-
const data = {}
|
|
19
|
-
|
|
20
|
-
// username
|
|
21
|
-
{
|
|
22
|
-
const input = await ask(t("Username") + ": ", true)
|
|
23
|
-
if (input.cancelled) throw new CancelError()
|
|
24
|
-
data.username = input.value
|
|
25
|
-
}
|
|
26
|
-
// password
|
|
27
|
-
{
|
|
28
|
-
const input = await ask(
|
|
29
|
-
t("Password (min 4 chars)") + ": ",
|
|
30
|
-
(input) => input.value.length < 4,
|
|
31
|
-
t("! Password must be at least 4 characters") + ": "
|
|
32
|
-
)
|
|
33
|
-
if (input.cancelled) throw new CancelError()
|
|
34
|
-
data.password = input.value
|
|
35
|
-
}
|
|
36
|
-
// confirm password
|
|
37
|
-
{
|
|
38
|
-
await ask(t("Confirm Password") + ": ", (input) => {
|
|
39
|
-
if (input.value !== data.password) {
|
|
40
|
-
console.error(t("Passwords do not match. Try again."))
|
|
41
|
-
return true
|
|
42
|
-
}
|
|
43
|
-
return false
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
// email or telephone
|
|
47
|
-
{
|
|
48
|
-
const input = await ask(t("Email or Telephone") + ": ", true)
|
|
49
|
-
if (input.cancelled) throw new CancelError()
|
|
50
|
-
data.emailOrTel = input.value
|
|
51
|
-
}
|
|
52
|
-
// final confirmation – empty line submits, "0" cancels
|
|
53
|
-
{
|
|
54
|
-
await ask(t("Press ENTER to submit, type 0 to cancel") + ": ", i => "" != i.value)
|
|
55
|
-
}
|
|
56
|
-
console.success("\n" + t("Form submitted successfully!"))
|
|
57
|
-
console.info(JSON.stringify({ ...data, password: "****" }, null, 2))
|
|
58
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple telephone top‑up form.
|
|
3
|
-
*
|
|
4
|
-
* Fields: number, amount, currency
|
|
5
|
-
*
|
|
6
|
-
* Controls:
|
|
7
|
-
* - Input "0" on any prompt to cancel.
|
|
8
|
-
* - Empty input on final confirmation submits.
|
|
9
|
-
*
|
|
10
|
-
* Validation:
|
|
11
|
-
* - Phone number must contain only digits and be 7‑15 characters long.
|
|
12
|
-
*/
|
|
13
|
-
import { CancelError } from "@nan0web/ui-cli"
|
|
14
|
-
|
|
15
|
-
const rates = {
|
|
16
|
-
USD: 1,
|
|
17
|
-
EUR: 0.9,
|
|
18
|
-
UAH: 27,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Main top‑up flow */
|
|
22
|
-
export async function runTopup(t, ask, console) {
|
|
23
|
-
/** Choose currency */
|
|
24
|
-
async function chooseCurrency(t) {
|
|
25
|
-
const list = Object.keys(rates)
|
|
26
|
-
console.info(t("Currency options:"))
|
|
27
|
-
list.forEach((c, i) => console.info(` ${i + 1}) ${c}`))
|
|
28
|
-
const input = await ask(
|
|
29
|
-
`${t("Select currency")} (1-${list.length}): `,
|
|
30
|
-
input => {
|
|
31
|
-
input.idx = Number(input.value) - 1
|
|
32
|
-
return ! (input.idx >= 0 && input.idx < list.length)
|
|
33
|
-
},
|
|
34
|
-
[t("Invalid choice."), t("Try again") + ":", ""].join(" ")
|
|
35
|
-
)
|
|
36
|
-
return list[input.idx] ?? null
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Validate phone number */
|
|
40
|
-
function isValidPhone(number) {
|
|
41
|
-
return /^[0-9]{7,15}$/.test(number)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
console.info("\n=== " + t("Top‑up Telephone") + " ===")
|
|
45
|
-
const numberInp = await ask(
|
|
46
|
-
t("Phone Number") + ": ",
|
|
47
|
-
input => !isValidPhone(input.value),
|
|
48
|
-
t("Invalid phone number. Use digits only, 7‑15 characters.") + ": "
|
|
49
|
-
)
|
|
50
|
-
if (numberInp.cancelled) throw new CancelError()
|
|
51
|
-
const number = numberInp.value
|
|
52
|
-
const amountInp = await ask(
|
|
53
|
-
t("Top‑up Amount") + ": ",
|
|
54
|
-
input => isNaN(input.value) || Number(input.value) <= 0 || Number(input.value) > 1_000_000,
|
|
55
|
-
t("Amount must be a positive number below 1 million.") + ": "
|
|
56
|
-
)
|
|
57
|
-
if (amountInp.cancelled) throw new CancelError()
|
|
58
|
-
const amount = Number(amountInp.value)
|
|
59
|
-
const currency = await chooseCurrency(t)
|
|
60
|
-
if (!currency) throw new CancelError()
|
|
61
|
-
console.success(`\n${t("Top‑up of")} ${amount} ${currency} ${t("to")} ${number} ${t("scheduled.")}\n`)
|
|
62
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test"
|
|
2
|
-
import { strict as assert } from "node:assert"
|
|
3
|
-
import UserApp from "./UserApp.js"
|
|
4
|
-
import View from "../../View/View.js"
|
|
5
|
-
import UserUI from "./UserUI.js"
|
|
6
|
-
import Command from "./Command/index.js"
|
|
7
|
-
import Frame from "../../Frame/Frame.js"
|
|
8
|
-
|
|
9
|
-
describe("UserApp", () => {
|
|
10
|
-
/** @type {UserApp} */
|
|
11
|
-
let app
|
|
12
|
-
/** @type {View} */
|
|
13
|
-
let view
|
|
14
|
-
/** @type {UserUI} */
|
|
15
|
-
let ui
|
|
16
|
-
|
|
17
|
-
it("should set user and welcome", async () => {
|
|
18
|
-
app = new UserApp()
|
|
19
|
-
view = new View()
|
|
20
|
-
view.register("Welcome", (input) => {
|
|
21
|
-
return ["Welcome " + input.user.name]
|
|
22
|
-
})
|
|
23
|
-
ui = new UserUI(app, view)
|
|
24
|
-
const cmd = Command.Message.parse("setUser --user Alice")
|
|
25
|
-
const result = await app.processCommand(cmd, ui)
|
|
26
|
-
assert.equal(String(result.message), Frame.CLEAR_LINE + "\r" + "Welcome Alice")
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it("should welcome with user", async () => {
|
|
30
|
-
app = new UserApp()
|
|
31
|
-
view = new View()
|
|
32
|
-
view.register("Welcome", (input) => {
|
|
33
|
-
return ["Welcome " + input.user.name]
|
|
34
|
-
})
|
|
35
|
-
ui = new UserUI(app, view)
|
|
36
|
-
const cmd = Command.Message.parse("welcome --user YaRa")
|
|
37
|
-
const result = await app.processCommand(cmd, ui)
|
|
38
|
-
assert.equal(String(result), Frame.CLEAR_LINE + "\r" + "Welcome YaRa")
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it("should ask for a user name and welcome with user", async () => {
|
|
42
|
-
app = new UserApp()
|
|
43
|
-
view = new View()
|
|
44
|
-
view.register("Welcome", (input) => {
|
|
45
|
-
return ["Welcome " + input.user.name]
|
|
46
|
-
})
|
|
47
|
-
ui = new UserUI(app, view)
|
|
48
|
-
const cmd = Command.Message.parse("welcome")
|
|
49
|
-
|
|
50
|
-
// Mock the stdin to provide input immediately
|
|
51
|
-
view.stdin.write("Alice\n")
|
|
52
|
-
|
|
53
|
-
const result = await app.processCommand(cmd, ui)
|
|
54
|
-
assert.equal(String(result), Frame.CLEAR_LINE + "\r" + "Welcome Alice\n")
|
|
55
|
-
})
|
|
56
|
-
})
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test"
|
|
2
|
-
import { strict as assert } from "node:assert"
|
|
3
|
-
import UserApp from "./UserApp.js"
|
|
4
|
-
import UserUI from "./UserUI.js"
|
|
5
|
-
import View from "../../View/View.js"
|
|
6
|
-
import InputMessage from "../../core/Message/InputMessage.js"
|
|
7
|
-
import Welcome from "../../Component/Welcome/index.js"
|
|
8
|
-
|
|
9
|
-
describe("UserUI", () => {
|
|
10
|
-
it("should convert input with user.name to commands", () => {
|
|
11
|
-
const app = new UserApp()
|
|
12
|
-
const view = new View()
|
|
13
|
-
const ui = new UserUI(app, view)
|
|
14
|
-
|
|
15
|
-
const commands = ui.convertInput("setUser --user Bob welcome")
|
|
16
|
-
assert.equal(commands.length, 1)
|
|
17
|
-
assert.equal(commands[0].args[0], "setUser")
|
|
18
|
-
assert.equal(commands[0].args[1], "welcome")
|
|
19
|
-
assert.equal(commands[0].opts.user, "Bob")
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it("should convert input without user.name to askUserName command", () => {
|
|
23
|
-
const app = new UserApp()
|
|
24
|
-
const view = new View()
|
|
25
|
-
const ui = new UserUI(app, view)
|
|
26
|
-
|
|
27
|
-
const commands = ui.convertInput("")
|
|
28
|
-
assert.equal(commands.length, 1)
|
|
29
|
-
assert.equal(commands[0].args.length, 0)
|
|
30
|
-
assert.equal(commands[0].opts.user, "")
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it("should process askUserName command interactively", async () => {
|
|
34
|
-
const app = new UserApp()
|
|
35
|
-
const view = new View()
|
|
36
|
-
const ui = new UserUI(app, view)
|
|
37
|
-
view.register("Welcome", Welcome)
|
|
38
|
-
|
|
39
|
-
// Mock view.ask to return a name
|
|
40
|
-
view.ask = (input) => Promise.resolve(new InputMessage("Charlie"))
|
|
41
|
-
// Mock output to collect outputs
|
|
42
|
-
const outputs = []
|
|
43
|
-
ui.output = (results) => outputs.push(...results)
|
|
44
|
-
|
|
45
|
-
const results = await ui.process(["welcome"])
|
|
46
|
-
assert.ok(view.ask)
|
|
47
|
-
assert.equal(results.length, 1)
|
|
48
|
-
assert.ok(results[0].value[0][0].includes("Welcome"))
|
|
49
|
-
assert.ok(view.stdout.stream[0].includes("Welcome Charlie!"))
|
|
50
|
-
})
|
|
51
|
-
})
|