@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.
Files changed (42) hide show
  1. package/README.md +7 -1
  2. package/package.json +6 -1
  3. package/src/README.md.js +23 -0
  4. package/src/core/Error/CancelError.js +6 -0
  5. package/src/core/InputAdapter.js +21 -3
  6. package/types/core/Error/CancelError.d.ts +3 -0
  7. package/types/core/InputAdapter.d.ts +18 -2
  8. package/.datasets/README.dataset.jsonl +0 -12
  9. package/.editorconfig +0 -20
  10. package/CONTRIBUTING.md +0 -42
  11. package/docs/uk/README.md +0 -240
  12. package/playground/User.js +0 -52
  13. package/playground/currency.exchange.js +0 -48
  14. package/playground/i18n/index.js +0 -21
  15. package/playground/i18n/uk.js +0 -53
  16. package/playground/language.form.js +0 -25
  17. package/playground/main.js +0 -72
  18. package/playground/registration.form.js +0 -58
  19. package/playground/topup.telephone.js +0 -62
  20. package/src/App/User/UserApp.test.js +0 -56
  21. package/src/App/User/UserUI.test.js +0 -51
  22. package/src/Frame/Frame.test.js +0 -429
  23. package/src/View/View.test.js +0 -77
  24. package/src/core/Form/Form.test.js +0 -116
  25. package/src/core/Form/Input.test.js +0 -58
  26. package/src/core/Form/Message.test.js +0 -54
  27. package/src/core/InputAdapter.test.js +0 -35
  28. package/src/core/Message/InputMessage.test.js +0 -45
  29. package/src/core/Message/Message.test.js +0 -58
  30. package/src/core/Message/OutputMessage.test.js +0 -61
  31. package/src/core/OutputAdapter.test.js +0 -35
  32. package/src/core/Stream.test.js +0 -78
  33. package/src/index.test.js +0 -14
  34. package/stories/App/AppView.js +0 -15
  35. package/stories/App/AppView.test.js +0 -22
  36. package/stories/App/RenderOptions.js +0 -14
  37. package/stories/nodejs/interface.test.js +0 -27
  38. package/system.md +0 -187
  39. package/system1.md +0 -137
  40. package/task.md +0 -181
  41. package/tsconfig.json +0 -23
  42. package/vitest.config.js +0 -26
@@ -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
- }
@@ -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
- })