@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,50 @@
1
+ import { User } from "../../Model/index.js"
2
+
3
+ /**
4
+ * Represents input data for the Welcome component.
5
+ * Holds user data to display in the welcome message.
6
+ */
7
+ class WelcomeInput {
8
+ /** @type {User} User data for welcome message */
9
+ user
10
+
11
+ /**
12
+ * Creates a new WelcomeInput instance.
13
+ * @param {object} props - Welcome input properties
14
+ * @param {User|object} [props.user=new User()] - User data
15
+ */
16
+ constructor(props = {}) {
17
+ const {
18
+ user = new User(),
19
+ } = props
20
+ this.user = user
21
+ }
22
+
23
+ /**
24
+ * Checks if the input is empty (no user data).
25
+ * @returns {boolean} True if user data is empty, false otherwise
26
+ */
27
+ get empty() {
28
+ return this.user.empty
29
+ }
30
+
31
+ /**
32
+ * Converts the input to a string representation.
33
+ * @returns {string} String representation of the WelcomeInput
34
+ */
35
+ toString() {
36
+ return `<WelcomeInput user=${this.user}>`
37
+ }
38
+
39
+ /**
40
+ * Creates a WelcomeInput instance from the given props.
41
+ * @param {WelcomeInput|object} props - The properties to create from
42
+ * @returns {WelcomeInput} A WelcomeInput instance
43
+ */
44
+ static from(props = {}) {
45
+ if (props instanceof WelcomeInput) return props
46
+ return new WelcomeInput(props)
47
+ }
48
+ }
49
+
50
+ export default WelcomeInput
@@ -0,0 +1,26 @@
1
+ import { empty } from "@nan0web/types"
2
+ import WelcomeInput from "./Input.js"
3
+
4
+ /**
5
+ * Renders a welcome message for a user.
6
+ * @param {WelcomeInput|object} props - Welcome component properties
7
+ * @returns {string[][]} Rendered welcome message as array of strings
8
+ * @throws {Error} If no user data is provided
9
+ */
10
+ function Welcome(props = {}) {
11
+ const input = WelcomeInput.from(props)
12
+ if (empty(input)) {
13
+ throw new Error("User is required")
14
+ }
15
+
16
+ return [
17
+ ["Welcome", " ", input.user.name, "!"],
18
+ ["What can we do today great?"],
19
+ [""],
20
+ ]
21
+ }
22
+
23
+ Welcome.Input = WelcomeInput
24
+ Welcome.ask = async () => ""
25
+
26
+ export default Welcome
@@ -0,0 +1,5 @@
1
+ import Welcome from "./Welcome.js"
2
+ import WelcomeInput from "./Input.js"
3
+
4
+ export default Welcome
5
+ export { WelcomeInput }
@@ -0,0 +1,9 @@
1
+ import Welcome from "./Welcome/index.js"
2
+ import Process from "./Process/index.js"
3
+
4
+ export { Welcome, Process }
5
+
6
+ export default {
7
+ Welcome,
8
+ Process,
9
+ }
@@ -0,0 +1,591 @@
1
+ import stringWidth from "string-width"
2
+ import { to, typeOf, empty } from "@nan0web/types"
3
+ import FrameProps from "./Props.js"
4
+
5
+ export class FrameRenderMethod {
6
+ static APPEND = "append"
7
+ static REPLACE = "replace"
8
+ static VISIBLE = "visible"
9
+ }
10
+
11
+ /**
12
+ * @link https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 - ANSI escape codes
13
+ */
14
+ export default class Frame {
15
+ /** @type {typeof FrameRenderMethod} */
16
+ static RenderMethod = FrameRenderMethod
17
+ static Props = FrameProps
18
+ /** @type {string} End of line */
19
+ static EOL = "\n"
20
+ /** @type {string} Beginning of line */
21
+ static BOL = "\r"
22
+ /** @type {string} Beginning of frame */
23
+ static BOF = "\x1b[0;0H"
24
+ /** @type {string} Hide cursor */
25
+ static HIDE_CURSOR = "\x1b[?25l"
26
+ /** @type {string} Show cursor */
27
+ static SHOW_CURSOR = "\x1b[?25h"
28
+ /** @type {string} Tab */
29
+ static TAB = "\t"
30
+ /** @type {string} Bold */
31
+ static BOLD = "\x1b[1m"
32
+ /** @type {string} Italic */
33
+ static ITALIC = "\x1b[3m"
34
+ /** @type {string} Underline */
35
+ static UNDERLINE = "\x1b[4m"
36
+ /** @type {string} Strikethrough */
37
+ static STRIKETHROUGH = "\x1b[9m"
38
+ /** @type {string} Reset */
39
+ static RESET = "\x1b[0m"
40
+ /** @type {string} Clear line */
41
+ static CLEAR_LINE = '\x1b[2K'
42
+ /**
43
+ * @example
44
+ * ```js
45
+ * new Frame([
46
+ * ["Hello", "World"],
47
+ * [["Hello", { color: "red", bgColor: "#009" }], "World"],
48
+ * ["<b i fg=#900>Hello</b>", "<i>World</i>"],
49
+ * ])
50
+ * ```
51
+ * @type {string[][]|any[][]}
52
+ */
53
+ value
54
+ /** @type {FrameProps} */
55
+ defaultProps
56
+ /** @type {string} */
57
+ imprint
58
+ /** @type {number} */
59
+ width
60
+ /** @type {number} */
61
+ height
62
+ /** @type {string} */
63
+ renderMethod
64
+ /**
65
+ * @param {object} [input]
66
+ * @param {string[]|string[][]} [input.value]
67
+ * @param {number} [input.width]
68
+ * @param {number} [input.height]
69
+ * @param {string} [input.imprint]
70
+ * @param {string} [input.renderMethod]
71
+ * @param {FrameProps} [input.defaultProps]
72
+ */
73
+ constructor(input = {}) {
74
+ // if (typeOf(Array)(input)) {
75
+ // input = { value: input }
76
+ // }
77
+ if (input instanceof Frame) {
78
+ input = { ...input }
79
+ }
80
+ let {
81
+ value = [],
82
+ width = -1,
83
+ height = -1,
84
+ imprint = "",
85
+ renderMethod = "append",
86
+ defaultProps = new FrameProps(),
87
+ } = input
88
+ if (value instanceof Frame) {
89
+ value = value.value
90
+ }
91
+ if (!typeOf(Array)(value)) {
92
+ throw new TypeError([
93
+ "Frame constructor allows only string[] for rows or string[][] for rows with columns",
94
+ "Provided value:", JSON.stringify(value, null, 2),
95
+ ].join("\n"))
96
+ }
97
+ value = value.map(row => {
98
+ if (typeOf(Array)(row)) {
99
+ return row.map(String)
100
+ }
101
+ return [row]
102
+ })
103
+ this.value = value.map(v => Array.isArray(v) ? v : [v])
104
+ this.imprint = String(imprint)
105
+ this.width = width
106
+ this.height = height
107
+ this.renderMethod = renderMethod
108
+ this.defaultProps = defaultProps
109
+ }
110
+ /**
111
+ * Get whether the frame is empty.
112
+ * @returns {boolean} True if the frame has no content.
113
+ */
114
+ get empty() {
115
+ return empty(this.value)
116
+ }
117
+ /**
118
+ * Calculate the visual width of a string.
119
+ * @param {string} str
120
+ * @returns {number} The visual width of the string.
121
+ */
122
+ lengthOf(str) {
123
+ return stringWidth(str)
124
+ }
125
+ /**
126
+ * Render the frame into a string representation.
127
+ * @param {object} [options]
128
+ * @param {string} [options.method] - Render method to use.
129
+ * @param {FrameProps} [options.props] - Properties to apply during rendering.
130
+ * @returns {string} The rendered frame as a string.
131
+ */
132
+ render(options = {}) {
133
+ const {
134
+ method = this.renderMethod,
135
+ props = this.defaultProps,
136
+ } = options
137
+ let rows = this.value.map(row => {
138
+ if (typeOf(Array)(row)) {
139
+ row = row.join("")
140
+ }
141
+ return row
142
+ })
143
+ let spacesOn = ""
144
+ if (Frame.BOF === rows[0]) {
145
+ rows = rows.slice(1)
146
+ spacesOn = "top"
147
+ }
148
+ else if (Frame.BOF === rows[rows.length - 1]) {
149
+ rows = rows.slice(0, -1)
150
+ spacesOn = "bottom"
151
+ }
152
+ if (this.height >= 0 && rows.length > this.height) {
153
+ rows = rows.slice(0, this.height)
154
+ }
155
+ if (this.width >= 0 && rows.length > 0) {
156
+ rows = rows.map(row => {
157
+ if (row.length > this.width) {
158
+ row = row.slice(0, this.width)
159
+ }
160
+ return row
161
+ })
162
+ }
163
+ let carret = ""
164
+ if (method === Frame.RenderMethod.REPLACE) {
165
+ const printedRows = rows.map(
166
+ row => row + " ".repeat(Math.max(0, this.width - this.lengthOf(row)))
167
+ )
168
+ const left = this.height >= 0 ? this.height - rows.length : 0
169
+ const eraser = []
170
+ for (let i = 0; i < left; i++) eraser.push(" ".repeat(this.width))
171
+ carret = Frame.BOF
172
+ if ("bottom" === spacesOn) {
173
+ rows = left > 0 ? [...printedRows, ...eraser] : []
174
+ } else {
175
+ rows = left > 0 ? [...eraser, ...printedRows] : []
176
+ }
177
+ }
178
+ else if (method === Frame.RenderMethod.APPEND) {
179
+ rows = rows.map(row => {
180
+ const used = this.lengthOf(row)
181
+ const left = Math.max(0, this.width - used)
182
+ row = row + " ".repeat(left)
183
+ if (row.length > this.width) row = row.slice(0, this.width)
184
+ return row
185
+ })
186
+ if (this.height >= 0 && rows.length > this.height) {
187
+ rows = rows.slice(0, this.height)
188
+ }
189
+ if (spacesOn) {
190
+ carret = Frame.BOF
191
+ const left = this.height >= 0 ? this.height - rows.length : 0
192
+ const eraser = []
193
+ for (let i = 0; i < left; i++) eraser.push("")
194
+ if (spacesOn === "top") {
195
+ rows = left > 0 ? [...eraser, ...rows] : []
196
+ }
197
+ else if (spacesOn === "bottom") {
198
+ rows = left > 0 ? [...rows, ...eraser] : []
199
+ }
200
+ }
201
+ }
202
+ else if (method === Frame.RenderMethod.VISIBLE) {
203
+ // Move cursor up # lines (Math.max(0, Math.min(rows.length, height))) before rendering
204
+ if (spacesOn) {
205
+ let moveUpLines = Math.max(0, Math.min(rows.length, this.height >= 0 ? this.height : rows.length))
206
+ if (moveUpLines > 0) {
207
+ --moveUpLines
208
+ }
209
+ carret = Frame.cursorUp(moveUpLines)
210
+ }
211
+ rows = rows.map(
212
+ row => Frame.clearLine("\r") + row
213
+ )
214
+ }
215
+ else {
216
+ if (spacesOn) {
217
+ carret = Frame.BOF
218
+ const left = this.height >= 0 ? this.height - rows.length : 0
219
+ const eraser = []
220
+ for (let i = 0; i < left; i++) eraser.push("")
221
+ if (spacesOn === "top") {
222
+ rows = left > 0 ? [...eraser, ...rows] : []
223
+ }
224
+ else if (spacesOn === "bottom") {
225
+ rows = left > 0 ? [...rows, ...eraser] : []
226
+ }
227
+ }
228
+ }
229
+ if ("bottom" === spacesOn) {
230
+ this.imprint = rows.join("\n") + carret
231
+ } else {
232
+ this.imprint = carret + rows.join("\n")
233
+ }
234
+ return this.imprint
235
+ }
236
+ #render1(options = {}) {
237
+ const {
238
+ method = this.renderMethod,
239
+ props = this.defaultProps,
240
+ } = options
241
+
242
+ /**
243
+ * Helper to apply CLI style codes.
244
+ * @param {string} str
245
+ * @param {object} style
246
+ * @returns {string}
247
+ */
248
+ function applyStyle(str, style = {}) {
249
+ let out = str
250
+ let prefix = ""
251
+ let suffix = Frame.RESET
252
+
253
+ if (style.bold) prefix += Frame.BOLD
254
+ if (style.italic) prefix += Frame.ITALIC
255
+ if (style.underline) prefix += Frame.UNDERLINE
256
+ if (style.strikethrough) prefix += Frame.STRIKETHROUGH
257
+
258
+ // Color
259
+ if (style.color) {
260
+ const color = style.color
261
+ if (/^#[0-9a-f]{3,6}$/i.test(color)) {
262
+ // 24-bit color
263
+ const hex = color.replace("#", "")
264
+ const rgb = hex.length === 3
265
+ ? [0, 1, 2].map(i => parseInt(hex[i] + hex[i], 16))
266
+ : [0, 2, 4].map(i => parseInt(hex.slice(i, i + 2), 16))
267
+ prefix += `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`
268
+ } else if (/^\d+$/.test(color)) {
269
+ prefix += `\x1b[38;5;${color}m`
270
+ } else {
271
+ // Named color, map to 8-bit
272
+ const map = {
273
+ black: 30, red: 31, green: 32, yellow: 33, blue: 34,
274
+ magenta: 35, cyan: 36, white: 37, gray: 90, grey: 90,
275
+ brightRed: 91, brightGreen: 92, brightYellow: 93, brightBlue: 94,
276
+ brightMagenta: 95, brightCyan: 96, brightWhite: 97,
277
+ }
278
+ if (map[color]) prefix += `\x1b[${map[color]}m`
279
+ }
280
+ }
281
+ // BgColor
282
+ if (style.bgColor) {
283
+ const color = style.bgColor
284
+ if (/^#[0-9a-f]{3,6}$/i.test(color)) {
285
+ const hex = color.replace("#", "")
286
+ const rgb = hex.length === 3
287
+ ? [0, 1, 2].map(i => parseInt(hex[i] + hex[i], 16))
288
+ : [0, 2, 4].map(i => parseInt(hex.slice(i, i + 2), 16))
289
+ prefix += `\x1b[48;2;${rgb[0]};${rgb[1]};${rgb[2]}m`
290
+ } else if (/^\d+$/.test(color)) {
291
+ prefix += `\x1b[48;5;${color}m`
292
+ } else {
293
+ const map = {
294
+ black: 40, red: 41, green: 42, yellow: 43, blue: 44,
295
+ magenta: 45, cyan: 46, white: 47, gray: 100, grey: 100,
296
+ brightRed: 101, brightGreen: 102, brightYellow: 103, brightBlue: 104,
297
+ brightMagenta: 105, brightCyan: 106, brightWhite: 107,
298
+ }
299
+ if (map[color]) prefix += `\x1b[${map[color]}m`
300
+ }
301
+ }
302
+ return prefix ? prefix + out + suffix : out
303
+ }
304
+
305
+ /**
306
+ * Merge style objects, rightmost has priority.
307
+ * @param {...object} styles
308
+ * @returns {object}
309
+ */
310
+ function mergeStyles(...styles) {
311
+ return Object.assign({}, ...styles)
312
+ }
313
+
314
+ /**
315
+ * Parse cell for value and style.
316
+ * @param {any} cell
317
+ * @param {object} inherited
318
+ * @returns {{text: string, style: object}}
319
+ */
320
+ function parseCell(cell, inherited = {}) {
321
+ if (typeOf(Array)(cell)) {
322
+ if (cell.length === 2 && typeOf(Object)(cell[1])) {
323
+ return { text: String(cell[0]), style: mergeStyles(inherited, cell[1]) }
324
+ }
325
+ return { text: cell.map(c => parseCell(c, inherited).text).join(""), style: inherited }
326
+ }
327
+ if (typeOf(Object)(cell)) {
328
+ return { text: "", style: mergeStyles(inherited, cell) }
329
+ }
330
+ if (typeof cell === "string" && cell.startsWith("<") && cell.endsWith(">")) {
331
+ // Simple XML-like tag parser for <b>, <i>, <u>, <s>, <fg=...>, <bg=...>
332
+ let text = cell
333
+ let style = { ...inherited }
334
+ const tagPattern = /<([bius]|fg|bg)(?:=([#\w\d]+))?>|<\/([bius]|fg|bg)>/gi
335
+ let stack = []
336
+ let result = ""
337
+ let lastIndex = 0
338
+ let m
339
+ while ((m = tagPattern.exec(cell))) {
340
+ result += cell.slice(lastIndex, m.index)
341
+ lastIndex = tagPattern.lastIndex
342
+ if (m[1]) {
343
+ // Opening tag
344
+ let tag = m[1]
345
+ let val = m[2]
346
+ let newStyle = { ...stack.length ? stack[stack.length - 1] : style }
347
+ switch (tag) {
348
+ case "b": newStyle.bold = true; break
349
+ case "i": newStyle.italic = true; break
350
+ case "u": newStyle.underline = true; break
351
+ case "s": newStyle.strikethrough = true; break
352
+ case "fg": newStyle.color = val; break
353
+ case "bg": newStyle.bgColor = val; break
354
+ }
355
+ stack.push(newStyle)
356
+ } else if (m[3]) {
357
+ // Closing tag
358
+ stack.pop()
359
+ }
360
+ }
361
+ result += cell.slice(lastIndex)
362
+ let finalStyle = stack.length ? stack[stack.length - 1] : style
363
+ return { text: result, style: finalStyle }
364
+ }
365
+ return { text: String(cell), style: inherited }
366
+ }
367
+
368
+ // Determine frame-level style
369
+ let frameStyle = {}
370
+ if (typeOf(Array)(this.value) && this.value.length && typeOf(Object)(this.value[this.value.length - 1])) {
371
+ frameStyle = this.value[this.value.length - 1]
372
+ }
373
+
374
+ let rows = this.value
375
+ .filter(row => !(typeOf(Object)(row) && !typeOf(Array)(row)))
376
+ .map(row => {
377
+ let rowStyle = frameStyle
378
+ let cells = row
379
+ if (typeOf(Array)(row) && row.length && typeOf(Object)(row[row.length - 1])) {
380
+ rowStyle = mergeStyles(frameStyle, row[row.length - 1])
381
+ cells = row.slice(0, -1)
382
+ }
383
+ if (!typeOf(Array)(cells)) cells = [cells]
384
+ let styled = cells.map(cell => {
385
+ const { text, style } = parseCell(cell, mergeStyles(props, rowStyle))
386
+ return applyStyle(text, style)
387
+ })
388
+ return styled.join("")
389
+ })
390
+
391
+ if (method === FrameRenderMethod.REPLACE) {
392
+ let emptyRows = rows.map(row => " ".repeat(this.lengthOf(row)))
393
+ if (rows.length > this.height) {
394
+ emptyRows = emptyRows.slice(0, this.height)
395
+ rows = rows.slice(0, this.height)
396
+ }
397
+ rows = [
398
+ ...emptyRows,
399
+ Frame.BOF,
400
+ ...rows,
401
+ ]
402
+ }
403
+ else if (method === FrameRenderMethod.APPEND) {
404
+ rows = rows.map(row => {
405
+ const used = this.lengthOf(row)
406
+ const left = this.width - used
407
+ row = row + " ".repeat(Math.max(0, left))
408
+ if (stringWidth(row) > this.width) {
409
+ let acc = ""
410
+ let w = 0
411
+ for (let ch of row) {
412
+ let chW = stringWidth(ch)
413
+ if (w + chW > this.width) break
414
+ acc += ch
415
+ w += chW
416
+ }
417
+ row = acc
418
+ }
419
+ return row
420
+ })
421
+ if (rows.length > this.height) {
422
+ rows = rows.slice(0, this.height)
423
+ }
424
+ }
425
+ else if (method === FrameRenderMethod.VISIBLE) {
426
+ const moveUpLines = Math.max(0, Math.min(rows.length, this.height >= 0 ? this.height : rows.length))
427
+ rows = [
428
+ `\x1b[${moveUpLines}A${Frame.BOF}`,
429
+ ...rows,
430
+ ]
431
+ if (rows.length > this.height && this.height >= 0) {
432
+ rows = rows.slice(0, this.height + 1) // +1 for the cursor move line
433
+ }
434
+ }
435
+ else {
436
+ if (rows.length > this.height) {
437
+ rows = rows.slice(0, this.height)
438
+ }
439
+ rows = [
440
+ Frame.BOF,
441
+ ...rows,
442
+ ]
443
+ }
444
+
445
+ this.imprint = rows.join("\n")
446
+ return this.imprint
447
+ }
448
+ /**
449
+ * Convert the frame to its string representation.
450
+ * @returns {string} The frame's imprint.
451
+ */
452
+ toString() {
453
+ return this.imprint
454
+ }
455
+ /**
456
+ * Transform each cell in the frame using a function.
457
+ * @param {Function} fn - Function to apply to each cell.
458
+ * @returns {Frame} A new Frame with transformed values.
459
+ */
460
+ transform(fn) {
461
+ const value = this.value.map(
462
+ row => row.map(fn)
463
+ )
464
+ return new Frame({ ...this, value })
465
+ }
466
+ /**
467
+ * Set the window size for the frame.
468
+ * @param {number} width - The width of the window.
469
+ * @param {number} height - The height of the window.
470
+ */
471
+ setWindowSize(width, height) {
472
+ this.width = Math.max(0, Number(width))
473
+ this.height = Math.max(0, Number(height))
474
+ this.render()
475
+ }
476
+ /**
477
+ * Check if a value can be used to create a Frame instance.
478
+ * @param {*} value - Value to check.
479
+ * @returns {boolean} True if the value is valid for Frame creation.
480
+ */
481
+ static is(value) {
482
+ try {
483
+ new Frame(value)
484
+ return true
485
+ } catch {
486
+ return false
487
+ }
488
+ }
489
+ /**
490
+ * Create a Frame instance from input.
491
+ * @param {*} input - Input value to convert.
492
+ * @returns {Frame} A new Frame instance.
493
+ */
494
+ static from(input) {
495
+ if (input instanceof Frame) return input
496
+ if (input?.value instanceof Frame) return new Frame(to(Object)(input.value))
497
+ if ("string" === typeof input) input = [input]
498
+ if (Array.isArray(input)) return new Frame({ value: input })
499
+ return new Frame(input)
500
+ }
501
+ /**
502
+ * Create a function to space columns based on options.
503
+ * @param {object} options - Spacing options.
504
+ * @param {number[]} [options.cols=[]] - Widths of the columns.
505
+ * @param {number} [options.padding=1] - Padding between columns.
506
+ * @param {string[]} [options.aligns=[]] - Alignment for each column ('l' or 'r').
507
+ * @returns {Function} Function that spaces a row.
508
+ */
509
+ static spaces(options = {}) {
510
+ const { cols = [], padding = 1, aligns = [] } = options
511
+ return (row) => (
512
+ row.map((str, i) => {
513
+ const pad = " ".repeat(cols[i] - str.length + padding)
514
+ return aligns[i] === "r" ? pad + str : str + pad
515
+ })
516
+ )
517
+ }
518
+ /**
519
+ *
520
+ * @param {Array} arr
521
+ * @returns {(v) => number[]}
522
+ */
523
+ static weight(arr) {
524
+ return (Fn = v => v) => {
525
+ const cols = []
526
+ arr.forEach(m => {
527
+ Fn(m).forEach((str, i) => {
528
+ if (undefined === cols[i]) cols[i] = 0
529
+ cols[i] = Math.max(String(str).length, cols[i])
530
+ })
531
+ })
532
+ return cols
533
+ }
534
+ }
535
+ /**
536
+ *
537
+ * @param {object} options
538
+ * @param {Function} [options.fn=(fn = v => v)] - Function to calculate weight.
539
+ * @param {number[]} [options.cols=[]] - Widths of the columns.
540
+ * @param {number} [options.padding=1] - The padding between columns.
541
+ * @param {string[]} [options.aligns=[]] - The column aligns: l, r
542
+ * @returns {(arr: []) => string[][]}
543
+ */
544
+ static table(options = {}) {
545
+ const {
546
+ fn = v => v,
547
+ cols: initialCols = [],
548
+ padding = 1,
549
+ aligns = []
550
+ } = options
551
+ return (arr) => {
552
+ let cols = initialCols
553
+ if (empty(cols)) {
554
+ cols = Frame.weight(arr)(fn)
555
+ }
556
+ return arr.map(row => Frame.spaces({ cols, padding, aligns })(row))
557
+ }
558
+ }
559
+ /**
560
+ * Move cursor up by specified lines.
561
+ * @param {number} [lines=1] - Number of lines to move up.
562
+ * @returns {string} ANSI escape code for cursor movement.
563
+ */
564
+ static cursorUp(lines = 1) {
565
+ return `\x1b[${lines}A`
566
+ }
567
+ /**
568
+ * Move cursor down by specified lines.
569
+ * @param {number} [lines=1] - Number of lines to move down.
570
+ * @returns {string} ANSI escape code for cursor movement.
571
+ */
572
+ static cursorDown(lines = 1) {
573
+ return `\x1b[${lines}B`
574
+ }
575
+ /**
576
+ * Clear the current line.
577
+ * @param {string} [str="\r"] - String to append after clearing.
578
+ * @returns {string} ANSI escape code for line clearing followed by the string.
579
+ */
580
+ static clearLine(str = "\r") {
581
+ return Frame.CLEAR_LINE + str
582
+ }
583
+ /**
584
+ * Clear the entire screen.
585
+ * @returns {string} ANSI escape codes for screen clearing.
586
+ */
587
+ static clearScreen() {
588
+ return '\x1b[2J\x1b[0;0H'
589
+ }
590
+
591
+ }