@nan0web/ui 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +7 -1
  2. package/package.json +6 -1
  3. package/src/README.md.js +23 -0
  4. package/src/core/Error/CancelError.js +6 -0
  5. package/src/core/InputAdapter.js +21 -3
  6. package/types/core/Error/CancelError.d.ts +3 -0
  7. package/types/core/InputAdapter.d.ts +18 -2
  8. package/.datasets/README.dataset.jsonl +0 -12
  9. package/.editorconfig +0 -20
  10. package/CONTRIBUTING.md +0 -42
  11. package/docs/uk/README.md +0 -240
  12. package/playground/User.js +0 -52
  13. package/playground/currency.exchange.js +0 -48
  14. package/playground/i18n/index.js +0 -21
  15. package/playground/i18n/uk.js +0 -53
  16. package/playground/language.form.js +0 -25
  17. package/playground/main.js +0 -72
  18. package/playground/registration.form.js +0 -58
  19. package/playground/topup.telephone.js +0 -62
  20. package/src/App/User/UserApp.test.js +0 -56
  21. package/src/App/User/UserUI.test.js +0 -51
  22. package/src/Frame/Frame.test.js +0 -429
  23. package/src/View/View.test.js +0 -77
  24. package/src/core/Form/Form.test.js +0 -116
  25. package/src/core/Form/Input.test.js +0 -58
  26. package/src/core/Form/Message.test.js +0 -54
  27. package/src/core/InputAdapter.test.js +0 -35
  28. package/src/core/Message/InputMessage.test.js +0 -45
  29. package/src/core/Message/Message.test.js +0 -58
  30. package/src/core/Message/OutputMessage.test.js +0 -61
  31. package/src/core/OutputAdapter.test.js +0 -35
  32. package/src/core/Stream.test.js +0 -78
  33. package/src/index.test.js +0 -14
  34. package/stories/App/AppView.js +0 -15
  35. package/stories/App/AppView.test.js +0 -22
  36. package/stories/App/RenderOptions.js +0 -14
  37. package/stories/nodejs/interface.test.js +0 -27
  38. package/system.md +0 -187
  39. package/system1.md +0 -137
  40. package/task.md +0 -181
  41. package/tsconfig.json +0 -23
  42. package/vitest.config.js +0 -26
@@ -1,54 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import FormMessage from "./Message.js"
4
-
5
- describe("FormMessage", () => {
6
- it("should create instance with default values", () => {
7
- const msg = new FormMessage()
8
- assert.ok(msg instanceof FormMessage)
9
- assert.deepEqual(msg.data, {})
10
- assert.deepEqual(msg.schema, {})
11
- assert.equal(msg.type, "form")
12
- })
13
-
14
- it("should create instance with custom values", () => {
15
- const props = {
16
- data: { name: "John" },
17
- schema: { name: { required: true } },
18
- body: ["Form content"]
19
- }
20
- const msg = new FormMessage(props)
21
- assert.deepEqual(msg.data, { name: "John" })
22
- assert.deepEqual(msg.schema, { name: { required: true } })
23
- assert.deepEqual(msg.body, ["Form content"])
24
- })
25
-
26
- it("should add data", () => {
27
- const msg = new FormMessage({ data: { name: "John" } })
28
- const newMsg = msg.addData({ age: 30 })
29
- assert.deepEqual(newMsg.data, { name: "John", age: 30 })
30
- })
31
-
32
- it("should validate data correctly", () => {
33
- const schema = {
34
- name: { required: true },
35
- email: { type: "email" },
36
- age: { type: "number" }
37
- }
38
- const msg = new FormMessage({ schema })
39
-
40
- // Valid data
41
- const validData = { name: "John", email: "john@example.com", age: "30" }
42
- const validResult = msg.validateData(validData)
43
- assert.ok(validResult.isValid)
44
- assert.deepEqual(validResult.errors, {})
45
-
46
- // Invalid data
47
- const invalidData = { email: "invalid-email", age: "not-a-number" }
48
- const invalidResult = msg.validateData(invalidData)
49
- assert.ok(!invalidResult.isValid)
50
- assert.ok(invalidResult.errors.name)
51
- assert.ok(invalidResult.errors.email)
52
- assert.ok(invalidResult.errors.age)
53
- })
54
- })
@@ -1,35 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import InputAdapter from "./InputAdapter.js"
4
-
5
- describe("InputAdapter", () => {
6
- it("should create instance", () => {
7
- const adapter = new InputAdapter()
8
- assert.ok(adapter instanceof InputAdapter)
9
- })
10
-
11
- it("should start and emit input message", () => {
12
- const adapter = new InputAdapter()
13
- let emitted = false
14
- let message = null
15
-
16
- adapter.on('input', (msg) => {
17
- emitted = true
18
- message = msg
19
- })
20
-
21
- adapter.start()
22
- assert.ok(emitted)
23
- assert.ok(message)
24
- })
25
-
26
- it("should stop without error", () => {
27
- const adapter = new InputAdapter()
28
- assert.doesNotThrow(() => adapter.stop())
29
- })
30
-
31
- it("should be ready by default", () => {
32
- const adapter = new InputAdapter()
33
- assert.ok(adapter.isReady())
34
- })
35
- })
@@ -1,45 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import InputMessage from "./InputMessage.js"
4
- import { notEmpty, empty } from "@nan0web/types"
5
-
6
- describe("InputMessage", () => {
7
- it("should create instance with default values", () => {
8
- const msg = new InputMessage()
9
- assert.ok(msg instanceof InputMessage)
10
- assert.equal(msg.value, "")
11
- assert.equal(msg.waiting, false)
12
- assert.deepEqual(msg.options, [])
13
- })
14
-
15
- it("should create instance with custom values", () => {
16
- const props = {
17
- value: "user input",
18
- waiting: true,
19
- options: ["option1", "option2"]
20
- }
21
- const msg = new InputMessage(props)
22
- assert.equal(msg.value, "user input")
23
- assert.equal(msg.waiting, true)
24
- assert.deepEqual(msg.options, ["option1", "option2"])
25
- })
26
-
27
- it("should handle string options correctly", () => {
28
- const msg = new InputMessage({ options: "single-option" })
29
- assert.deepEqual(msg.options, ["single-option"])
30
- })
31
-
32
- it("should detect empty value", () => {
33
- const emptyMsg = new InputMessage({ value: "" })
34
- const nonEmptyMsg = new InputMessage({ value: "test" })
35
- assert.ok(emptyMsg.empty)
36
- assert.ok(!nonEmptyMsg.empty)
37
- })
38
-
39
- it("should validate message", () => {
40
- const validMsg = new InputMessage({ value: "test" })
41
- const invalidMsg = new InputMessage({ value: "" })
42
- assert.ok(validMsg.isValid)
43
- assert.ok(!invalidMsg.isValid)
44
- })
45
- })
@@ -1,58 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import UIMessage from "./Message.js"
4
-
5
- describe("UIMessage", () => {
6
- it("should create instance with default values", () => {
7
- const msg = new UIMessage()
8
- assert.ok(msg instanceof UIMessage)
9
- assert.equal(typeof msg.type, "string")
10
- assert.equal(typeof msg.id, "string")
11
- assert.ok(msg.time instanceof Date)
12
- })
13
-
14
- it("should create instance with custom values", () => {
15
- const props = {
16
- type: "test",
17
- id: "custom-id",
18
- body: ["test content"]
19
- }
20
- const msg = new UIMessage(props)
21
- assert.equal(msg.type, "test")
22
- assert.equal(msg.id, "custom-id")
23
- assert.deepEqual(msg.body, ["test content"])
24
- })
25
-
26
- it("should generate unique id when not provided", () => {
27
- const msg1 = new UIMessage()
28
- const msg2 = new UIMessage()
29
- assert.notEqual(msg1.id, msg2.id)
30
- })
31
-
32
- it("should use content as body when body not provided", () => {
33
- const msg = new UIMessage({ content: ["hello"] })
34
- assert.deepEqual(msg.body, ["hello"])
35
- })
36
-
37
- it("should create from data using from() method", () => {
38
- const data = { type: "info", body: ["test"] }
39
- const msg = UIMessage.from(data)
40
- assert.ok(msg instanceof UIMessage)
41
- assert.equal(msg.type, "info")
42
- assert.deepEqual(msg.body, ["test"])
43
- })
44
-
45
- it("should validate type correctly", () => {
46
- const validMsg = new UIMessage({ type: UIMessage.TYPES.TEXT })
47
- const invalidMsg = new UIMessage({ type: "invalid-type" })
48
- assert.ok(validMsg.isValidType())
49
- assert.ok(!invalidMsg.isValidType())
50
- })
51
-
52
- it("should check if message is empty", () => {
53
- const emptyMsg = new UIMessage()
54
- const nonEmptyMsg = new UIMessage({ body: ["content"] })
55
- assert.ok(emptyMsg.isEmpty())
56
- assert.ok(!nonEmptyMsg.isEmpty())
57
- })
58
- })
@@ -1,61 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import OutputMessage from "./OutputMessage.js"
4
-
5
- describe("OutputMessage", () => {
6
- it("should create instance with default values", () => {
7
- const msg = new OutputMessage()
8
- assert.ok(msg instanceof OutputMessage)
9
- assert.deepEqual(msg.body, [])
10
- assert.deepEqual(msg.meta, {})
11
- assert.equal(msg.error, null)
12
- assert.equal(msg.priority, OutputMessage.PRIORITY.NORMAL)
13
- })
14
-
15
- it("should create instance with custom values", () => {
16
- const props = {
17
- content: ["test content"],
18
- meta: { key: "value" },
19
- error: new Error("test error"),
20
- priority: OutputMessage.PRIORITY.HIGH
21
- }
22
- const msg = new OutputMessage(props)
23
- assert.deepEqual(msg.content, ["test content"])
24
- assert.deepEqual(msg.meta, { key: "value" })
25
- assert.ok(msg.error instanceof Error)
26
- assert.equal(msg.priority, OutputMessage.PRIORITY.HIGH)
27
- })
28
-
29
- it("should set type based on error presence", () => {
30
- const errorMsg = new OutputMessage({ error: "error occurred" })
31
- const infoMsg = new OutputMessage({ content: ["info"] })
32
- assert.equal(errorMsg.type, "error")
33
- assert.equal(infoMsg.type, "info")
34
- })
35
-
36
- it("should handle string content", () => {
37
- const msg = new OutputMessage({ content: "single string" })
38
- assert.deepEqual(msg.content, ["single string"])
39
- })
40
-
41
- it("should combine messages", () => {
42
- const msg1 = new OutputMessage({ content: ["part1"], priority: 1 })
43
- const msg2 = new OutputMessage({ content: ["part2"], priority: 2 })
44
- const combined = msg1.combine(msg2)
45
- assert.deepEqual(combined.content, ["part1", "part2"])
46
- assert.equal(combined.priority, 2)
47
- })
48
-
49
- it("should convert to JSON", () => {
50
- const msg = new OutputMessage({
51
- content: ["test"],
52
- meta: { test: true },
53
- error: new Error("test error")
54
- })
55
- const json = msg.toJSON()
56
- assert.ok(json.body)
57
- assert.ok(json.meta)
58
- assert.ok(json.error)
59
- assert.ok(json.time)
60
- })
61
- })
@@ -1,35 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import OutputAdapter from "./OutputAdapter.js"
4
-
5
- describe("OutputAdapter", () => {
6
- it("should create instance", () => {
7
- const adapter = new OutputAdapter()
8
- assert.ok(adapter instanceof OutputAdapter)
9
- })
10
-
11
- it("should throw error on render method", () => {
12
- const adapter = new OutputAdapter()
13
- assert.throws(() => adapter.render(), {
14
- message: "render() must be implemented by subclass"
15
- })
16
- })
17
-
18
- it("should call render on progress", () => {
19
- const adapter = new OutputAdapter()
20
- let rendered = false
21
-
22
- // Override render for test
23
- adapter.render = () => {
24
- rendered = true
25
- }
26
-
27
- adapter.progress(0.5)
28
- assert.ok(rendered)
29
- })
30
-
31
- it("should stop without error", () => {
32
- const adapter = new OutputAdapter()
33
- assert.doesNotThrow(() => adapter.stop())
34
- })
35
- })
@@ -1,78 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import UIStream from "./Stream.js"
4
- import StreamEntry from "./StreamEntry.js"
5
-
6
- describe("UIStream", () => {
7
- it("should create processor generator", async () => {
8
- const controller = new AbortController()
9
- const processorFn = () => Promise.resolve("test result")
10
- const generator = UIStream.createProcessor(controller.signal, processorFn)
11
-
12
- assert.equal(typeof generator, "function")
13
-
14
- for await (const item of generator()) {
15
- assert.equal(item, "test result")
16
- break
17
- }
18
- })
19
-
20
- it("should handle aborted signal", async () => {
21
- const controller = new AbortController()
22
- controller.abort()
23
-
24
- const processorFn = () => Promise.resolve(StreamEntry.from({ value: "Hello", done: true }))
25
- const generator = UIStream.createProcessor(controller.signal, processorFn)
26
-
27
- for await (const item of generator()) {
28
- assert.deepEqual(item, StreamEntry.from({ done: true, value: "Hello" }))
29
- break
30
- }
31
- })
32
-
33
- it("should process stream with callbacks", async () => {
34
- const controller = new AbortController()
35
- let progressCalled = false
36
- let completeCalled = false
37
-
38
- const generatorFn = async function* () {
39
- yield StreamEntry.from({ progress: 0.5 })
40
- yield StreamEntry.from({ done: true })
41
- }
42
-
43
- const onProgress = () => { progressCalled = true }
44
- const onComplete = () => { completeCalled = true }
45
-
46
- await UIStream.process(
47
- controller.signal,
48
- generatorFn,
49
- onProgress,
50
- null,
51
- onComplete
52
- )
53
-
54
- assert.ok(progressCalled)
55
- assert.ok(completeCalled)
56
- })
57
-
58
- it("should handle errors in stream processing", async () => {
59
- const controller = new AbortController()
60
- let errorCalled = false
61
-
62
- const generatorFn = async function* () {
63
- yield { error: "test error" }
64
- }
65
-
66
- const onError = () => { errorCalled = true }
67
-
68
- await UIStream.process(
69
- controller.signal,
70
- generatorFn,
71
- null,
72
- onError,
73
- null
74
- )
75
-
76
- assert.ok(errorCalled)
77
- })
78
- })
package/src/index.test.js DELETED
@@ -1,14 +0,0 @@
1
- import { describe, it } from "node:test"
2
- import { strict as assert } from "node:assert"
3
- import { App } from "@nan0web/ui"
4
-
5
- describe("App", () => {
6
- it("should be defined", () => {
7
- assert.ok(App)
8
- assert.ok(App.Core)
9
- assert.ok(App.User)
10
- assert.ok(App.Command)
11
- assert.ok(App.Scenario)
12
- assert.ok(App.UI)
13
- })
14
- })
@@ -1,15 +0,0 @@
1
- import View from "../../src/View/View.js"
2
- import Welcome from "../../src/Component/Welcome/index.js"
3
- import Process from "../../src/Component/Process/index.js"
4
- import AppRenderOptions from "./RenderOptions.js"
5
-
6
- class AppView extends View {
7
- static RenderOptions = AppRenderOptions
8
- constructor(props = {}) {
9
- super(props)
10
- this.register(Welcome)
11
- this.register(Process)
12
- }
13
- }
14
-
15
- export default AppView
@@ -1,22 +0,0 @@
1
- import { describe, it, expect } from "vitest"
2
- import AppView from "./AppView.js"
3
- import Frame from "../../src/Frame/Frame.js"
4
-
5
- describe("AppView", () => {
6
- it("should create an instance", () => {
7
- const view = new AppView()
8
- expect(view).toBeInstanceOf(AppView)
9
- })
10
- it("should render a welcome component", () => {
11
- const view = new AppView()
12
- const welcome = view.render("Welcome")({ user: { name: "NaN•" } })
13
- expect(welcome).toBeInstanceOf(Frame)
14
- expect(view.stdout.stream).toEqual([
15
- [
16
- "Welcome NaN•!",
17
- "What can we do today great?",
18
- "",
19
- ].join("\n")
20
- ])
21
- })
22
- })
@@ -1,14 +0,0 @@
1
- import RenderOptions from "../../src/View/RenderOptions.js"
2
-
3
- class AppRenderOptions extends RenderOptions {
4
- static DEFAULTS = {
5
- ...RenderOptions.DEFAULTS,
6
- resizeToView: true,
7
- }
8
- static from(props = {}) {
9
- if (props instanceof AppRenderOptions) return props
10
- return new this(props)
11
- }
12
- }
13
-
14
- export default AppRenderOptions
@@ -1,27 +0,0 @@
1
- /**
2
- * @todo write tests for the interfaces StdIn, StdOut, Locale, Frame, FrameProps and View in NodeJS with vitest.
3
- * Follow the actor / scenario model.
4
- * Basic interfaces must include:
5
- * - read text from stdin
6
- * - write text to stdout
7
- * - read an option from a selectbox with the bordered frame
8
- * - write result to stdout
9
- *
10
- * Frames and Views must follow the blessed interface as for flexibility.
11
- */
12
- import { describe, it, expect } from "vitest"
13
-
14
- describe("Interface", () => {
15
- it("should read text from stdin", () => {
16
- expect("").toBe("")
17
- })
18
- it("should write text to stdout", () => {
19
- expect("").toBe("")
20
- })
21
- it("should read an option from a selectbox with the bordered frame", () => {
22
- expect("").toBe("")
23
- })
24
- it("should write result to stdout with the bordered frame", () => {
25
- expect("").toBe("")
26
- })
27
- })
package/system.md DELETED
@@ -1,187 +0,0 @@
1
- # System intructions `system.md` / NaN•Web UI
2
-
3
- Vanilla javascript UI interfaces with no platform dependencies.
4
- Must work for nodejs, browser and other compilers that supports ESM.
5
-
6
- ## Requirements
7
-
8
- ### 1. Javascript
9
-
10
- 1.1. Pure vanilla js only.
11
- 1.2. Platform abstract.
12
- 1.3. Minimum dependency as possible.
13
- 1.4. Classes
14
- 1.4.1. Order of the class elements:
15
- - static properties
16
- - properties
17
- - constructor
18
- - getters
19
- - setters
20
- - base functions
21
- - async functions
22
- - static base functions
23
- - static async functions
24
-
25
- 1.5. Views, every must:
26
- 1.5.1. Have a separate file and test file `View/AppView.js` and `View/AppView.test.js`.
27
- 1.6. Components, every must:
28
- 1.6.1. Have a separate directory in `View/Component/*` or registered from any other directory with files:
29
- - `Component.js`
30
- - `Component.test.js`
31
- - `ComponentInput.js`
32
- - `index.js`
33
- ```js
34
- export default Component
35
- export { ComponentInput }
36
- ```
37
-
38
- ### 2. Tests
39
-
40
- Every model, component, class must have their own tests file to cover their functionality in isolated environment.
41
-
42
- #### node:test
43
-
44
- For the core (base), cgi and cli interfaces, and web interfaces non `.jsx` components `node:test` with `node:assert/strict` must be used for tests.
45
-
46
- #### vitest
47
-
48
- For `React` apps and any that uses `.jsx` components `vitest` must be used for tests.
49
-
50
- ## Interfaces
51
-
52
- 1. `base` - Самий базовий інтерфейс, який має описувати всі доступні для інтерфейсів функції + тести `node:test`.
53
- 1. `cli` - Console line application на основі команд через `argv[]` і вивід на екран за допомогою `@nan0web/log`.
54
- 1. `api` - API який працює постійно для надання даних для `web`, `mobile` та інших інтерфейсів.
55
- 1. `cgi` - Console graphic interface з віконцями і кнопками за допомогою `blessed`.
56
- 1. `web` - Веб інтерфейс з `Web.customElements`, `Lit`, або `React`.
57
- 1. `mobile` - Мобільний інтерфейс з `Ionic` або `Swift` і `Java`.
58
- 1. `desktop` - Веб інтерфейс у обкладинці `Electron` або `Tauri`.
59
- 1. `chat` - Чат інтерфейс `@nan0web/chat-ui`.
60
- 1. `audio` - Мовний (голосовий) інтерфейс `@nan0web/audio-ui`.
61
-
62
- Базовий інтерфейс описує всі взаємодії і можливі функції.
63
- Всі інші інтерфейси мають розширювати базовий.
64
-
65
- Інтерфейси клієнти, у випадку коли є окремий `api` сервер, можуть розширювати свій `ClientUI`.
66
-
67
- Приклад успадкування:
68
- ```js
69
- import UI from "@nan0web/ui"
70
-
71
- class BaseUI extends UI {}
72
-
73
- class CliUI extends BaseUI {}
74
-
75
- class APIUI extends CliUI {}
76
-
77
- class CgiUI extends CliUI {}
78
-
79
- class WebUI extends BaseUI {}
80
-
81
- class MobileUI extends BaseUI {}
82
-
83
- class DesktopUI extends WebUI {}
84
-
85
- class ChatUI extends BaseUI {}
86
-
87
- class AudioUI extends ChatUI {}
88
- ```
89
-
90
- ### Views
91
-
92
- #### Widgets
93
-
94
- Widget is a control alement including a view with and ask() ability to input data.
95
-
96
- #### Output
97
-
98
- Output data is passed to the view but also in specific format:
99
- ```js
100
- function UserDocumentsView(input = {}) {
101
- input = UserDocumentsInput.from(input)
102
- if (empty(input)) {
103
- throw new Error("Input data required in a format off UserDocumentsInput")
104
- }
105
-
106
- return Frame.from([
107
- ['Documents'],
108
- ...input.documents.map(d => d.name),
109
- ])
110
- }
111
- ```
112
-
113
- In this scenario I can easily define Input classes for the every View and Widget to be sure all the input pass, and easily require the proper input from the app before running a job.
114
-
115
-
116
- #### Input
117
-
118
- When application needs any data it requires.
119
-
120
- The input might be a command line `node start.js -user.name root` or, if not provided, user input through the provided infterface (any of CLI, API, voice, mobile, table, desktop, web, etc.)
121
-
122
- ```js
123
- class App {
124
- async requireUser() {
125
- const user = await this.view.ask("User")()
126
- if (user instanceof User) return user
127
- throw new TypeError("User is required to continue")
128
- }
129
- async run() {
130
- const user = await this.requireUser()
131
- this.view.render("Welcome")({ user })
132
- }
133
- }
134
- ```
135
-
136
- ## Cross platform
137
-
138
- Very important that I can extend basic UI with different apps, for instance:
139
- ```jsx
140
- // apps/web/src/components/User/DocumentsView.jsx
141
- import React from "react"
142
- /** the very basic component **/
143
- import UserDocumentsInput from "src/components/User/DocumentsInput.js"
144
-
145
- function UserDocumentsView(input = {}) {
146
- input = UserDocumentsInput.from(input)
147
- if (empty(input)) {
148
- throw new Error("Input data required in a format off UserDocumentsInput")
149
- }
150
-
151
- return <div>
152
- <h3>Documents</h3>
153
- <ul>
154
- {input.documents.map((d, i) => <li key={i}>{d.name}</li>)}
155
- </ul>
156
- </div>
157
- }
158
- ```
159
-
160
- ### Apps locations
161
-
162
- #### Core application
163
-
164
- Core application s with the minimum stdin, stdout
165
- - `src/*`
166
- - `src/App.js`
167
- - `bin/start.js`
168
- - `package.json`
169
-
170
- Core application might have own resository, for instance `@nan0web/app`.
171
-
172
- #### CLI application
173
-
174
- CLI application can utilize `blessed` or other CLI library for rich UI experience, compared to Core.
175
-
176
- - `apps/cli/src/*`
177
- - `apps/cli/bin/start.js`
178
- - `apps/cli/package.json`
179
-
180
- #### Web application
181
-
182
- Web applications can utilize `react` or `lit` or other Web UI library for rich UI experience.
183
- Most desired is `lit` to avoid using typescript or jsx.
184
-
185
- - `apps/web/src/*`
186
- - `apps/web/bin/start.js`
187
- - `apps/web/package.json`