@nan0web/ui 1.0.4 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nan0web/ui",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "NaN•Web UI. One application logic (algorithm) and many UI.",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -0,0 +1,29 @@
1
+ import UiMessage from "../../core/Message/Message.js"
2
+
3
+ class DepsCommandBody {
4
+ fix = false
5
+ static fix = {
6
+ help: "Fix dependencies",
7
+ defaultValue: false
8
+ }
9
+ constructor(input = {}) {
10
+ const {
11
+ fix = this.fix
12
+ } = input
13
+ }
14
+ }
15
+
16
+ export class DepsCommand extends UiMessage {
17
+ static Body = DepsCommandBody
18
+ /** @type {DepsCommandBody} */
19
+ body
20
+ constructor(input = {}) {
21
+ const {
22
+ body = new DepsCommandBody()
23
+ } = UiMessage.parseBody(input, DepsCommandBody)
24
+ super(input)
25
+ this.body = body
26
+ }
27
+ }
28
+
29
+ export default DepsCommand
@@ -1,5 +1,4 @@
1
1
  import { Message } from "@nan0web/co"
2
- import { notEmpty } from "@nan0web/types"
3
2
  import View from "../../View/View.js"
4
3
  import CoreApp from "./CoreApp.js"
5
4
  import Widget from "./Widget.js"
@@ -10,7 +9,7 @@ import Widget from "./Widget.js"
10
9
  * Abstract UI class to connect apps and widgets.
11
10
  * Supports input/output data typed classes and views.
12
11
  */
13
- class UI extends Widget {
12
+ export default class UI extends Widget {
14
13
  /** @type {CoreApp} The app instance connected to this UI */
15
14
  app
16
15
 
@@ -66,5 +65,3 @@ class UI extends Widget {
66
65
  })
67
66
  }
68
67
  }
69
-
70
- export default UI
@@ -1,30 +1,3 @@
1
- import Message from "@nan0web/co"
2
- import UIMessage from "../../../core/Message/Message.js"
1
+ import UiMessage from "../../../core/Message/Message.js"
3
2
 
4
- class DepsCommandParams {
5
- fix = false
6
- static fix = {
7
- help: "Fix dependencies",
8
- defaultValue: false
9
- }
10
- constructor(input = {}) {
11
- const {
12
- fix = this.fix
13
- } = input
14
- }
15
- }
16
-
17
- export class DepsCommand extends UIMessage {
18
- static Body = DepsCommandParams
19
- /** @type {DepsCommandParams} */
20
- body
21
- constructor(input = {}) {
22
- const {
23
- body = new DepsCommandParams()
24
- } = UIMessage.parseBody(input, DepsCommandParams)
25
- super(input)
26
- this.body = body
27
- }
28
- }
29
-
30
- export default DepsCommand
3
+ export default UiMessage
@@ -1,6 +1,5 @@
1
1
  import CommandMessage from "./Message.js"
2
- import DepsCommand from "./Message.js"
3
2
 
4
- export { CommandMessage, DepsCommand }
3
+ export { CommandMessage }
5
4
 
6
- export default CommandMessage
5
+ export default CommandMessage
@@ -6,7 +6,7 @@ import UserUI from "./UserUI.js"
6
6
  import UserAppCommandMessage from "./Command/Message.js"
7
7
  import DepsCommand from "./Command/Message.js"
8
8
  import UIStream from "../../core/Stream.js"
9
- import { UiMessage } from "../../core/index.js"
9
+ import { StreamEntry, UiMessage } from "../../core/index.js"
10
10
 
11
11
  /**
12
12
  * UserApp requires user name and shows Welcome view.
@@ -33,11 +33,11 @@ export default class UserApp extends CoreApp {
33
33
  */
34
34
  async handleDeps(cmd, ui) {
35
35
  // Example: Use async generator to stream deps processing
36
- const processorFn = async () => new UIStream.StreamEntry({ value: { message: `Deps command executed with fix: ${cmd.body.fix}` }, done: true })
36
+ const processorFn = async () => new StreamEntry({ value: { message: `Deps command executed with fix: ${cmd.body.fix}` }, done: true })
37
37
  const generatorFn = UIStream.createProcessor(new AbortController().signal, processorFn)
38
38
  await UIStream.process(new AbortController().signal, generatorFn,
39
39
  (progress, item) => ui.output && ui.output(item.value), // Fix to output the value
40
- (error) => ui.output && ui.output({ error }), // Assume ui has output method
40
+ (error) => ui.output && ui.output([error]), // Assume ui has output method
41
41
  (item) => ui.output && ui.output(item.value) // Fix complete callback
42
42
  )
43
43
  return { completed: true }
@@ -72,7 +72,7 @@ export default class UserApp extends CoreApp {
72
72
  return ui.render("Welcome", { user: this.user })
73
73
  }
74
74
  const answer = await ui.ask(UiMessage.from("What is your name?"))
75
- this.user = User.from(answer?.value)
75
+ this.user = User.from(answer?.body)
76
76
  return ui.render("Welcome", { user: this.user })
77
77
  }
78
78
  }
package/src/View/View.js CHANGED
@@ -269,8 +269,8 @@ export default class View {
269
269
  do {
270
270
  const answer = await this.stdin.read()
271
271
  result = /** @type {typeof UiMessage} */ (input.constructor).from(answer)
272
- } while (!result.isValid && !result.escaped)
273
- return result.escaped ? null : result
272
+ } while (!result.isValid && !result.head.cancelled)
273
+ return result.head.cancelled ? null : result
274
274
  }
275
275
 
276
276
  /**
@@ -16,8 +16,8 @@
16
16
  * @property {string} type - Input type (text, email, number, select, etc.).
17
17
  * @property {boolean} required - Whether the field is required.
18
18
  * @property {string} placeholder - Placeholder text.
19
- * @property {Array<string>} options - Select options (if type is 'select').
20
- * @property {Function|null} validation - Custom validation function.
19
+ * @property {InputOptions} options - Select options (if type is 'select').
20
+ * @property {Function} validation - Custom validation function.
21
21
  * @property {*} defaultValue - Default value.
22
22
  */
23
23
  export default class FormInput {
@@ -27,7 +27,7 @@ export default class FormInput {
27
27
  /** @type {boolean} */ required = false
28
28
  /** @type {string} */ placeholder = ''
29
29
  /** @type {InputOptions} */ options = []
30
- /** @type {import("@nan0web/co").ValidateFn|null} */ validation = null
30
+ /** @type {Function} */ validation = () => true
31
31
  /** @type {*} */ defaultValue = null
32
32
 
33
33
  /**
@@ -39,7 +39,7 @@ export default class FormInput {
39
39
  NUMBER: 'number',
40
40
  SELECT: 'select',
41
41
  CHECKBOX: 'checkbox',
42
- TEXTAREA: 'textarea'
42
+ TEXTAREA: 'textarea',
43
43
  }
44
44
 
45
45
  /**
@@ -52,7 +52,7 @@ export default class FormInput {
52
52
  * @param {boolean} [props.required=false] - Is required.
53
53
  * @param {string} [props.placeholder=''] - Placeholder.
54
54
  * @param {InputOptions} [props.options=[]] - Select options or async function to retrieve data with the search and page.
55
- * @param {Function} [props.validation=null] - Custom validation.
55
+ * @param {Function} [props.validation] - Custom validation.
56
56
  * @param {*} [props.defaultValue=null] - Default value.
57
57
  */
58
58
  constructor(props) {
@@ -64,7 +64,7 @@ export default class FormInput {
64
64
  placeholder = this.placeholder,
65
65
  options = [],
66
66
  validation = this.validation,
67
- defaultValue = this.defaultValue
67
+ defaultValue = this.defaultValue,
68
68
  } = props
69
69
 
70
70
  if (!name) {
@@ -86,11 +86,11 @@ export default class FormInput {
86
86
  requireValidType() {
87
87
  if (!Object.values(FormInput.TYPES).includes(this.type)) {
88
88
  throw new TypeError([
89
- "FormInput.type is invalid!",
90
- ["Provided", this.type].join(": "),
91
- "Available types:",
92
- ...Object.values(FormInput.TYPES).map(t => ` - ${t}`)
93
- ].join("\n"))
89
+ 'FormInput.type is invalid!',
90
+ ['Provided', this.type].join(': '),
91
+ 'Available types:',
92
+ ...Object.values(FormInput.TYPES).map((t) => ` - ${t}`),
93
+ ].join('\n'))
94
94
  }
95
95
  }
96
96
 
@@ -107,7 +107,7 @@ export default class FormInput {
107
107
  required: this.required,
108
108
  placeholder: this.placeholder,
109
109
  options: this.options,
110
- defaultValue: this.defaultValue
110
+ defaultValue: this.defaultValue,
111
111
  }
112
112
  }
113
113
 
@@ -117,7 +117,7 @@ export default class FormInput {
117
117
  */
118
118
  static from(input) {
119
119
  if (input instanceof FormInput) return input
120
- if ("string" === typeof input) {
120
+ if (typeof input === 'string') {
121
121
  return new FormInput({ name: input, label: input })
122
122
  }
123
123
  return new FormInput(input)
@@ -1,7 +1,5 @@
1
1
  import StreamEntry from "./StreamEntry.js"
2
2
 
3
- export { StreamEntry } // Export if needed
4
-
5
3
  /**
6
4
  * Agnostic UI stream for processing progress using async generators.
7
5
  *
@@ -18,6 +16,9 @@ export default class UIStream {
18
16
  static createProcessor(signal, processorFn) {
19
17
  return async function* () {
20
18
  try {
19
+ if (signal.aborted) {
20
+ throw new DOMException('Aborted', 'AbortError')
21
+ }
21
22
  const result = await processorFn()
22
23
  yield result
23
24
  } catch (/** @type {any} */ error) {
@@ -3,6 +3,8 @@ import CancelError from "./Error/CancelError.js"
3
3
  import UIMessage from "./Message/Message.js"
4
4
  import UIForm from "./Form/Form.js"
5
5
  import FormInput from "./Form/Input.js"
6
+ import OutputAdapter from "./OutputAdapter.js"
7
+ import OutputMessage from "./Message/OutputMessage.js"
6
8
 
7
9
  /**
8
10
  * Unified UI Adapter that handles both input and output operations.
@@ -81,6 +83,30 @@ export default class UiAdapter extends EventProcessor {
81
83
  throw new Error('select() method must be implemented in subclass')
82
84
  }
83
85
 
86
+ /**
87
+ * Process a UIForm and return its result.
88
+ *
89
+ * This default implementation follows an **agnostic UI** approach:
90
+ * it simply returns the form instance (with optional initial state merged)
91
+ * without UI interaction. Concrete adapters (CLI, Web, etc.) can override
92
+ * this method to render the form, collect user input and return a richer
93
+ * result object (`{ form, cancelled }`).
94
+ *
95
+ * @param {UIForm} form - The UIForm instance to process.
96
+ * @param {object} [initialState={}] - Pre‑filled values for the form.
97
+ * @returns {Promise<{ form: UIForm, cancelled?: boolean }>} Form processing result.
98
+ */
99
+ async processForm(form, initialState = {}) {
100
+ // Merge any provided initial state into the form's internal state.
101
+ if (initialState && typeof initialState === 'object') {
102
+ form.state = { ...form.state, ...initialState }
103
+ }
104
+ // In the agnostic baseline we do not perform any interactive I/O.
105
+ // Sub‑classes may provide a UI (render, ask, etc.) and return
106
+ // `{ form, cancelled: true/false, ... }`.
107
+ return { form }
108
+ }
109
+
84
110
  /**
85
111
  * Ensures a message's body is fully and validly filled.
86
112
  * Generates a form from the message's static Body schema,
@@ -105,12 +131,12 @@ export default class UiAdapter extends EventProcessor {
105
131
  let errors = msg.validate()
106
132
  while (errors.size > 0) {
107
133
  const form = generateForm(
108
- /** @type {any} */ (msg.constructor).Body,
134
+ /** @type {any} */(msg.constructor).Body,
109
135
  { initialState: msg.body }
110
136
  )
111
137
 
112
138
  const formResult = await this.processForm ? this.processForm(form, msg.body) : {} // Assume method exists or handle differently, but error indicates missing method; perhaps remove if not used
113
- if (formResult.escaped) {
139
+ if (formResult.cancelled) {
114
140
  throw new CancelError("User cancelled form")
115
141
  }
116
142
 
@@ -119,10 +145,11 @@ export default class UiAdapter extends EventProcessor {
119
145
 
120
146
  if (updatedErrors.size > 0) {
121
147
  if (this.output) {
122
- await this.output.render("Alert", {
148
+ this.output.render(new OutputMessage({
149
+ type: "Alert",
123
150
  variant: "error",
124
- content: Array.from(updatedErrors.values()).join("\n")
125
- })
151
+ body: Array.from(updatedErrors.values()).join("\n")
152
+ }))
126
153
  }
127
154
  errors = updatedErrors
128
155
  continue
package/src/core/index.js CHANGED
@@ -1,11 +1,17 @@
1
1
  export { default as InputAdapter } from "./InputAdapter.js"
2
2
  export { default as OutputAdapter } from "./OutputAdapter.js"
3
- export { default as UIStream } from "./Stream.js"
3
+
4
+ import UIStream from "./Stream.js"
5
+ export { UIStream, UIStream as UiStream }
6
+ import StreamEntry from "./StreamEntry.js"
7
+ export { StreamEntry, StreamEntry as UiStreamEntry }
4
8
 
5
9
  export { default as UiMessage } from "./Message/Message.js"
6
10
  export { default as FormMessage } from "./Form/Message.js"
7
11
  export { default as FormInput } from "./Form/Input.js"
8
- export { default as UIForm } from "./Form/Form.js"
12
+
13
+ import UIForm from "./Form/Form.js"
14
+ export { UIForm, UIForm as UiForm }
9
15
 
10
16
  export { default as Error, CancelError } from "./Error/index.js"
11
17
 
@@ -0,0 +1,16 @@
1
+ export class DepsCommand extends UiMessage {
2
+ static Body: typeof DepsCommandBody;
3
+ constructor(input?: {});
4
+ /** @type {DepsCommandBody} */
5
+ body: DepsCommandBody;
6
+ }
7
+ export default DepsCommand;
8
+ import UiMessage from "../../core/Message/Message.js";
9
+ declare class DepsCommandBody {
10
+ static fix: {
11
+ help: string;
12
+ defaultValue: boolean;
13
+ };
14
+ constructor(input?: {});
15
+ fix: boolean;
16
+ }
@@ -1,11 +1,9 @@
1
- export default UI;
2
- export type ComponentFn = import("../../View/View.js").ComponentFn;
3
1
  /** @typedef {import("../../View/View.js").ComponentFn} ComponentFn */
4
2
  /**
5
3
  * Abstract UI class to connect apps and widgets.
6
4
  * Supports input/output data typed classes and views.
7
5
  */
8
- declare class UI extends Widget {
6
+ export default class UI extends Widget {
9
7
  /**
10
8
  * Creates a new UI instance.
11
9
  * @param {CoreApp} app - The app to connect to this UI
@@ -33,6 +31,7 @@ declare class UI extends Widget {
33
31
  */
34
32
  output(results: any[]): void;
35
33
  }
34
+ export type ComponentFn = import("../../View/View.js").ComponentFn;
36
35
  import Widget from "./Widget.js";
37
36
  import CoreApp from "./CoreApp.js";
38
37
  import { Message } from "@nan0web/co";
@@ -1,16 +1,2 @@
1
- export class DepsCommand extends UIMessage {
2
- static Body: typeof DepsCommandParams;
3
- constructor(input?: {});
4
- /** @type {DepsCommandParams} */
5
- body: DepsCommandParams;
6
- }
7
- export default DepsCommand;
8
- import UIMessage from "../../../core/Message/Message.js";
9
- declare class DepsCommandParams {
10
- static fix: {
11
- help: string;
12
- defaultValue: boolean;
13
- };
14
- constructor(input?: {});
15
- fix: boolean;
16
- }
1
+ export default UiMessage;
2
+ import UiMessage from "../../../core/Message/Message.js";
@@ -1,4 +1,3 @@
1
+ export { CommandMessage };
1
2
  export default CommandMessage;
2
3
  import CommandMessage from "./Message.js";
3
- import DepsCommand from "./Message.js";
4
- export { CommandMessage, DepsCommand };
@@ -14,8 +14,8 @@
14
14
  * @property {string} type - Input type (text, email, number, select, etc.).
15
15
  * @property {boolean} required - Whether the field is required.
16
16
  * @property {string} placeholder - Placeholder text.
17
- * @property {Array<string>} options - Select options (if type is 'select').
18
- * @property {Function|null} validation - Custom validation function.
17
+ * @property {InputOptions} options - Select options (if type is 'select').
18
+ * @property {Function} validation - Custom validation function.
19
19
  * @property {*} defaultValue - Default value.
20
20
  */
21
21
  export default class FormInput {
@@ -45,7 +45,7 @@ export default class FormInput {
45
45
  * @param {boolean} [props.required=false] - Is required.
46
46
  * @param {string} [props.placeholder=''] - Placeholder.
47
47
  * @param {InputOptions} [props.options=[]] - Select options or async function to retrieve data with the search and page.
48
- * @param {Function} [props.validation=null] - Custom validation.
48
+ * @param {Function} [props.validation] - Custom validation.
49
49
  * @param {*} [props.defaultValue=null] - Default value.
50
50
  */
51
51
  constructor(props: {
@@ -64,7 +64,7 @@ export default class FormInput {
64
64
  /** @type {boolean} */ required: boolean;
65
65
  /** @type {string} */ placeholder: string;
66
66
  /** @type {InputOptions} */ options: InputOptions;
67
- /** @type {import("@nan0web/co").ValidateFn|null} */ validation: import("@nan0web/co").ValidateFn | null;
67
+ /** @type {Function} */ validation: Function;
68
68
  /** @type {*} */ defaultValue: any;
69
69
  requireValidType(): void;
70
70
  /**
@@ -1,4 +1,3 @@
1
- export { StreamEntry };
2
1
  /**
3
2
  * Agnostic UI stream for processing progress using async generators.
4
3
  *
@@ -74,6 +74,23 @@ export default class UiAdapter extends EventProcessor {
74
74
  index: number;
75
75
  value: string | null;
76
76
  }>;
77
+ /**
78
+ * Process a UIForm and return its result.
79
+ *
80
+ * This default implementation follows an **agnostic UI** approach:
81
+ * it simply returns the form instance (with optional initial state merged)
82
+ * without UI interaction. Concrete adapters (CLI, Web, etc.) can override
83
+ * this method to render the form, collect user input and return a richer
84
+ * result object (`{ form, cancelled }`).
85
+ *
86
+ * @param {UIForm} form - The UIForm instance to process.
87
+ * @param {object} [initialState={}] - Pre‑filled values for the form.
88
+ * @returns {Promise<{ form: UIForm, cancelled?: boolean }>} Form processing result.
89
+ */
90
+ processForm(form: UIForm, initialState?: object): Promise<{
91
+ form: UIForm;
92
+ cancelled?: boolean;
93
+ }>;
77
94
  /**
78
95
  * Ensures a message's body is fully and validly filled.
79
96
  * Generates a form from the message's static Body schema,
@@ -101,4 +118,5 @@ export default class UiAdapter extends EventProcessor {
101
118
  import UIForm from "./Form/Form.js";
102
119
  import EventProcessor from "@nan0web/event/oop";
103
120
  import CancelError from "./Error/CancelError.js";
121
+ import OutputAdapter from "./OutputAdapter.js";
104
122
  import UIMessage from "./Message/Message.js";
@@ -1,9 +1,11 @@
1
1
  export { default as InputAdapter } from "./InputAdapter.js";
2
2
  export { default as OutputAdapter } from "./OutputAdapter.js";
3
- export { default as UIStream } from "./Stream.js";
4
3
  export { default as UiMessage } from "./Message/Message.js";
5
4
  export { default as FormMessage } from "./Form/Message.js";
6
5
  export { default as FormInput } from "./Form/Input.js";
7
- export { default as UIForm } from "./Form/Form.js";
8
6
  export { default as UiAdapter } from "./UiAdapter.js";
7
+ import UIStream from "./Stream.js";
8
+ import StreamEntry from "./StreamEntry.js";
9
+ import UIForm from "./Form/Form.js";
10
+ export { UIStream, UIStream as UiStream, StreamEntry, StreamEntry as UiStreamEntry, UIForm, UIForm as UiForm };
9
11
  export { default as Error, CancelError } from "./Error/index.js";