@nan0web/ui 1.0.4 → 1.3.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 (95) hide show
  1. package/README.md +19 -14
  2. package/package.json +6 -4
  3. package/src/App/Command/DepsCommand.js +25 -0
  4. package/src/App/Core/CoreApp.js +18 -17
  5. package/src/App/Core/UI.js +12 -19
  6. package/src/App/Core/Widget.js +6 -10
  7. package/src/App/Core/index.js +3 -3
  8. package/src/App/Scenario.js +4 -4
  9. package/src/App/User/Command/Message.js +2 -29
  10. package/src/App/User/Command/index.js +3 -4
  11. package/src/App/User/UserApp.js +30 -23
  12. package/src/App/User/UserUI.js +2 -2
  13. package/src/App/User/index.js +2 -2
  14. package/src/App/index.js +5 -10
  15. package/src/Component/Process/Input.js +10 -17
  16. package/src/Component/Process/Process.js +3 -5
  17. package/src/Component/Process/index.js +2 -2
  18. package/src/Component/SortableList/SortableList.js +100 -0
  19. package/src/Component/SortableList/index.js +3 -0
  20. package/src/Component/Welcome/Input.js +2 -4
  21. package/src/Component/Welcome/Welcome.js +5 -9
  22. package/src/Component/Welcome/index.js +2 -2
  23. package/src/Component/index.js +5 -3
  24. package/src/Frame/Frame.js +163 -146
  25. package/src/Frame/Props.js +20 -20
  26. package/src/Locale.js +17 -18
  27. package/src/Model/User/User.js +3 -6
  28. package/src/Model/index.js +1 -1
  29. package/src/README.md.js +84 -94
  30. package/src/StdIn.js +8 -12
  31. package/src/StdOut.js +23 -27
  32. package/src/View/RenderOptions.js +1 -1
  33. package/src/View/View.js +42 -38
  34. package/src/core/Error/CancelError.js +2 -2
  35. package/src/core/Error/index.js +3 -5
  36. package/src/core/Flow.js +347 -0
  37. package/src/core/Form/Form.js +35 -33
  38. package/src/core/Form/Input.js +29 -14
  39. package/src/core/Form/Message.js +3 -6
  40. package/src/core/Form/index.js +4 -8
  41. package/src/core/InputAdapter.js +4 -6
  42. package/src/core/Message/Message.js +9 -12
  43. package/src/core/Message/OutputMessage.js +19 -17
  44. package/src/core/Message/index.js +2 -2
  45. package/src/core/OutputAdapter.js +12 -10
  46. package/src/core/Stream.js +4 -3
  47. package/src/core/StreamEntry.js +2 -2
  48. package/src/core/UiAdapter.js +57 -29
  49. package/src/core/index.js +38 -9
  50. package/src/functions.js +8 -15
  51. package/src/index.js +21 -32
  52. package/types/App/Command/DepsCommand.d.ts +16 -0
  53. package/types/App/Command/Options.d.ts +37 -40
  54. package/types/App/Command/index.d.ts +6 -6
  55. package/types/App/Core/CoreApp.d.ts +2 -2
  56. package/types/App/Core/UI.d.ts +6 -7
  57. package/types/App/Core/Widget.d.ts +4 -4
  58. package/types/App/Core/index.d.ts +3 -3
  59. package/types/App/Scenario.d.ts +1 -1
  60. package/types/App/User/Command/Message.d.ts +2 -16
  61. package/types/App/User/Command/Options.d.ts +29 -29
  62. package/types/App/User/Command/index.d.ts +2 -3
  63. package/types/App/User/UserApp.d.ts +5 -5
  64. package/types/App/User/index.d.ts +2 -2
  65. package/types/App/index.d.ts +4 -4
  66. package/types/Component/Process/Process.d.ts +2 -2
  67. package/types/Component/Process/index.d.ts +2 -2
  68. package/types/Component/SortableList/SortableList.d.ts +58 -0
  69. package/types/Component/SortableList/index.d.ts +2 -0
  70. package/types/Component/Welcome/Input.d.ts +1 -1
  71. package/types/Component/Welcome/Welcome.d.ts +1 -1
  72. package/types/Component/Welcome/index.d.ts +2 -2
  73. package/types/Component/index.d.ts +5 -3
  74. package/types/Frame/Frame.d.ts +1 -1
  75. package/types/Frame/Props.d.ts +1 -1
  76. package/types/Model/index.d.ts +1 -1
  77. package/types/StdIn.d.ts +2 -2
  78. package/types/StdOut.d.ts +1 -1
  79. package/types/View/View.d.ts +7 -7
  80. package/types/core/Error/index.d.ts +1 -1
  81. package/types/core/Flow.d.ts +320 -0
  82. package/types/core/Form/Form.d.ts +2 -2
  83. package/types/core/Form/Input.d.ts +15 -4
  84. package/types/core/Form/Message.d.ts +1 -1
  85. package/types/core/Form/index.d.ts +3 -3
  86. package/types/core/InputAdapter.d.ts +2 -2
  87. package/types/core/Intent.d.ts +65 -68
  88. package/types/core/Message/InputMessage.d.ts +65 -65
  89. package/types/core/Message/Message.d.ts +1 -1
  90. package/types/core/Message/OutputMessage.d.ts +1 -1
  91. package/types/core/Message/index.d.ts +2 -2
  92. package/types/core/Stream.d.ts +1 -2
  93. package/types/core/UiAdapter.d.ts +22 -4
  94. package/types/core/index.d.ts +5 -2
  95. package/types/index.d.ts +10 -10
package/src/View/View.js CHANGED
@@ -1,10 +1,10 @@
1
- import { empty, equal, typeOf } from "@nan0web/types"
2
- import Frame, { FrameRenderMethod } from "../Frame/Frame.js"
3
- import Locale from "../Locale.js"
4
- import StdOut from "../StdOut.js"
5
- import StdIn from "../StdIn.js"
6
- import RenderOptions from "./RenderOptions.js"
7
- import UiMessage from "../core/Message/Message.js"
1
+ import { empty, equal, typeOf } from '@nan0web/types'
2
+ import Frame, { FrameRenderMethod } from '../Frame/Frame.js'
3
+ import Locale from '../Locale.js'
4
+ import StdOut from '../StdOut.js'
5
+ import StdIn from '../StdIn.js'
6
+ import RenderOptions from './RenderOptions.js'
7
+ import UiMessage from '../core/Message/Message.js'
8
8
 
9
9
  /**
10
10
  * @typedef {Object} ComponentFn
@@ -54,7 +54,7 @@ export default class View {
54
54
  stdout = new StdOut(),
55
55
  startedAt = Date.now(),
56
56
  frame = new Frame(),
57
- locale = Locale.from("uk-UA"),
57
+ locale = Locale.from('uk-UA'),
58
58
  vocab = new Map(),
59
59
  windowSize = [0, 0],
60
60
  components = new Map(),
@@ -103,14 +103,18 @@ export default class View {
103
103
  const [width, height] = this.getWindowSize()
104
104
  options = this.RenderOptions.from({
105
105
  ...options,
106
- renderMethod: this.renderMethod, width, height,
106
+ renderMethod: this.renderMethod,
107
+ width,
108
+ height,
107
109
  // @ts-ignore
108
110
  })
109
- const renderFn = "function" === typeof shouldRender // no errors.
110
- // const renderFn = typeOf(Function)(shouldRender) // Property 'bind' does not exist on type 'number | boolean | Function'.
111
- ? shouldRender.bind(this) : "string" === typeof shouldRender
112
- ? this.components.get(shouldRender)?.bind(this)
113
- : null
111
+ const renderFn =
112
+ 'function' === typeof shouldRender // no errors.
113
+ ? // const renderFn = typeOf(Function)(shouldRender) // Property 'bind' does not exist on type 'number | boolean | Function'.
114
+ shouldRender.bind(this)
115
+ : 'string' === typeof shouldRender
116
+ ? this.components.get(shouldRender)?.bind(this)
117
+ : null
114
118
 
115
119
  return (value, ...args) => {
116
120
  if (renderFn) {
@@ -137,7 +141,7 @@ export default class View {
137
141
  let frame = Frame.from({ ...options, value })
138
142
  frame = View.fixFrame(frame, options)
139
143
  let clearFrame = false
140
- if (String(frame.value[0] ?? "") === Frame.BOF) {
144
+ if (String(frame.value[0] ?? '') === Frame.BOF) {
141
145
  frame.value = frame.value.slice(1)
142
146
  clearFrame = true
143
147
  }
@@ -179,9 +183,9 @@ export default class View {
179
183
 
180
184
  t(value) {
181
185
  if (typeOf(Array)(value)) {
182
- value = value.map(row => {
186
+ value = value.map((row) => {
183
187
  if (typeOf(Array)(row)) {
184
- return row.map(col => {
188
+ return row.map((col) => {
185
189
  return this.vocab.has(col) ? this.vocab.get(col) : col
186
190
  })
187
191
  }
@@ -193,31 +197,32 @@ export default class View {
193
197
  }
194
198
 
195
199
  debug(...args) {
196
- return this.render(1)(
197
- [StdOut.STYLES.dim,
198
- "Debug: ", args.join(" "), Frame.EOL, StdOut.RESET],
199
- )
200
+ return this.render(1)([StdOut.STYLES.dim, 'Debug: ', args.join(' '), Frame.EOL, StdOut.RESET])
200
201
  }
201
202
 
202
203
  info(...args) {
203
- return this.render(1)(
204
- [StdOut.COLORS.green,
205
- "Info : ", args.join(" "), Frame.EOL, StdOut.RESET],
206
- )
204
+ return this.render(1)([StdOut.COLORS.green, 'Info : ', args.join(' '), Frame.EOL, StdOut.RESET])
207
205
  }
208
206
 
209
207
  warn(...args) {
210
- return this.render(1)(
211
- [StdOut.COLORS.yellow,
212
- "Warn : ", args.join(" "), Frame.EOL, StdOut.RESET],
213
- )
208
+ return this.render(1)([
209
+ StdOut.COLORS.yellow,
210
+ 'Warn : ',
211
+ args.join(' '),
212
+ Frame.EOL,
213
+ StdOut.RESET,
214
+ ])
214
215
  }
215
216
 
216
217
  error(...args) {
217
- return this.render(1)(
218
- [StdOut.COLORS.red, StdOut.STYLES.bold,
219
- "Error: ", args.join(" "), Frame.EOL, StdOut.RESET],
220
- )
218
+ return this.render(1)([
219
+ StdOut.COLORS.red,
220
+ StdOut.STYLES.bold,
221
+ 'Error: ',
222
+ args.join(' '),
223
+ Frame.EOL,
224
+ StdOut.RESET,
225
+ ])
221
226
  }
222
227
 
223
228
  /**
@@ -225,7 +230,7 @@ export default class View {
225
230
  * @param {ComponentFn} component
226
231
  */
227
232
  register(name, component) {
228
- if (undefined === component && "function" === typeof name) {
233
+ if (undefined === component && 'function' === typeof name) {
229
234
  component = name
230
235
  name = component.name
231
236
  }
@@ -260,7 +265,7 @@ export default class View {
260
265
  * @returns {Promise<UiMessage | null>}
261
266
  */
262
267
  async ask(input) {
263
- const name = input.constructor.name.replace(/Input$/, "")
268
+ const name = input.constructor.name.replace(/Input$/, '')
264
269
  const component = this.get(name)
265
270
  if (component) {
266
271
  return await component.ask.apply(this, [input])
@@ -269,8 +274,8 @@ export default class View {
269
274
  do {
270
275
  const answer = await this.stdin.read()
271
276
  result = /** @type {typeof UiMessage} */ (input.constructor).from(answer)
272
- } while (!result.isValid && !result.escaped)
273
- return result.escaped ? null : result
277
+ } while (!result.isValid && !result.head.cancelled)
278
+ return result.head.cancelled ? null : result
274
279
  }
275
280
 
276
281
  /**
@@ -285,5 +290,4 @@ export default class View {
285
290
  // @todo add multiline visibility, for instance extended frame row into rows if it's wider than width.
286
291
  return frame
287
292
  }
288
-
289
293
  }
@@ -1,6 +1,6 @@
1
1
  export default class CancelError extends Error {
2
- constructor(message = "Operation cancelled by user") {
2
+ constructor(message = 'Operation cancelled by user') {
3
3
  super(message)
4
- this.name = "CancelError"
4
+ this.name = 'CancelError'
5
5
  }
6
6
  }
@@ -1,9 +1,7 @@
1
- import CancelError from "./CancelError.js"
1
+ import CancelError from './CancelError.js'
2
2
 
3
- export {
4
- CancelError
5
- }
3
+ export { CancelError }
6
4
 
7
5
  export default {
8
- CancelError
6
+ CancelError,
9
7
  }
@@ -0,0 +1,347 @@
1
+ /**
2
+ * @fileoverview Universal Flow Runner for yield-based UI architecture.
3
+ *
4
+ * The Flow pattern enables "One Logic, Many UI" by separating business logic
5
+ * from presentation. A Flow is an async generator that yields Components,
6
+ * which are then rendered by platform-specific Adapters.
7
+ *
8
+ * @module @nan0web/ui/core/Flow
9
+ */
10
+
11
+ import CancelError from './Error/CancelError.js'
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ // TYPES (JSDoc for pure JavaScript, TypeScript-compatible)
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+
17
+ /**
18
+ * Component types that can be yielded from a Flow.
19
+ *
20
+ * @typedef {'view' | 'prompt' | 'stream' | 'action' | 'flow'} ComponentType
21
+ */
22
+
23
+ /**
24
+ * Base interface for all UI components.
25
+ *
26
+ * @typedef {Object} FlowComponent
27
+ * @property {ComponentType} type - Component type discriminator.
28
+ * @property {string} [name] - Optional component name for debugging.
29
+ */
30
+
31
+ /**
32
+ * Static view component (no user input required).
33
+ * Examples: Alert, Badge, Toast, Table, Text
34
+ *
35
+ * @typedef {Object} ViewComponent
36
+ * @property {'view'} type - Always 'view'.
37
+ * @property {string} name - Component name (e.g., 'Alert', 'Toast').
38
+ * @property {Object} props - Component-specific properties.
39
+ */
40
+
41
+ /**
42
+ * Interactive prompt component (requires user input).
43
+ * Examples: Input, Select, Confirm, Multiselect, Mask
44
+ *
45
+ * @typedef {Object} PromptComponent
46
+ * @property {'prompt'} type - Always 'prompt'.
47
+ * @property {string} name - Component name (e.g., 'Select', 'Input').
48
+ * @property {Object} props - Component-specific properties.
49
+ * @property {string} [props.message] - Prompt message to display.
50
+ * @property {any[]} [props.choices] - Options for Select/Multiselect.
51
+ * @property {Function} [props.validate] - Validation function.
52
+ */
53
+
54
+ /**
55
+ * Streaming component for progress/async operations.
56
+ * Examples: Spinner, ProgressBar, StreamChunk
57
+ *
58
+ * @typedef {Object} StreamComponent
59
+ * @property {'stream'} type - Always 'stream'.
60
+ * @property {string} name - Component name (e.g., 'Spinner', 'Progress').
61
+ * @property {Object} props - Component-specific properties.
62
+ * @property {AsyncIterable} [iterable] - Async iterator for streaming content.
63
+ */
64
+
65
+ /**
66
+ * Action component for physical or side-effect operations.
67
+ * Examples: Move, Sound, Light, Notify
68
+ *
69
+ * @typedef {Object} ActionComponent
70
+ * @property {'action'} type - Always 'action'.
71
+ * @property {string} name - Component name (e.g., 'Move', 'Beep').
72
+ * @property {Object} props - Component-specific properties.
73
+ */
74
+
75
+ /**
76
+ * Result returned by prompt components.
77
+ *
78
+ * @typedef {Object} PromptResult
79
+ * @property {any} value - The value entered/selected by user.
80
+ * @property {boolean} [cancelled] - True if user cancelled the prompt.
81
+ */
82
+
83
+ /**
84
+ * Adapter interface that all platform adapters must implement.
85
+ *
86
+ * @typedef {Object} FlowAdapter
87
+ * @property {(component: ViewComponent) => void | Promise<void>} renderView
88
+ * Renders a static view component.
89
+ * @property {(component: PromptComponent) => Promise<PromptResult>} executePrompt
90
+ * Execute an interactive prompt and returns user input.
91
+ * @property {(component: StreamComponent) => AsyncIterable} streamProgress
92
+ * Handles streaming components.
93
+ * @property {(component: ActionComponent) => Promise<any>} [executeAction]
94
+ * Executes an action component (physical or side-effect).
95
+ */
96
+
97
+ // ═══════════════════════════════════════════════════════════════════════════
98
+ // COMPONENT FACTORIES
99
+ // ═══════════════════════════════════════════════════════════════════════════
100
+
101
+ /**
102
+ * Creates a View component.
103
+ *
104
+ * @param {string} name - Component name (e.g., 'Alert', 'Toast').
105
+ * @param {Object} props - Component properties.
106
+ * @returns {ViewComponent}
107
+ *
108
+ * @example
109
+ * yield View('Alert', { variant: 'success', message: 'Done!' })
110
+ */
111
+ export function View(name, props = {}) {
112
+ return { type: 'view', name, props }
113
+ }
114
+
115
+ /**
116
+ * Creates a Prompt component.
117
+ *
118
+ * @param {string} name - Component name (e.g., 'Select', 'Input').
119
+ * @param {Object} props - Component properties.
120
+ * @returns {PromptComponent}
121
+ *
122
+ * @example
123
+ * const value = yield Prompt('Select', { message: 'Choose:', choices: ['a', 'b'] })
124
+ */
125
+ export function Prompt(name, props = {}) {
126
+ return { type: 'prompt', name, props }
127
+ }
128
+
129
+ /**
130
+ * Creates a Stream component.
131
+ *
132
+ * @param {string} name - Component name (e.g., 'Spinner', 'Progress').
133
+ * @param {Object} props - Component properties.
134
+ * @returns {StreamComponent}
135
+ *
136
+ * @example
137
+ * yield* Stream('Progress', { total: 100, current: 50 })
138
+ */
139
+ export function Stream(name, props = {}) {
140
+ return { type: 'stream', name, props }
141
+ }
142
+
143
+ /**
144
+ * Creates an Action component.
145
+ *
146
+ * @param {string} name - Action name.
147
+ * @param {Object} props - Action properties.
148
+ * @returns {ActionComponent}
149
+ */
150
+ export function Action(name, props = {}) {
151
+ return { type: 'action', name, props }
152
+ }
153
+
154
+ // ═══════════════════════════════════════════════════════════════════════════
155
+ // CONVENIENCE FACTORIES (Pre-defined components)
156
+ // ═══════════════════════════════════════════════════════════════════════════
157
+
158
+ /** @param {Object} props */
159
+ export const Alert = (props) => View('Alert', props)
160
+
161
+ /** @param {Object} props */
162
+ export const Toast = (props) => View('Toast', props)
163
+
164
+ /** @param {Object} props */
165
+ export const Badge = (props) => View('Badge', props)
166
+
167
+ /** @param {Object} props */
168
+ export const Text = (props) => View('Text', props)
169
+
170
+ /** @param {Object} props */
171
+ export const Table = (props) => View('Table', props)
172
+
173
+ /** @param {Object} props */
174
+ export const Input = (props) => Prompt('Input', props)
175
+
176
+ /** @param {Object} props */
177
+ export const Select = (props) => Prompt('Select', props)
178
+
179
+ /** @param {Object} props */
180
+ export const Confirm = (props) => Prompt('Confirm', props)
181
+
182
+ /** @param {Object} props */
183
+ export const Multiselect = (props) => Prompt('Multiselect', props)
184
+
185
+ /** @param {Object} props */
186
+ export const Mask = (props) => Prompt('Mask', props)
187
+
188
+ /** @param {Object} props */
189
+ export const Password = (props) => Prompt('Password', props)
190
+
191
+ /** @param {Object} props */
192
+ export const Spinner = (props) => Stream('Spinner', props)
193
+
194
+ /** @param {Object} props */
195
+ export const Progress = (props) => Stream('Progress', props)
196
+
197
+ /** @param {Object} props */
198
+ export const Beep = (props) => Action('Beep', props)
199
+
200
+ /** @param {Object} props */
201
+ export const Move = (props) => Action('Move', props)
202
+
203
+ // ═══════════════════════════════════════════════════════════════════════════
204
+ // FLOW RUNNER
205
+ // ═══════════════════════════════════════════════════════════════════════════
206
+
207
+ /**
208
+ * Runs a Flow (async generator) with the provided adapter.
209
+ *
210
+ * The Flow runner iterates through yielded components and dispatches them
211
+ * to the appropriate adapter method based on component type.
212
+ *
213
+ * @param {AsyncGenerator} flow - The flow generator to execute.
214
+ * @param {FlowAdapter} adapter - The platform-specific adapter.
215
+ * @param {Object} [options={}] - Additional options.
216
+ * @param {AbortSignal} [options.signal] - Abort signal for cancellation.
217
+ * @returns {Promise<any>} The final return value of the flow.
218
+ *
219
+ * @example
220
+ * async function* loginFlow() {
221
+ * yield Alert({ message: 'Welcome!' })
222
+ * const username = yield Input({ message: 'Username:' })
223
+ * const password = yield Password({ message: 'Password:' })
224
+ * return { username, password }
225
+ * }
226
+ *
227
+ * const result = await runFlow(loginFlow(), cliAdapter)
228
+ */
229
+ export async function runFlow(flow, adapter, options = {}) {
230
+ const { signal } = options
231
+ let nextValue = undefined
232
+
233
+ while (true) {
234
+ // Check for abort
235
+ if (signal?.aborted) {
236
+ throw new CancelError('Flow aborted')
237
+ }
238
+
239
+ // Get next component from flow
240
+ const { value: component, done } = await flow.next(nextValue)
241
+
242
+ if (done) {
243
+ return component // Final return value
244
+ }
245
+
246
+ // Handle null/undefined yields
247
+ if (!component) {
248
+ nextValue = undefined
249
+ continue
250
+ }
251
+
252
+ // Dispatch based on component type
253
+ switch (component.type) {
254
+ case 'view':
255
+ await adapter.renderView?.(component)
256
+ nextValue = undefined
257
+ break
258
+
259
+ case 'prompt': {
260
+ const result = await adapter.executePrompt?.(component)
261
+ if (result?.cancelled) {
262
+ throw new CancelError('User cancelled prompt')
263
+ }
264
+ nextValue = result?.value
265
+ break
266
+ }
267
+
268
+ case 'stream':
269
+ // For streams, we iterate and yield each chunk
270
+ if (adapter.streamProgress) {
271
+ for await (const chunk of adapter.streamProgress(component)) {
272
+ // Optionally allow flow to react to stream chunks
273
+ // by using a specific protocol if needed
274
+ }
275
+ }
276
+ nextValue = undefined
277
+ break
278
+
279
+ case 'action':
280
+ nextValue = await adapter.executeAction?.(component)
281
+ break
282
+
283
+ case 'flow':
284
+ // Nested flow - recursive execution
285
+ nextValue = await runFlow(component.generator, adapter, options)
286
+ break
287
+
288
+ default:
289
+ // Unknown component type - try to render as view
290
+ if (typeof component === 'string') {
291
+ await adapter.renderView?.({ type: 'view', name: 'Text', props: { content: component } })
292
+ } else if (typeof component.toString === 'function') {
293
+ await adapter.renderView?.({
294
+ type: 'view',
295
+ name: 'Text',
296
+ props: { content: String(component) },
297
+ })
298
+ }
299
+ nextValue = undefined
300
+ }
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Wraps a nested flow for composition with yield*.
306
+ *
307
+ * @param {Function} flowFn - Flow generator function.
308
+ * @param {...any} args - Arguments to pass to the flow function.
309
+ * @returns {Object} A flow component.
310
+ *
311
+ * @example
312
+ * async function* mainFlow() {
313
+ * yield Alert({ message: 'Starting...' })
314
+ * const user = yield* flow(loginFlow)
315
+ * yield Alert({ message: `Welcome, ${user.name}!` })
316
+ * }
317
+ */
318
+ export function flow(flowFn, ...args) {
319
+ return { type: 'flow', generator: flowFn(...args) }
320
+ }
321
+
322
+ // ═══════════════════════════════════════════════════════════════════════════
323
+ // EXPORTS
324
+ // ═══════════════════════════════════════════════════════════════════════════
325
+
326
+ export default {
327
+ runFlow,
328
+ flow,
329
+ // Factories
330
+ View,
331
+ Prompt,
332
+ Stream,
333
+ // Components
334
+ Alert,
335
+ Toast,
336
+ Badge,
337
+ Text,
338
+ Table,
339
+ Input,
340
+ Select,
341
+ Confirm,
342
+ Multiselect,
343
+ Mask,
344
+ Password,
345
+ Spinner,
346
+ Progress,
347
+ }
@@ -1,6 +1,6 @@
1
- import Message from "@nan0web/co"
2
- import FormMessage from "./Message.js"
3
- import FormInput from "./Input.js"
1
+ import Message from '@nan0web/co'
2
+ import FormMessage from './Message.js'
3
+ import FormInput from './Input.js'
4
4
 
5
5
  /**
6
6
  * Abstract form for data entry.
@@ -33,8 +33,8 @@ export default class UIForm extends FormMessage {
33
33
  * otherwise returns an error message.
34
34
  */
35
35
  static addValidation(name, fn) {
36
- if (typeof name !== "string" || typeof fn !== "function") {
37
- throw new Error("validation name must be a string and fn must be a function")
36
+ if (typeof name !== 'string' || typeof fn !== 'function') {
37
+ throw new Error('validation name must be a string and fn must be a function')
38
38
  }
39
39
  UIForm._validations[name] = fn
40
40
  }
@@ -51,16 +51,10 @@ export default class UIForm extends FormMessage {
51
51
  constructor(props = {}) {
52
52
  super(props)
53
53
 
54
- const {
55
- title = '',
56
- fields = [],
57
- state = {},
58
- schema = {},
59
- ...rest
60
- } = props
54
+ const { title = '', fields = [], state = {}, schema = {}, ...rest } = props
61
55
 
62
56
  // Normalise fields
63
- this.fields = fields.map(f => FormInput.from(f))
57
+ this.fields = fields.map((f) => FormInput.from(f))
64
58
  this.title = title
65
59
  this.state = { ...state }
66
60
  this.schema = schema
@@ -68,8 +62,8 @@ export default class UIForm extends FormMessage {
68
62
  // Update meta with form data
69
63
  this.meta = {
70
64
  title: this.title,
71
- fields: this.fields.map(f => f.toJSON ? f.toJSON() : f),
72
- initialState: this.state
65
+ fields: this.fields.map((f) => (f.toJSON ? f.toJSON() : f)),
66
+ initialState: this.state,
73
67
  }
74
68
  }
75
69
 
@@ -86,7 +80,7 @@ export default class UIForm extends FormMessage {
86
80
  setData(data) {
87
81
  return new UIForm({
88
82
  ...this,
89
- state: { ...this.state, ...data }
83
+ state: { ...this.state, ...data },
90
84
  })
91
85
  }
92
86
 
@@ -97,7 +91,7 @@ export default class UIForm extends FormMessage {
97
91
  * @returns {FormInput|undefined}
98
92
  */
99
93
  getField(name) {
100
- return this.fields.find(f => f.name === name)
94
+ return this.fields.find((f) => f.name === name)
101
95
  }
102
96
 
103
97
  /**
@@ -122,14 +116,20 @@ export default class UIForm extends FormMessage {
122
116
  const fieldValue = this.state[field.name]
123
117
 
124
118
  // Required validation based on field definition or schema
125
- if (field.required && (fieldValue === '' || fieldValue === null || fieldValue === undefined)) {
119
+ if (
120
+ field.required &&
121
+ (fieldValue === '' || fieldValue === null || fieldValue === undefined)
122
+ ) {
126
123
  errors.set(field.name, 'This field is required')
127
124
  isValid = false
128
125
  return
129
126
  }
130
127
 
131
128
  // Validation via schema (if provided) or field type
132
- const { isValid: fieldValid, errors: fieldErrors } = this.validateField(field.name, fieldValue)
129
+ const { isValid: fieldValid, errors: fieldErrors } = this.validateField(
130
+ field.name,
131
+ fieldValue,
132
+ )
133
133
 
134
134
  if (!fieldValid) {
135
135
  for (const [key, err] of Object.entries(fieldErrors)) {
@@ -241,9 +241,9 @@ export default class UIForm extends FormMessage {
241
241
  return {
242
242
  time: new Date(this.time).toISOString(),
243
243
  title: this.title,
244
- fields: this.fields.map(f => f.toJSON ? f.toJSON() : f),
244
+ fields: this.fields.map((f) => (f.toJSON ? f.toJSON() : f)),
245
245
  state: this.state,
246
- meta: this.meta
246
+ meta: this.meta,
247
247
  }
248
248
  }
249
249
 
@@ -257,19 +257,21 @@ export default class UIForm extends FormMessage {
257
257
  const Class = input.constructor
258
258
  const fields = []
259
259
  for (const [name, value] of Object.entries(input)) {
260
- fields.push(new FormInput({
261
- name,
262
- label: Class[name]?.label ?? Class[`${name}Label`] ?? name,
263
- type: Class[name]?.type ?? Class[`${name}Type`] ?? typeof value,
264
- required: Class[name]?.required ?? Class[`${name}Required`] ?? false,
265
- placeholder: Class[name]?.placeholder ?? Class[`${name}Placeholder`] ?? "",
266
- defaultValue: Class[name]?.defaultValue ?? Class[`${name}Default`] ?? "",
267
- validation: Class[name]?.validation ?? Class[`${name}Validation`] ?? (() => true),
268
- }))
260
+ fields.push(
261
+ new FormInput({
262
+ name,
263
+ label: Class[name]?.label ?? Class[`${name}Label`] ?? name,
264
+ type: Class[name]?.type ?? Class[`${name}Type`] ?? typeof value,
265
+ required: Class[name]?.required ?? Class[`${name}Required`] ?? false,
266
+ placeholder: Class[name]?.placeholder ?? Class[`${name}Placeholder`] ?? '',
267
+ defaultValue: Class[name]?.defaultValue ?? Class[`${name}Default`] ?? '',
268
+ validation: Class[name]?.validation ?? Class[`${name}Validation`] ?? (() => true),
269
+ }),
270
+ )
269
271
  }
270
272
  return new UIForm({
271
273
  title: Class.name,
272
- fields
274
+ fields,
273
275
  })
274
276
  }
275
277
  return new UIForm(input)
@@ -298,10 +300,10 @@ export default class UIForm extends FormMessage {
298
300
  label,
299
301
  type: custom.type ?? FormInput.TYPES.TEXT,
300
302
  required: !!custom.required,
301
- placeholder: custom.placeholder ?? "",
303
+ placeholder: custom.placeholder ?? '',
302
304
  options: custom.options ?? [],
303
305
  validation: custom.validation ?? undefined,
304
- defaultValue: custom.defaultValue ?? "",
306
+ defaultValue: custom.defaultValue ?? '',
305
307
  })
306
308
  })
307
309
  return new UIForm({ fields })