@nan0web/ui 1.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.
Files changed (131) hide show
  1. package/.datasets/README.dataset.jsonl +12 -0
  2. package/.editorconfig +20 -0
  3. package/CONTRIBUTING.md +42 -0
  4. package/LICENSE +15 -0
  5. package/README.md +238 -0
  6. package/docs/uk/README.md +240 -0
  7. package/package.json +64 -0
  8. package/playground/User.js +52 -0
  9. package/playground/currency.exchange.js +48 -0
  10. package/playground/i18n/index.js +21 -0
  11. package/playground/i18n/uk.js +53 -0
  12. package/playground/language.form.js +25 -0
  13. package/playground/main.js +72 -0
  14. package/playground/registration.form.js +58 -0
  15. package/playground/topup.telephone.js +62 -0
  16. package/src/App/Command/Options.js +78 -0
  17. package/src/App/Command/index.js +9 -0
  18. package/src/App/Core/CoreApp.js +129 -0
  19. package/src/App/Core/UI.js +116 -0
  20. package/src/App/Core/Widget.js +67 -0
  21. package/src/App/Core/index.js +11 -0
  22. package/src/App/Scenario.js +45 -0
  23. package/src/App/User/Command/Message.js +44 -0
  24. package/src/App/User/Command/Options.js +48 -0
  25. package/src/App/User/Command/index.js +11 -0
  26. package/src/App/User/UserApp.js +73 -0
  27. package/src/App/User/UserApp.test.js +56 -0
  28. package/src/App/User/UserUI.js +20 -0
  29. package/src/App/User/UserUI.test.js +51 -0
  30. package/src/App/User/index.js +15 -0
  31. package/src/App/index.js +22 -0
  32. package/src/Component/Process/Input.js +70 -0
  33. package/src/Component/Process/Process.js +26 -0
  34. package/src/Component/Process/index.js +5 -0
  35. package/src/Component/Welcome/Input.js +50 -0
  36. package/src/Component/Welcome/Welcome.js +26 -0
  37. package/src/Component/Welcome/index.js +5 -0
  38. package/src/Component/index.js +9 -0
  39. package/src/Frame/Frame.js +591 -0
  40. package/src/Frame/Frame.test.js +429 -0
  41. package/src/Frame/Props.js +102 -0
  42. package/src/Locale.js +119 -0
  43. package/src/Model/User/User.js +56 -0
  44. package/src/Model/index.js +7 -0
  45. package/src/README.md.js +371 -0
  46. package/src/StdIn.js +111 -0
  47. package/src/StdOut.js +99 -0
  48. package/src/View/RenderOptions.js +48 -0
  49. package/src/View/View.js +289 -0
  50. package/src/View/View.test.js +77 -0
  51. package/src/core/Form/Form.js +289 -0
  52. package/src/core/Form/Form.test.js +116 -0
  53. package/src/core/Form/Input.js +116 -0
  54. package/src/core/Form/Input.test.js +58 -0
  55. package/src/core/Form/Message.js +86 -0
  56. package/src/core/Form/Message.test.js +54 -0
  57. package/src/core/Form/index.js +11 -0
  58. package/src/core/InputAdapter.js +41 -0
  59. package/src/core/InputAdapter.test.js +35 -0
  60. package/src/core/Message/InputMessage.js +119 -0
  61. package/src/core/Message/InputMessage.test.js +45 -0
  62. package/src/core/Message/Message.js +77 -0
  63. package/src/core/Message/Message.test.js +58 -0
  64. package/src/core/Message/OutputMessage.js +143 -0
  65. package/src/core/Message/OutputMessage.test.js +61 -0
  66. package/src/core/Message/index.js +7 -0
  67. package/src/core/OutputAdapter.js +50 -0
  68. package/src/core/OutputAdapter.test.js +35 -0
  69. package/src/core/Stream.js +71 -0
  70. package/src/core/Stream.test.js +78 -0
  71. package/src/core/StreamEntry.js +59 -0
  72. package/src/core/index.js +13 -0
  73. package/src/functions.js +38 -0
  74. package/src/index.js +34 -0
  75. package/src/index.test.js +14 -0
  76. package/src/models/SimpleUser.js +18 -0
  77. package/stories/App/AppView.js +15 -0
  78. package/stories/App/AppView.test.js +22 -0
  79. package/stories/App/RenderOptions.js +14 -0
  80. package/stories/nodejs/interface.test.js +27 -0
  81. package/system.md +187 -0
  82. package/system1.md +137 -0
  83. package/task.md +181 -0
  84. package/tsconfig.json +23 -0
  85. package/types/App/Command/Options.d.ts +46 -0
  86. package/types/App/Command/index.d.ts +8 -0
  87. package/types/App/Core/CoreApp.d.ts +70 -0
  88. package/types/App/Core/UI.d.ts +49 -0
  89. package/types/App/Core/Widget.d.ts +40 -0
  90. package/types/App/Core/index.d.ts +10 -0
  91. package/types/App/Scenario.d.ts +26 -0
  92. package/types/App/User/Command/Message.d.ts +30 -0
  93. package/types/App/User/Command/Options.d.ts +27 -0
  94. package/types/App/User/Command/index.d.ts +8 -0
  95. package/types/App/User/UserApp.d.ts +31 -0
  96. package/types/App/User/UserUI.d.ts +18 -0
  97. package/types/App/User/index.d.ts +12 -0
  98. package/types/App/index.d.ts +14 -0
  99. package/types/Component/Process/Input.d.ts +48 -0
  100. package/types/Component/Process/Process.d.ts +13 -0
  101. package/types/Component/Process/index.d.ts +4 -0
  102. package/types/Component/Welcome/Input.d.ts +34 -0
  103. package/types/Component/Welcome/Welcome.d.ts +13 -0
  104. package/types/Component/Welcome/index.d.ts +4 -0
  105. package/types/Component/index.d.ts +8 -0
  106. package/types/Frame/Frame.d.ts +186 -0
  107. package/types/Frame/Props.d.ts +77 -0
  108. package/types/Locale.d.ts +55 -0
  109. package/types/Model/User/User.d.ts +36 -0
  110. package/types/Model/index.d.ts +6 -0
  111. package/types/StdIn.d.ts +62 -0
  112. package/types/StdOut.d.ts +52 -0
  113. package/types/View/RenderOptions.d.ts +29 -0
  114. package/types/View/View.d.ts +115 -0
  115. package/types/core/Form/Form.d.ts +123 -0
  116. package/types/core/Form/Input.d.ts +69 -0
  117. package/types/core/Form/Message.d.ts +28 -0
  118. package/types/core/Form/index.d.ts +5 -0
  119. package/types/core/InputAdapter.d.ts +28 -0
  120. package/types/core/Message/InputMessage.d.ts +71 -0
  121. package/types/core/Message/Message.d.ts +50 -0
  122. package/types/core/Message/OutputMessage.d.ts +53 -0
  123. package/types/core/Message/index.d.ts +5 -0
  124. package/types/core/OutputAdapter.d.ts +33 -0
  125. package/types/core/Stream.d.ts +27 -0
  126. package/types/core/StreamEntry.d.ts +45 -0
  127. package/types/core/index.d.ts +9 -0
  128. package/types/functions.d.ts +3 -0
  129. package/types/index.d.ts +20 -0
  130. package/types/models/SimpleUser.d.ts +21 -0
  131. package/vitest.config.js +26 -0
@@ -0,0 +1,429 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import { empty, notEmpty } from "@nan0web/types"
4
+ import Frame from "./Frame.js"
5
+ import stringWidth from "string-width"
6
+
7
+ describe("Frame", () => {
8
+ it("should create empty Frame", () => {
9
+ const frame = new Frame()
10
+ assert.ok(empty(frame))
11
+ })
12
+ it("should create non-empty Frame", () => {
13
+ const frame = Frame.from(["Non", "empty"])
14
+ assert.ok(notEmpty(frame))
15
+ })
16
+ it("should print empty zero and false", () => {
17
+ const input = [
18
+ [0, "0", false, "false"],
19
+ [undefined, null, "", {}]
20
+ ]
21
+ const frame = new Frame({ value: input, width: 144, height: 33 })
22
+ assert.ok(notEmpty(frame))
23
+ const rows = input.map(row => row.map(String))
24
+ assert.deepStrictEqual(frame.value, rows)
25
+ frame.render()
26
+ assert.equal(
27
+ frame.imprint,
28
+ rows.map(
29
+ row => row.join("")
30
+ ).map(
31
+ row => row + " ".repeat(144 - stringWidth(row))
32
+ ).join("\n")
33
+ )
34
+ })
35
+ it("should print welcome", () => {
36
+ const input = [
37
+ ["Welcome", " ", "World", "!"],
38
+ ["What can we do today great?"],
39
+ ]
40
+ const rows = input.map(
41
+ row => row.join("")
42
+ ).map(
43
+ row => row + " ".repeat(144 - stringWidth(row))
44
+ )
45
+ const frame = new Frame({ value: input, width: 144, height: 33 })
46
+ frame.render()
47
+ assert.equal(frame.imprint, rows.join("\n"))
48
+ })
49
+ it("should transform value", () => {
50
+ const frame = Frame.from(["Welcome"])
51
+ const t = v => "Вітання"
52
+ const transformed = frame.transform(t)
53
+ assert.deepStrictEqual(transformed.value, [["Вітання"]])
54
+ })
55
+ it("should print special utf characters and fit the width", () => {
56
+ const input = [
57
+ ["你好", "世界"],
58
+ ["Привіт", "Світ"],
59
+ ["こんにちは", "世界"],
60
+ ]
61
+ const frame = Frame.from(input)
62
+ frame.render()
63
+ const imprintLines = frame.imprint.split("\n")
64
+ imprintLines.forEach(line => {
65
+ assert.ok(stringWidth(line) <= 144)
66
+ })
67
+ assert.deepStrictEqual(frame.value, input)
68
+ })
69
+ it("should render table with padding", () => {
70
+ const rows = [
71
+ ["gpt-4.1", 1_047_576],
72
+ ["gpt-4o", 128_000],
73
+ ]
74
+ const table = Frame.table({ padding: 3 })(rows)
75
+ assert.deepStrictEqual(table, [
76
+ ["gpt-4.1 ", "1047576"],
77
+ ["gpt-4o ", "128000"],
78
+ ])
79
+ })
80
+ it("should render with replace method and fill spaces", () => {
81
+ const input = [["Test"]]
82
+ const frame = new Frame({ value: input, width: 10, height: 3 })
83
+ const output = frame.render({ method: Frame.RenderMethod.REPLACE }).split("\n")
84
+ assert.equal(output.length, 3)
85
+ assert.equal(output[0].length - `\x1b[0;0H`.length, 10)
86
+ assert.equal(output[1].length, 10)
87
+ assert.equal(output[2].length, 10)
88
+ })
89
+ it("should render with append method over previous frame", () => {
90
+ const input = [["Append"]]
91
+ const frame = new Frame({ value: input, width: 10, height: 3 })
92
+ const output = frame.render({ method: "append" }).split("\n")
93
+ assert.ok(output.length <= 3)
94
+ assert.ok(output[0].length <= 10)
95
+ })
96
+ it("should render visible method", () => {
97
+ const input = [["Visible"]]
98
+ const frame = new Frame({ value: input, width: 10, height: 3 })
99
+ const output = frame.render({ method: "visible" }).split("\n")
100
+ assert.ok(output.length <= 3)
101
+ })
102
+ it("should handle cell options with style objects", () => {
103
+ const input = [
104
+ ["<b>Hello</b>", "<i>World</i>"],
105
+ ["<fg=red>Red</>", "<bg=#00ff00>Green background</>"]
106
+ ]
107
+ const frame = new Frame({ value: input, width: 20, height: 4 })
108
+ const output = frame.render()
109
+ assert.ok(output.split("\n").length <= 4)
110
+ })
111
+ it("should handle row options with style objects", () => {
112
+ const input = [
113
+ ["Name", "Age"],
114
+ ["John", 30],
115
+ ["<u>Underlined</u>", "<s>Strikethrough</s>", { color: "blue" }]
116
+ ]
117
+ const frame = new Frame({ value: input, width: 30, height: 4 })
118
+ const output = frame.render()
119
+ assert.ok(output.split("\n").length <= 4)
120
+ })
121
+ it("should handle frame options set by method with XML tags", () => {
122
+ const input = [
123
+ ["<b>Bold</b>", "<i>Italic</i>"],
124
+ ["<fg=red>Red</fg>", "<bg=#0000ff>Blue bg</bg>"]
125
+ ]
126
+ const frame = new Frame({ value: input, width: 20, height: 4 })
127
+ const output = frame.render()
128
+ assert.ok(output.split("\n").length <= 4)
129
+ })
130
+
131
+ it("should render correctly with different window sizes", () => {
132
+ const input = [
133
+ ["Line 1: Hello World!"],
134
+ ["Line 2: Another line"],
135
+ ["Line 3: Yet another line"],
136
+ ["Line 4: More text here"],
137
+ ["Line 5: Last line"]
138
+ ]
139
+
140
+ const sizes = [
141
+ { width: 10, height: 2 },
142
+ { width: 20, height: 3 },
143
+ { width: 50, height: 5 },
144
+ { width: 144, height: 33 },
145
+ ]
146
+
147
+ sizes.forEach(({ width, height }) => {
148
+ const frame = new Frame({ value: input, width, height })
149
+ const output = frame.render({ method: Frame.RenderMethod.APPEND })
150
+ const lines = output.split("\n")
151
+ assert.ok(lines.length <= height)
152
+ lines.forEach(line => {
153
+ assert.ok(stringWidth(line) <= width)
154
+ })
155
+ })
156
+ })
157
+
158
+ // Additional tests for BOF, BOL in different positions with REPLACE, VISIBLE, APPEND
159
+
160
+ it("should handle BOF at start with REPLACE method", () => {
161
+ const input = [
162
+ Frame.BOF,
163
+ ["Line 1"],
164
+ ["Line 2"]
165
+ ]
166
+ const frame = new Frame({ value: input, width: 10, height: 4 })
167
+ const output = frame.render({ method: Frame.RenderMethod.REPLACE })
168
+ assert.ok(output.startsWith(Frame.BOF))
169
+ const lines = output.split("\n")
170
+ assert.equal(lines.length, 4)
171
+ assert.match(lines[1], /^\s*$/) // empty row
172
+ assert.deepStrictEqual(lines.slice(-2), ["Line 1 ", "Line 2 "])
173
+ })
174
+
175
+ it("should handle BOF at end with REPLACE method", () => {
176
+ const input = [
177
+ ["Line 1"],
178
+ ["Line 2"],
179
+ Frame.BOF
180
+ ]
181
+ const frame = new Frame({ value: input, width: 10, height: 4 })
182
+ const output = frame.render({ method: Frame.RenderMethod.REPLACE })
183
+ assert.ok(output.endsWith(Frame.BOF))
184
+ const lines = output.split("\n")
185
+ assert.equal(lines.length, 4)
186
+ assert.deepStrictEqual(lines.slice(0, 2), [
187
+ "Line 1 ",
188
+ "Line 2 "
189
+ ])
190
+ assert.match(lines[2], /^\s*$/) // empty row
191
+ })
192
+
193
+ it("should handle BOF at start with APPEND method", () => {
194
+ const input = [
195
+ Frame.BOF,
196
+ ["Line 1"],
197
+ ["Line 2"]
198
+ ]
199
+ const frame = new Frame({ value: input, width: 10, height: 4 })
200
+ const output = frame.render({ method: Frame.RenderMethod.APPEND })
201
+ assert.ok(output.startsWith(Frame.BOF))
202
+ const lines = output.split("\n")
203
+ assert.ok(lines.length <= 4)
204
+ assert.deepStrictEqual(lines.slice(-2), ["Line 1 ", "Line 2 "])
205
+ })
206
+
207
+ it("should handle BOF at end with APPEND method", () => {
208
+ const input = [
209
+ ["Line 1"],
210
+ ["Line 2"],
211
+ Frame.BOF
212
+ ]
213
+ const frame = new Frame({ value: input, width: 10, height: 4 })
214
+ const output = frame.render({ method: Frame.RenderMethod.APPEND })
215
+ assert.ok(output.endsWith(Frame.BOF))
216
+ const lines = output.split("\n")
217
+ assert.equal(lines.length, 4)
218
+ assert.deepStrictEqual(lines.slice(1, 3), ["Line 2 ", ""])
219
+ })
220
+
221
+ it("should handle BOF at start with VISIBLE method", () => {
222
+ const input = [
223
+ Frame.BOF,
224
+ ["Line 1"],
225
+ ["Line 2"]
226
+ ]
227
+ const frame = new Frame({ value: input, width: 10, height: 4 })
228
+ const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
229
+ assert.ok(output.startsWith(`\x1b[1A`))
230
+ const lines = output.split("\n")
231
+ assert.ok(lines.length <= 2)
232
+ assert.deepStrictEqual(lines, [
233
+ Frame.cursorUp() + Frame.CLEAR_LINE + "\r" + "Line 1",
234
+ Frame.CLEAR_LINE + "\r" + "Line 2"
235
+ ])
236
+ })
237
+
238
+ it("should handle BOF at end with VISIBLE method", () => {
239
+ const input = [
240
+ ["Line 1"],
241
+ ["Line 2"],
242
+ Frame.BOF
243
+ ]
244
+ const frame = new Frame({ value: input, width: 10, height: 4 })
245
+ const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
246
+ assert.ok(output.startsWith(Frame.CLEAR_LINE + "\r"))
247
+ const lines = output.split("\n")
248
+ assert.ok(lines.length <= 2)
249
+ assert.deepStrictEqual(lines, [
250
+ Frame.CLEAR_LINE + "\r" + "Line 1",
251
+ Frame.CLEAR_LINE + "\r" + "Line 2" + Frame.cursorUp(1)
252
+ ])
253
+ })
254
+
255
+ it("should handle BOL in lines with REPLACE method", () => {
256
+ const input = [
257
+ ["Line 1" + Frame.BOL],
258
+ ["Line 2"]
259
+ ]
260
+ const frame = new Frame({ value: input, width: 10, height: 3 })
261
+ const output = frame.render({ method: Frame.RenderMethod.REPLACE })
262
+ const lines = output.split("\n")
263
+ assert.ok(lines.some(line => line.includes(Frame.BOL)))
264
+ assert.equal(lines.length, 3)
265
+ })
266
+
267
+ it("should handle BOL in lines with APPEND method", () => {
268
+ const input = [
269
+ ["Line 1" + Frame.BOL],
270
+ ["Line 2"]
271
+ ]
272
+ const frame = new Frame({ value: input, width: 10, height: 3 })
273
+ const output = frame.render({ method: Frame.RenderMethod.APPEND })
274
+ const lines = output.split("\n")
275
+ assert.ok(lines.some(line => line.includes(Frame.BOL)))
276
+ assert.ok(lines.length <= 3)
277
+ })
278
+
279
+ it("should handle BOL in lines with VISIBLE method", () => {
280
+ const input = [
281
+ ["Line 1" + Frame.BOL],
282
+ ["Line 2"]
283
+ ]
284
+ const frame = new Frame({ value: input, width: 10, height: 3 })
285
+ const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
286
+ const lines = output.split("\n")
287
+ assert.ok(lines.some(line => line.includes(Frame.BOL)))
288
+ assert.ok(lines.length <= 3)
289
+ })
290
+ })
291
+
292
+ describe("Frame.RenderMethod", () => {
293
+ const table = Frame.table({
294
+ aligns: ["r"],
295
+ padding: 2,
296
+ })([
297
+ ["Usage: node memory-usage.js", "", ""],
298
+ ["[process_name]", "", "Name of the process to check memory usage for"],
299
+ ["-n number_of_processes", "", "Number of top memory consumers to show (default: 9)"],
300
+ ["--help", "", "Show this help message"],
301
+ ])
302
+ const frame = new Frame({
303
+ value: [...table, ""],
304
+ renderMethod: Frame.RenderMethod.REPLACE,
305
+ })
306
+
307
+ it("should render with REPLACE method", () => {
308
+ const output = frame.render({
309
+ method: Frame.RenderMethod.REPLACE,
310
+ })
311
+ assert.equal(typeof output, "string")
312
+ const lines = output.split("\n")
313
+ assert.ok(lines.length > 0)
314
+ lines.forEach(line => {
315
+ assert.ok(line.length <= (frame.width < 0 ? 144 : frame.width))
316
+ })
317
+ })
318
+
319
+ it("should render with APPEND method", () => {
320
+ const output = frame.render({ method: Frame.RenderMethod.APPEND })
321
+ assert.equal(typeof output, "string")
322
+ const lines = output.split("\n")
323
+ const height = frame.height < 0 ? 10 : frame.height
324
+ const width = frame.width < 0 ? 144 : frame.width
325
+ assert.ok(lines.length <= height)
326
+ lines.forEach(line => {
327
+ assert.ok(line.length <= width)
328
+ })
329
+ })
330
+
331
+ it("should render with VISIBLE method", () => {
332
+ const output = frame.render({ method: Frame.RenderMethod.VISIBLE })
333
+ assert.equal(typeof output, "string")
334
+ const lines = output.split("\n")
335
+ const height = frame.height < 0 ? 10 : frame.height
336
+ const width = frame.width < 0 ? 144 : frame.width
337
+ assert.ok(lines.length <= height)
338
+ lines.forEach(line => {
339
+ assert.ok(line.length <= width)
340
+ })
341
+ })
342
+ })
343
+
344
+ describe("Frame (3rd party deprecations)", () => {
345
+ it("should detect extra lines above BOF and allow for offset rendering", () => {
346
+ // Simulate a terminal with 2 extra lines above (e.g., from warnings)
347
+ const extraLines = 2
348
+ const selectBox = [
349
+ Frame.BOF,
350
+ ["Select a command to run"],
351
+ ["[+]draw"],
352
+ [" - send"],
353
+ [" - stats"],
354
+ [" - analyze"],
355
+ [" - extract"]
356
+ ]
357
+ const frame = new Frame({ value: selectBox, width: 30, height: 8 })
358
+ const output = frame.render({ method: Frame.RenderMethod.REPLACE })
359
+ // The output should start with BOF and have the select box content
360
+ assert.ok(output.startsWith(Frame.BOF))
361
+ const lines = output.split("\n")
362
+ // Simulate that the select box is rendered at the top, but if there are extra lines above,
363
+ // the select box will be shifted down by `extraLines`
364
+ // To check for this, we can simulate a "screen" with extra lines and see where the select box appears
365
+ const screen = [
366
+ "DeprecationWarning: ...", // extra line 1
367
+ "Some other warning...", // extra line 2
368
+ ...lines
369
+ ]
370
+ // The select box should appear at index `extraLines` (after the warnings)
371
+ assert.ok(screen[extraLines + 2].includes("Select a command to run"))
372
+ assert.ok(screen[extraLines + 3].includes("[+]draw"))
373
+ assert.ok(screen[extraLines + 4].includes("- send"))
374
+ })
375
+
376
+ it("should allow for offset rendering by prepending empty lines before BOF", () => {
377
+ // If you want to compensate for extra lines, you can prepend empty lines before BOF
378
+ const extraLines = 2
379
+ const selectBox = [
380
+ "",
381
+ "",
382
+ Frame.BOF,
383
+ ["Select a command to run"],
384
+ ["[+]draw"],
385
+ [" - send"],
386
+ [" - stats"],
387
+ [" - analyze"],
388
+ [" - extract"]
389
+ ]
390
+ const frame = new Frame({ value: selectBox, width: 30, height: 10 })
391
+ const output = frame.render({ method: Frame.RenderMethod.REPLACE })
392
+ const lines = output.split("\n")
393
+ // The select box should now appear after the empty lines
394
+ assert.equal(lines[4], "Select a command to run".padEnd(30))
395
+ assert.equal(lines[5], "[+]draw".padEnd(30))
396
+ })
397
+
398
+ it("should render select box correctly even with extra lines above (simulate terminal shift)", () => {
399
+ // Simulate a terminal with 2 extra lines above (e.g., from warnings)
400
+ const extraLines = 2
401
+ const selectBox = [
402
+ Frame.BOF,
403
+ ["Select a command to run"],
404
+ ["[+]draw"],
405
+ [" - send"],
406
+ [" - stats"],
407
+ [" - analyze"],
408
+ [" - extract"]
409
+ ]
410
+ const frame = new Frame({ value: selectBox, width: 30, height: 8 })
411
+ const output = frame.render({ method: Frame.RenderMethod.REPLACE })
412
+ const lines = output.split("\n")
413
+ // Simulate a "screen" with extra lines above
414
+ const screen = [
415
+ "DeprecationWarning: ...",
416
+ "Some other warning...",
417
+ ...lines
418
+ ]
419
+ // The select box should still be visible at the correct offset
420
+ assert.equal(screen[extraLines].trim(), `\x1b[0;0H`)
421
+ assert.equal(screen[extraLines + 1].trim(), "")
422
+ assert.equal(screen[extraLines + 2].trim(), "Select a command to run")
423
+ assert.equal(screen[extraLines + 3].trim(), "[+]draw")
424
+ assert.equal(screen[extraLines + 4].trim(), "- send")
425
+ assert.equal(screen[extraLines + 5].trim(), "- stats")
426
+ assert.equal(screen[extraLines + 6].trim(), "- analyze")
427
+ assert.equal(screen[extraLines + 7].trim(), "- extract")
428
+ })
429
+ })
@@ -0,0 +1,102 @@
1
+ import { typeOf, ObjectWithAlias } from "@nan0web/types"
2
+
3
+ /**
4
+ * Represents default styling properties for Frame rendering.
5
+ * Every tag must be a separate value in the array of rows/columns.
6
+ * If you want to apply the same props to multiple values, you can use an array of values.
7
+ * If you want to apply different props to multiple values, you can use an object with the props.
8
+ * If you want to apply props to a single value, you can use a string with the props in XML format.
9
+ * Parser checks every atom for its beginning and end and if it's a tag, it applies the props to the value.
10
+ *
11
+ * @example
12
+ * const defaultProps = new FrameProps({
13
+ * color: "red",
14
+ * bgColor: "blue",
15
+ * bold: true,
16
+ * italic: true,
17
+ * underline: true,
18
+ * strikethrough: true,
19
+ * })
20
+ * or by aliases:
21
+ * const defaultProps = new FrameProps({
22
+ * fg: "red",
23
+ * bg: "blue",
24
+ * b: true,
25
+ * i: true,
26
+ * u: true,
27
+ * s: true,
28
+ * })
29
+ * from an array of strings:
30
+ * const rows = [
31
+ * ["Hello", "World"],
32
+ * ["<fg=red>Hello</>", "<bg=blue>World</>"],
33
+ * ["<b>Hello</b>", "<i>World</i>"],
34
+ * ["<u>Hello</u>", "<s>World</s>"],
35
+ * ["<b fg=red>Hello</b>", "<i bg=blue>World</i>"],
36
+ * ["<b i>Hello</b>", "<i b>World</i>"],
37
+ * ["<b i s>Some</b>", ["thing", {b: true, i: true, s: true}]],
38
+ * [["Hello", "World", {b: true}]],
39
+ * ]
40
+ * const defaultProps = new FrameProps(rows)
41
+ */
42
+ class FrameProps extends ObjectWithAlias {
43
+ /**
44
+ * Property aliases for shorthand notation.
45
+ * @type {Record<string, string>}
46
+ */
47
+ static ALIAS = {
48
+ fg: "color",
49
+ bg: "bgColor",
50
+ b: "bold",
51
+ i: "italic",
52
+ u: "underline",
53
+ s: "strikethrough",
54
+ }
55
+
56
+ /** @type {string} Text color */
57
+ color = ""
58
+
59
+ /** @type {string} Background color */
60
+ bgColor = ""
61
+
62
+ /** @type {boolean} Bold text flag */
63
+ bold = false
64
+
65
+ /** @type {boolean} Italic text flag */
66
+ italic = false
67
+
68
+ /** @type {boolean} Underline text flag */
69
+ underline = false
70
+
71
+ /** @type {boolean} Strikethrough text flag */
72
+ strikethrough = false
73
+
74
+ /**
75
+ * @param {object} props - Frame properties
76
+ * @param {string} [props.color=""] - Text color
77
+ * @param {string} [props.bgColor=""] - Background color
78
+ * @param {boolean} [props.bold=false] - Bold text flag
79
+ * @param {boolean} [props.italic=false] - Italic text flag
80
+ * @param {boolean} [props.underline=false] - Underline text flag
81
+ * @param {boolean} [props.strikethrough=false] - Strikethrough text flag
82
+ */
83
+ constructor(props = {}) {
84
+ super()
85
+ const {
86
+ color = "",
87
+ bgColor = "",
88
+ bold = false,
89
+ italic = false,
90
+ underline = false,
91
+ strikethrough = false,
92
+ } = props
93
+ this.color = color
94
+ this.bgColor = bgColor
95
+ this.bold = bold
96
+ this.italic = italic
97
+ this.underline = underline
98
+ this.strikethrough = strikethrough
99
+ }
100
+ }
101
+
102
+ export default FrameProps
package/src/Locale.js ADDED
@@ -0,0 +1,119 @@
1
+ import { typeOf, notEmpty } from "@nan0web/types"
2
+
3
+ /**
4
+ * Handles locale-specific formatting for different data types.
5
+ */
6
+ export default class Locale {
7
+ /** @type {string} Language locale */
8
+ lang
9
+
10
+ /** @type {string} Collation locale */
11
+ collate
12
+
13
+ /** @type {string} Character type locale */
14
+ ctype
15
+
16
+ /** @type {string} Messages locale */
17
+ messages
18
+
19
+ /** @type {string} Monetary locale */
20
+ monetary
21
+
22
+ /** @type {string} Numeric locale */
23
+ numeric
24
+
25
+ /** @type {string} Time locale */
26
+ time
27
+
28
+ /** @type {string} General locale fallback */
29
+ all
30
+
31
+ /**
32
+ * Creates a new Locale instance.
33
+ * @param {object} props - Locale properties or all locale string
34
+ * @param {string} [props.lang=""] - Language locale
35
+ * @param {string} [props.collate=""] - Collation locale
36
+ * @param {string} [props.ctype=""] - Character type locale
37
+ * @param {string} [props.messages=""] - Messages locale
38
+ * @param {string} [props.monetary=""] - Monetary locale
39
+ * @param {string} [props.numeric=""] - Numeric locale
40
+ * @param {string} [props.time=""] - Time locale
41
+ * @param {string} [props.all="uk_UA.UTF-8"] - General locale fallback
42
+ */
43
+ constructor(props = {}) {
44
+ const {
45
+ lang = "",
46
+ collate = "",
47
+ ctype = "",
48
+ messages = "",
49
+ monetary = "",
50
+ numeric = "",
51
+ time = "",
52
+ all = "uk_UA.UTF-8",
53
+ } = props
54
+ this.lang = lang
55
+ this.collate = collate
56
+ this.ctype = ctype
57
+ this.messages = messages
58
+ this.monetary = monetary
59
+ this.numeric = numeric
60
+ this.time = time
61
+ this.all = all
62
+ }
63
+
64
+ /**
65
+ * Formats values according to locale settings.
66
+ * @param {Function} type - Type constructor (Number, String, etc.)
67
+ * @param {object} options - Formatting options
68
+ * @returns {Function|null} Formatting function or null if unsupported type
69
+ */
70
+ format(type, options) {
71
+ if (Number === type || typeOf(Number)(type)) {
72
+ /**
73
+ * new (locales?: LocalesArgument, options?: NumberFormatOptions): NumberFormat;
74
+ * (locales?: LocalesArgument, options?: NumberFormatOptions): NumberFormat;
75
+ * supportedLocalesOf(locales: LocalesArgument, options?: NumberFormatOptions): string[];
76
+ */
77
+ /**
78
+ * localeMatcher?: "lookup" | "best fit" | undefined;
79
+ * style?: NumberFormatOptionsStyle | undefined;
80
+ * currency?: string | undefined;
81
+ * currencyDisplay?: NumberFormatOptionsCurrencyDisplay | undefined;
82
+ * useGrouping?: NumberFormatOptionsUseGrouping | undefined;
83
+ * minimumIntegerDigits?: number | undefined;
84
+ * minimumFractionDigits?: number | undefined;
85
+ * maximumFractionDigits?: number | undefined;
86
+ * minimumSignificantDigits?: number | undefined;
87
+ * maximumSignificantDigits?: number | undefined;
88
+ */
89
+ const locales = [this.numeric, this.all, this.lang].filter(notEmpty)
90
+ return (value) => {
91
+ return new Intl.NumberFormat(locales, options).format(value)
92
+ }
93
+ }
94
+ if ("string" === typeof type) {
95
+ const locales = [this.monetary, this.numeric, this.all, this.lang].filter(notEmpty)
96
+ return (value) => {
97
+ return new Intl.NumberFormat(locales, {
98
+ style: "currency",
99
+ currency: type === "currency" ? options.currency : type,
100
+ ...options,
101
+ }).format(value)
102
+ }
103
+ }
104
+ return null
105
+ }
106
+
107
+ /**
108
+ * @param {any} input
109
+ * @returns {Locale}
110
+ */
111
+ static from(input) {
112
+ if (input instanceof Locale) return input
113
+ if ("string" === typeof input) {
114
+ return new Locale({ all: input })
115
+ }
116
+ return new Locale(input)
117
+ }
118
+ }
119
+
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Represents a user with name and email properties.
3
+ */
4
+ class User {
5
+ /** @type {string} User name */
6
+ name
7
+
8
+ /** @type {string} User email */
9
+ email
10
+
11
+ /**
12
+ * Creates a new User instance.
13
+ * @param {object} props - User properties or name string
14
+ * @param {string} [props.name=""] - User name
15
+ * @param {string} [props.email=""] - User email
16
+ */
17
+ constructor(props = {}) {
18
+ const {
19
+ name = "",
20
+ email = "",
21
+ } = props
22
+ this.name = String(name)
23
+ this.email = String(email)
24
+ }
25
+
26
+ /**
27
+ * Checks if user data is empty (no name and no email).
28
+ * @returns {boolean} True if both name and email are empty, false otherwise
29
+ */
30
+ get empty() {
31
+ return !this.name && !this.email
32
+ }
33
+
34
+ /**
35
+ * Converts user to string representation.
36
+ * @returns {string} User name and email (if exists)
37
+ */
38
+ toString() {
39
+ return [this.name, this.email ? `<${this.email}>` : ""].filter(Boolean).join(" ")
40
+ }
41
+
42
+ /**
43
+ * Creates a User instance from the given props.
44
+ * @param {User|object|string} props - The properties to create from
45
+ * @returns {User} A User instance
46
+ */
47
+ static from(props) {
48
+ if (props instanceof User) return props
49
+ if ("string" === typeof props) {
50
+ return new User({ name: props })
51
+ }
52
+ return new User(props)
53
+ }
54
+ }
55
+
56
+ export default User