@nan0web/ui 1.8.0 → 1.10.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/README.md +29 -10
- package/package.json +18 -22
- package/src/Model/index.js +32 -3
- package/src/core/Form/Form.js +8 -7
- package/src/core/Form/Message.js +1 -1
- package/src/core/GeneratorRunner.js +10 -0
- package/src/core/Intent.js +21 -5
- package/src/core/IntentErrorModel.js +6 -1
- package/src/core/Stream.js +16 -5
- package/src/core/index.js +1 -1
- package/src/domain/FooterModel.js +57 -0
- package/src/domain/HeaderModel.js +50 -0
- package/src/domain/HeroModel.js +48 -0
- package/src/domain/Navigation.js +11 -10
- package/src/domain/SandboxModel.js +66 -115
- package/src/domain/ShowcaseAppModel.js +133 -50
- package/src/domain/components/AccordionModel.js +38 -0
- package/src/domain/components/AutocompleteModel.js +11 -21
- package/src/domain/components/BannerModel.js +37 -0
- package/src/domain/components/BreadcrumbModel.js +11 -9
- package/src/domain/components/ButtonModel.js +31 -58
- package/src/domain/components/CommentModel.js +44 -0
- package/src/domain/components/ConfirmModel.js +26 -33
- package/src/domain/components/EmptyStateModel.js +45 -0
- package/src/domain/components/FAQModel.js +32 -0
- package/src/domain/components/FooterConfigModel.js +26 -0
- package/src/domain/components/FooterVisibilityModel.js +48 -0
- package/src/domain/components/GalleryModel.js +36 -0
- package/src/domain/components/HeaderConfigModel.js +26 -0
- package/src/domain/components/HeaderVisibilityModel.js +54 -0
- package/src/domain/components/InputModel.js +21 -41
- package/src/domain/components/PriceModel.js +30 -0
- package/src/domain/components/PricingModel.js +39 -0
- package/src/domain/components/PricingSectionModel.js +32 -0
- package/src/domain/components/ProfileDropdownModel.js +45 -0
- package/src/domain/components/SelectModel.js +11 -21
- package/src/domain/components/SpinnerModel.js +11 -26
- package/src/domain/components/StatsItemModel.js +38 -0
- package/src/domain/components/StatsModel.js +32 -0
- package/src/domain/components/TableModel.js +11 -24
- package/src/domain/components/TabsModel.js +30 -0
- package/src/domain/components/TestimonialModel.js +24 -0
- package/src/domain/components/TimelineItemModel.js +38 -0
- package/src/domain/components/TimelineModel.js +32 -0
- package/src/domain/components/ToastModel.js +24 -51
- package/src/domain/components/TreeModel.js +10 -26
- package/src/domain/components/index.js +34 -0
- package/src/domain/index.js +24 -0
- package/src/index.js +2 -0
- package/src/testing/GalleryGenerator.js +85 -0
- package/src/testing/LogicInspector.js +55 -0
- package/src/testing/SnapshotInspector.js +84 -0
- package/src/testing/VisualAdapter.js +41 -0
- package/src/testing/index.js +3 -0
- package/types/Model/index.d.ts +62 -4
- package/types/core/Form/Form.d.ts +2 -2
- package/types/core/GeneratorRunner.d.ts +4 -0
- package/types/core/Intent.d.ts +31 -3
- package/types/core/IntentErrorModel.d.ts +4 -0
- package/types/core/index.d.ts +1 -1
- package/types/domain/FooterModel.d.ts +52 -0
- package/types/domain/HeaderModel.d.ts +45 -0
- package/types/domain/HeroModel.d.ts +43 -0
- package/types/domain/Navigation.d.ts +10 -9
- package/types/domain/SandboxModel.d.ts +16 -40
- package/types/domain/ShowcaseAppModel.d.ts +26 -54
- package/types/domain/components/AccordionModel.d.ts +33 -0
- package/types/domain/components/AutocompleteModel.d.ts +10 -29
- package/types/domain/components/BannerModel.d.ts +32 -0
- package/types/domain/components/BreadcrumbModel.d.ts +13 -6
- package/types/domain/components/ButtonModel.d.ts +18 -54
- package/types/domain/components/CommentModel.d.ts +39 -0
- package/types/domain/components/ConfirmModel.d.ts +20 -35
- package/types/domain/components/EmptyStateModel.d.ts +40 -0
- package/types/domain/components/FAQModel.d.ts +27 -0
- package/types/domain/components/FooterConfigModel.d.ts +21 -0
- package/types/domain/components/FooterVisibilityModel.d.ts +43 -0
- package/types/domain/components/GalleryModel.d.ts +35 -0
- package/types/domain/components/HeaderConfigModel.d.ts +21 -0
- package/types/domain/components/HeaderVisibilityModel.d.ts +49 -0
- package/types/domain/components/HeroModel.d.ts +24 -0
- package/types/domain/components/InputModel.d.ts +19 -59
- package/types/domain/components/PriceModel.d.ts +25 -0
- package/types/domain/components/PricingModel.d.ts +34 -0
- package/types/domain/components/PricingSectionModel.d.ts +27 -0
- package/types/domain/components/ProfileDropdownModel.d.ts +40 -0
- package/types/domain/components/SelectModel.d.ts +13 -28
- package/types/domain/components/ShowcaseAppModel.d.ts +32 -0
- package/types/domain/components/SpinnerModel.d.ts +10 -27
- package/types/domain/components/StatsItemModel.d.ts +33 -0
- package/types/domain/components/StatsModel.d.ts +27 -0
- package/types/domain/components/TableModel.d.ts +10 -26
- package/types/domain/components/TabsModel.d.ts +28 -0
- package/types/domain/components/TestimonialModel.d.ts +18 -0
- package/types/domain/components/TimelineItemModel.d.ts +33 -0
- package/types/domain/components/TimelineModel.d.ts +27 -0
- package/types/domain/components/ToastModel.d.ts +16 -45
- package/types/domain/components/TreeModel.d.ts +13 -36
- package/types/domain/components/index.d.ts +20 -0
- package/types/domain/index.d.ts +4 -1
- package/types/index.d.ts +1 -0
- package/types/testing/GalleryGenerator.d.ts +1 -0
- package/types/testing/LogicInspector.d.ts +22 -0
- package/types/testing/SnapshotInspector.d.ts +17 -0
- package/types/testing/VisualAdapter.d.ts +15 -0
- package/types/testing/index.d.ts +3 -0
- package/src/README.md.js +0 -436
package/README.md
CHANGED
|
@@ -90,9 +90,9 @@ const form = new UiForm({
|
|
|
90
90
|
message: 'Hello!',
|
|
91
91
|
},
|
|
92
92
|
})
|
|
93
|
-
const errors = form.validate()
|
|
94
|
-
console.info(errors.
|
|
95
|
-
console.info(errors.
|
|
93
|
+
const { isValid, errors } = form.validate()
|
|
94
|
+
console.info(Object.keys(errors).length) // ← 1
|
|
95
|
+
console.info(errors.email) // ← Invalid email format
|
|
96
96
|
```
|
|
97
97
|
### Components
|
|
98
98
|
|
|
@@ -148,18 +148,37 @@ const frame = new Frame({
|
|
|
148
148
|
const rendered = frame.render()
|
|
149
149
|
console.info(rendered.includes('Frame content')) // ← true
|
|
150
150
|
```
|
|
151
|
-
### Models
|
|
151
|
+
### Domain Models (v1.9.0)
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
v1.9.0 introduces a comprehensive set of domain models for layout and components.
|
|
154
|
+
These models follow the **Model-as-Schema** pattern.
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
#### Layout Models
|
|
157
|
+
- `HeaderModel` — title, logo, navigation actions
|
|
158
|
+
- `FooterModel` — copyright, version, social links
|
|
159
|
+
- `HeroModel` — prominent call-to-action
|
|
156
160
|
|
|
157
|
-
|
|
161
|
+
#### Component Models
|
|
162
|
+
- `PricingModel` — plans with features and prices
|
|
163
|
+
- `CommentModel` & `TestimonialModel` — social proof
|
|
164
|
+
- `StatsModel` — data visualizations
|
|
165
|
+
- `TimelineModel` — event history
|
|
166
|
+
|
|
167
|
+
How to use the new Header and Hero models?
|
|
158
168
|
```js
|
|
159
169
|
import { Model } from '@nan0web/ui'
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
const { HeaderModel, HeroModel } = Model
|
|
171
|
+
const header = new HeaderModel({
|
|
172
|
+
title: 'NaN•Web',
|
|
173
|
+
logo: '/logo.svg',
|
|
174
|
+
actions: [{ title: 'Docs', href: '/docs' }],
|
|
175
|
+
})
|
|
176
|
+
const hero = new HeroModel({
|
|
177
|
+
title: 'One Logic — Many UI',
|
|
178
|
+
actions: [{ title: 'Get Started', href: '/start' }],
|
|
179
|
+
})
|
|
180
|
+
console.info(header.title) // ← NaN•Web
|
|
181
|
+
console.info(hero.actions[0].title) // ← Get Started
|
|
163
182
|
```
|
|
164
183
|
### Testing UI
|
|
165
184
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"src/**/*.js",
|
|
10
10
|
"!src/**/*.test.js",
|
|
11
|
+
"!src/README.md.js",
|
|
11
12
|
"types/**/*.d.ts"
|
|
12
13
|
],
|
|
13
14
|
"exports": {
|
|
@@ -15,17 +16,10 @@
|
|
|
15
16
|
"import": "./src/index.js",
|
|
16
17
|
"types": "./types/index.d.ts"
|
|
17
18
|
},
|
|
18
|
-
"./components":
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"./core": {
|
|
23
|
-
"import": "./src/core/index.js",
|
|
24
|
-
"types": "./types/core/index.d.ts"
|
|
25
|
-
},
|
|
26
|
-
"./domain": {
|
|
27
|
-
"import": "./src/domain/index.js"
|
|
28
|
-
},
|
|
19
|
+
"./components": "./src/Component/index.js",
|
|
20
|
+
"./core": "./src/core/index.js",
|
|
21
|
+
"./domain": "./src/domain/index.js",
|
|
22
|
+
"./testing": "./src/testing/index.js",
|
|
29
23
|
"./cli-app": "./apps/cli/src/index.js",
|
|
30
24
|
"./mobile-app": "./apps/mobile/src/index.js",
|
|
31
25
|
"./web-app": "./apps/web/src/App.jsx"
|
|
@@ -37,11 +31,12 @@
|
|
|
37
31
|
"author": "YaRaSLove (ЯRаСлав) <support@yaro.page>",
|
|
38
32
|
"license": "ISC",
|
|
39
33
|
"devDependencies": {
|
|
40
|
-
"@nan0web/event": "
|
|
34
|
+
"@nan0web/event": "^1.0.1",
|
|
41
35
|
"@nan0web/i18n": "^1.1.0",
|
|
42
36
|
"@nan0web/release": "^1.0.2",
|
|
43
37
|
"@nan0web/test": "^1.1.3",
|
|
44
|
-
"@nan0web/ui-cli": "^2.
|
|
38
|
+
"@nan0web/ui-cli": "^2.9.0",
|
|
39
|
+
"@nan0web/ui-lit": "^1.1.0",
|
|
45
40
|
"@playwright/test": "^1.58.2",
|
|
46
41
|
"@rollup/plugin-yaml": "^4.1.2",
|
|
47
42
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -50,16 +45,14 @@
|
|
|
50
45
|
"knip": "^5.86.0",
|
|
51
46
|
"lit": "^3.3.2",
|
|
52
47
|
"vite": "^6.4.1",
|
|
53
|
-
"vitest": "^3.2.4"
|
|
54
|
-
"@nan0web/ui-lit": "1.1.0"
|
|
48
|
+
"vitest": "^3.2.4"
|
|
55
49
|
},
|
|
56
50
|
"dependencies": {
|
|
57
51
|
"@nan0web/co": "^2.0.0",
|
|
58
|
-
"@nan0web/event": "^1.0.1",
|
|
59
52
|
"@nan0web/log": "^1.1.1",
|
|
60
53
|
"@nan0web/types": "^1.4.0",
|
|
61
54
|
"string-width": "^7.2.0",
|
|
62
|
-
"@nan0web/core": "1.1.
|
|
55
|
+
"@nan0web/core": "1.1.2"
|
|
63
56
|
},
|
|
64
57
|
"scripts": {
|
|
65
58
|
"build": "tsc",
|
|
@@ -74,10 +67,13 @@
|
|
|
74
67
|
"test:release": "node --test \"releases/**/*.test.js\"",
|
|
75
68
|
"test:status": "nan0test status --hide-name",
|
|
76
69
|
"test:play": "node --test --test-timeout=3333 \"play/**/*.test.js\"",
|
|
77
|
-
"test:
|
|
78
|
-
"test:
|
|
79
|
-
"test:
|
|
80
|
-
"test:
|
|
70
|
+
"test:snapshots": "node src/testing/GalleryGenerator.js",
|
|
71
|
+
"test:gallery": "npm run test:snapshots && npx nan0gallery --dir=snapshots/core",
|
|
72
|
+
"test:audit": "node --test src/test/gallery_audit.test.js",
|
|
73
|
+
"test:ssg": "node --test --test-timeout=5000 \"src/test/ssg.test.js\"",
|
|
74
|
+
"test:e2e": "playwright test --ignore-snapshots test/e2e/components.spec.js test/e2e/debug-label.spec.js",
|
|
75
|
+
"test:e2e:slow": "E2E_SLOW=1 playwright test test/e2e/visual.spec.js",
|
|
76
|
+
"test:all": "npm run test && npm run test:docs && npm run test:play && npm run test:gallery && npm run test:audit && npm run test:e2e && npm run knip",
|
|
81
77
|
"knip": "knip --production",
|
|
82
78
|
"precommit": "npm test",
|
|
83
79
|
"prepush": "npm test",
|
package/src/Model/index.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import User from './User/User.js'
|
|
2
|
+
import * as DomainModels from '../domain/index.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export default {
|
|
4
|
+
const Model = {
|
|
6
5
|
User,
|
|
6
|
+
...DomainModels,
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
export { User }
|
|
10
|
+
export const {
|
|
11
|
+
HeaderModel,
|
|
12
|
+
FooterModel,
|
|
13
|
+
HeroModel,
|
|
14
|
+
ButtonModel,
|
|
15
|
+
ConfirmModel,
|
|
16
|
+
InputModel,
|
|
17
|
+
SpinnerModel,
|
|
18
|
+
TableModel,
|
|
19
|
+
ToastModel,
|
|
20
|
+
SelectModel,
|
|
21
|
+
AutocompleteModel,
|
|
22
|
+
TreeModel,
|
|
23
|
+
TabsModel,
|
|
24
|
+
AccordionModel,
|
|
25
|
+
GalleryModel,
|
|
26
|
+
PriceModel,
|
|
27
|
+
PricingModel,
|
|
28
|
+
CommentModel,
|
|
29
|
+
TestimonialModel,
|
|
30
|
+
StatsItemModel,
|
|
31
|
+
StatsModel,
|
|
32
|
+
TimelineItemModel,
|
|
33
|
+
TimelineModel,
|
|
34
|
+
} = DomainModels
|
|
35
|
+
|
|
36
|
+
export default Model
|
package/src/core/Form/Form.js
CHANGED
|
@@ -106,10 +106,11 @@ export default class UIForm extends FormMessage {
|
|
|
106
106
|
/**
|
|
107
107
|
* Validates the entire form.
|
|
108
108
|
*
|
|
109
|
-
* @returns {
|
|
109
|
+
* @returns {any}
|
|
110
110
|
*/
|
|
111
111
|
validate() {
|
|
112
|
-
|
|
112
|
+
/** @type {any} */
|
|
113
|
+
const errors = {}
|
|
113
114
|
let isValid = true
|
|
114
115
|
|
|
115
116
|
this.fields.forEach((field) => {
|
|
@@ -120,7 +121,7 @@ export default class UIForm extends FormMessage {
|
|
|
120
121
|
field.required &&
|
|
121
122
|
(fieldValue === '' || fieldValue === null || fieldValue === undefined)
|
|
122
123
|
) {
|
|
123
|
-
errors
|
|
124
|
+
errors[field.name] = 'This field is required'
|
|
124
125
|
isValid = false
|
|
125
126
|
return
|
|
126
127
|
}
|
|
@@ -132,14 +133,12 @@ export default class UIForm extends FormMessage {
|
|
|
132
133
|
)
|
|
133
134
|
|
|
134
135
|
if (!fieldValid) {
|
|
135
|
-
|
|
136
|
-
errors.set(key, err)
|
|
137
|
-
}
|
|
136
|
+
Object.assign(errors, fieldErrors)
|
|
138
137
|
isValid = false
|
|
139
138
|
}
|
|
140
139
|
})
|
|
141
140
|
|
|
142
|
-
return errors
|
|
141
|
+
return { isValid, errors }
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
/**
|
|
@@ -239,6 +238,8 @@ export default class UIForm extends FormMessage {
|
|
|
239
238
|
*/
|
|
240
239
|
toJSON() {
|
|
241
240
|
return {
|
|
241
|
+
id: this.id,
|
|
242
|
+
type: this.type,
|
|
242
243
|
time: new Date(this.time).toISOString(),
|
|
243
244
|
title: this.title,
|
|
244
245
|
fields: this.fields.map((f) => (f.toJSON ? f.toJSON() : f)),
|
package/src/core/Form/Message.js
CHANGED
|
@@ -14,7 +14,7 @@ export default class FormMessage extends UiMessage {
|
|
|
14
14
|
* @param {Object} [input={}] - Message properties.
|
|
15
15
|
*/
|
|
16
16
|
constructor(input = {}) {
|
|
17
|
-
super(input)
|
|
17
|
+
super({ type: 'form', ...input })
|
|
18
18
|
const { data = {}, schema = {} } = input
|
|
19
19
|
|
|
20
20
|
// Store data and schema for easy access
|
|
@@ -23,6 +23,8 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
23
23
|
* Handler for 'progress' intents. Optional (defaults to no-op).
|
|
24
24
|
* @property {(intent: import('./Intent.js').LogIntent) => void | Promise<void>} [log]
|
|
25
25
|
* Handler for 'log' intents. Optional (defaults to no-op).
|
|
26
|
+
* @property {(intent: import('./Intent.js').RenderIntent) => void | Promise<void>} [render]
|
|
27
|
+
* Handler for 'render' intents (visual component injection). Optional.
|
|
26
28
|
* @property {(intent: import('./Intent.js').ResultIntent) => void | Promise<void>} [result]
|
|
27
29
|
* Handler for the final 'result'. Optional (defaults to no-op).
|
|
28
30
|
*/
|
|
@@ -206,6 +208,14 @@ export async function runGenerator(generator, handlers, options = {}) {
|
|
|
206
208
|
break
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
case 'render': {
|
|
212
|
+
if (handlers.render) {
|
|
213
|
+
await handlers.render(intent)
|
|
214
|
+
}
|
|
215
|
+
nextVal = undefined
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
|
|
209
219
|
default:
|
|
210
220
|
throw IntentErrorModel.error('unhandled_intent', { type: /** @type {any} */ (intent).type })
|
|
211
221
|
}
|
package/src/core/Intent.js
CHANGED
|
@@ -58,9 +58,18 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
58
58
|
* @property {*} data - The raw result data (JSON-serializable).
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Model requests rendering of a pure UI component (Header, Footer, Static Map).
|
|
63
|
+
* No response expected from the logic loop.
|
|
64
|
+
* @typedef {Object} RenderIntent
|
|
65
|
+
* @property {'render'} type
|
|
66
|
+
* @property {string} component - Component name (e.g. 'App.Layout.Header').
|
|
67
|
+
* @property {object} props - Static props for the component.
|
|
68
|
+
*/
|
|
69
|
+
|
|
61
70
|
/**
|
|
62
71
|
* Union of all possible yielded intents.
|
|
63
|
-
* @typedef {AskIntent | ProgressIntent | LogIntent} Intent
|
|
72
|
+
* @typedef {AskIntent | ProgressIntent | LogIntent | RenderIntent} Intent
|
|
64
73
|
*/
|
|
65
74
|
|
|
66
75
|
// ─── Response Types (Adapter → Model) ───
|
|
@@ -95,7 +104,7 @@ import { IntentErrorModel } from './IntentErrorModel.js'
|
|
|
95
104
|
* @typedef {AskResponse | AbortResponse | undefined} IntentResponse
|
|
96
105
|
*/
|
|
97
106
|
|
|
98
|
-
export const INTENT_TYPES = /** @type {const} */ (['ask', 'progress', 'log'])
|
|
107
|
+
export const INTENT_TYPES = /** @type {const} */ (['ask', 'progress', 'log', 'render'])
|
|
99
108
|
|
|
100
109
|
/**
|
|
101
110
|
* Detects if a value is a Model-as-Schema class (has static fields with `help`).
|
|
@@ -131,17 +140,23 @@ export function validateIntent(intent) {
|
|
|
131
140
|
if (typeof intent.field !== 'string' || !intent.field) {
|
|
132
141
|
throw IntentErrorModel.error('ask_missing_field')
|
|
133
142
|
}
|
|
134
|
-
// Accept both: plain schema {help: '...'} and Model-as-Schema class
|
|
135
|
-
const isModel = intent.model
|
|
143
|
+
// Accept both: plain schema {help: '...'} and Model-as-Schema class/instance
|
|
144
|
+
const isModel = !!intent.model
|
|
136
145
|
if (!isModel && (!intent.schema || typeof intent.schema !== 'object' || !('help' in intent.schema))) {
|
|
137
146
|
throw IntentErrorModel.error('ask_missing_schema_help')
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
if (intent.type === 'progress' || intent.type === 'log') {
|
|
141
|
-
|
|
150
|
+
const isComponentLog = intent.type === 'log' && intent.component
|
|
151
|
+
if (!isComponentLog && typeof intent.message !== 'string') {
|
|
142
152
|
throw IntentErrorModel.error('intent_missing_message', { type: intent.type })
|
|
143
153
|
}
|
|
144
154
|
}
|
|
155
|
+
if (intent.type === 'render') {
|
|
156
|
+
if (typeof intent.component !== 'string' || !intent.component) {
|
|
157
|
+
throw IntentErrorModel.error('render_missing_component')
|
|
158
|
+
}
|
|
159
|
+
}
|
|
145
160
|
return true
|
|
146
161
|
}
|
|
147
162
|
|
|
@@ -165,4 +180,5 @@ export const ask = (field, schema) => {
|
|
|
165
180
|
|
|
166
181
|
export const progress = (message) => ({ type: 'progress', message })
|
|
167
182
|
export const log = (level, message, data = {}) => ({ type: 'log', level, message, ...data })
|
|
183
|
+
export const render = (component, props = {}) => ({ type: 'render', component, props })
|
|
168
184
|
export const result = (data) => ({ type: 'result', data })
|
|
@@ -39,7 +39,12 @@ export class IntentErrorModel {
|
|
|
39
39
|
|
|
40
40
|
static intent_missing_message = {
|
|
41
41
|
help: 'Progress and Log intents require a message',
|
|
42
|
-
error: '
|
|
42
|
+
error: "'{type}' intent requires a \"message\" string",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static render_missing_component = {
|
|
46
|
+
help: 'Render intent requires a component name',
|
|
47
|
+
error: 'Render intent requires a non-empty "component" string',
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
// ─── Runner Contract Errors ───
|
package/src/core/Stream.js
CHANGED
|
@@ -44,19 +44,30 @@ export default class UIStream {
|
|
|
44
44
|
static async process(signal, generatorFn, onProgress, onError, onComplete) {
|
|
45
45
|
const iter = generatorFn()
|
|
46
46
|
|
|
47
|
+
/** @type {Promise<never>} */
|
|
48
|
+
const abortPromise = new Promise((_, reject) => {
|
|
49
|
+
const onAbort = () => reject(new DOMException('The operation was aborted', 'AbortError'))
|
|
50
|
+
if (signal.aborted) return onAbort()
|
|
51
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
47
54
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
while (true) {
|
|
56
|
+
const { value: item, done } = await Promise.race([iter.next(), abortPromise])
|
|
57
|
+
|
|
58
|
+
if (done) {
|
|
59
|
+
if (item) onComplete?.(item)
|
|
60
|
+
break
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
if (item.done) {
|
|
54
64
|
onComplete?.(item)
|
|
55
65
|
break
|
|
56
|
-
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (item.error) {
|
|
57
69
|
onError?.(item.error, item)
|
|
58
70
|
} else {
|
|
59
|
-
// Intermediate results
|
|
60
71
|
onProgress?.(null, item)
|
|
61
72
|
}
|
|
62
73
|
}
|
package/src/core/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export {
|
|
|
41
41
|
export { default as Flow } from './Flow.js'
|
|
42
42
|
|
|
43
43
|
// OLMUI Generator Engine — Intent-based Model→Adapter contract
|
|
44
|
-
export { validateIntent, ask, progress, log, result, INTENT_TYPES, isModelSchema } from './Intent.js'
|
|
44
|
+
export { validateIntent, ask, progress, log, render, result, INTENT_TYPES, isModelSchema } from './Intent.js'
|
|
45
45
|
export { IntentErrorModel } from './IntentErrorModel.js'
|
|
46
46
|
export { runGenerator } from './GeneratorRunner.js'
|
|
47
47
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Model } from '@nan0web/types'
|
|
2
|
+
import Navigation from './Navigation.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FooterModel — OLMUI Model-as-Schema
|
|
6
|
+
* Universal footer structure: copyright, version, license, navigation, sharing, languages.
|
|
7
|
+
*/
|
|
8
|
+
export default class FooterModel extends Model {
|
|
9
|
+
static $id = '@nan0web/ui/FooterModel'
|
|
10
|
+
|
|
11
|
+
static copyright = {
|
|
12
|
+
help: 'Copyright text',
|
|
13
|
+
placeholder: '© 2026 Company',
|
|
14
|
+
default: '',
|
|
15
|
+
}
|
|
16
|
+
static version = {
|
|
17
|
+
help: 'Application version string',
|
|
18
|
+
placeholder: '1.0.0',
|
|
19
|
+
default: '',
|
|
20
|
+
}
|
|
21
|
+
static license = {
|
|
22
|
+
help: 'License type',
|
|
23
|
+
placeholder: 'ISC',
|
|
24
|
+
default: '',
|
|
25
|
+
}
|
|
26
|
+
static nav = {
|
|
27
|
+
help: 'Footer navigation links',
|
|
28
|
+
type: 'Navigation[]',
|
|
29
|
+
hint: Navigation,
|
|
30
|
+
default: [],
|
|
31
|
+
}
|
|
32
|
+
static share = {
|
|
33
|
+
help: 'Social sharing links',
|
|
34
|
+
type: 'Navigation[]',
|
|
35
|
+
hint: Navigation,
|
|
36
|
+
default: [],
|
|
37
|
+
}
|
|
38
|
+
static langs = {
|
|
39
|
+
help: 'Available languages for switcher',
|
|
40
|
+
type: 'Language[]',
|
|
41
|
+
default: [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {Partial<FooterModel> | Record<string, any>} data Model input data.
|
|
46
|
+
* @param {object} [options] Extended options (db, etc.)
|
|
47
|
+
*/
|
|
48
|
+
constructor(data = {}, options = {}) {
|
|
49
|
+
super(data, options)
|
|
50
|
+
/** @type {string} Copyright text */ this.copyright
|
|
51
|
+
/** @type {string} Application version string */ this.version
|
|
52
|
+
/** @type {string} License type */ this.license
|
|
53
|
+
/** @type {Navigation[]} Footer navigation links */ this.nav
|
|
54
|
+
/** @type {Navigation[]} Social sharing links */ this.share
|
|
55
|
+
/** @type {any[]} Available languages for switcher */ this.langs
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Model } from '@nan0web/types'
|
|
2
|
+
import Navigation from './Navigation.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HeaderModel — OLMUI Model-as-Schema
|
|
6
|
+
* Universal header structure: logo, navigation, language switcher, actions.
|
|
7
|
+
*/
|
|
8
|
+
export default class HeaderModel extends Model {
|
|
9
|
+
static $id = '@nan0web/ui/HeaderModel'
|
|
10
|
+
|
|
11
|
+
static title = {
|
|
12
|
+
help: 'Site or app title displayed in the header',
|
|
13
|
+
placeholder: 'My App',
|
|
14
|
+
default: '',
|
|
15
|
+
}
|
|
16
|
+
static logo = {
|
|
17
|
+
help: 'Logo image URL or icon name',
|
|
18
|
+
placeholder: 'https://...',
|
|
19
|
+
default: '',
|
|
20
|
+
}
|
|
21
|
+
static actions = {
|
|
22
|
+
help: 'Header action links (CTA, Sign In, etc.)',
|
|
23
|
+
type: 'Navigation[]',
|
|
24
|
+
hint: Navigation,
|
|
25
|
+
default: [],
|
|
26
|
+
}
|
|
27
|
+
static lang = {
|
|
28
|
+
help: 'Currently active language',
|
|
29
|
+
type: 'Language',
|
|
30
|
+
default: null,
|
|
31
|
+
}
|
|
32
|
+
static langs = {
|
|
33
|
+
help: 'Available languages for switcher',
|
|
34
|
+
type: 'Language[]',
|
|
35
|
+
default: [],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Partial<HeaderModel> | Record<string, any>} data Model input data.
|
|
40
|
+
* @param {object} [options] Extended options (db, etc.)
|
|
41
|
+
*/
|
|
42
|
+
constructor(data = {}, options = {}) {
|
|
43
|
+
super(data, options)
|
|
44
|
+
/** @type {string} Site or app title displayed in the header */ this.title
|
|
45
|
+
/** @type {string} Logo image URL or icon name */ this.logo
|
|
46
|
+
/** @type {Navigation[]} Header action links (CTA, Sign In, etc.) */ this.actions
|
|
47
|
+
/** @type {any|null} Currently active language */ this.lang
|
|
48
|
+
/** @type {any[]} Available languages for switcher */ this.langs
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Model } from '@nan0web/types'
|
|
2
|
+
import Navigation from './Navigation.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HeroModel — OLMUI Model-as-Schema
|
|
6
|
+
* Universal hero/banner section for landing pages.
|
|
7
|
+
* Uses Navigation[] for actions instead of a single CTA.
|
|
8
|
+
*/
|
|
9
|
+
export default class HeroModel extends Model {
|
|
10
|
+
static $id = '@nan0web/ui/HeroModel'
|
|
11
|
+
|
|
12
|
+
static title = {
|
|
13
|
+
help: 'Hero main headline',
|
|
14
|
+
placeholder: 'Welcome to Our Platform',
|
|
15
|
+
default: '',
|
|
16
|
+
required: true,
|
|
17
|
+
}
|
|
18
|
+
static description = {
|
|
19
|
+
help: 'Hero sub-headline or description text',
|
|
20
|
+
placeholder: 'Build something amazing...',
|
|
21
|
+
default: '',
|
|
22
|
+
}
|
|
23
|
+
static image = {
|
|
24
|
+
help: 'Hero background or feature image URL',
|
|
25
|
+
placeholder: 'https://...',
|
|
26
|
+
hint: 'image',
|
|
27
|
+
upload: true,
|
|
28
|
+
default: '',
|
|
29
|
+
}
|
|
30
|
+
static actions = {
|
|
31
|
+
help: 'Call-to-action buttons (multiple CTA support)',
|
|
32
|
+
type: 'Navigation[]',
|
|
33
|
+
hint: Navigation,
|
|
34
|
+
default: [],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {Partial<HeroModel> | Record<string, any>} data Model input data.
|
|
39
|
+
* @param {object} [options] Extended options (db, etc.)
|
|
40
|
+
*/
|
|
41
|
+
constructor(data = {}, options = {}) {
|
|
42
|
+
super(data, options)
|
|
43
|
+
/** @type {string} Hero main headline */ this.title
|
|
44
|
+
/** @type {string} Hero sub-headline or description text */ this.description
|
|
45
|
+
/** @type {string} Hero background or feature image URL */ this.image
|
|
46
|
+
/** @type {Navigation[]} Call-to-action buttons (multiple CTA support) */ this.actions
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/domain/Navigation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Model } from '@nan0web/
|
|
1
|
+
import { Model } from '@nan0web/types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Navigation Model — OLMUI Model-as-Schema
|
|
@@ -41,16 +41,17 @@ export default class Navigation extends Model {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* @param {Partial<Navigation>} data
|
|
44
|
+
* @param {Partial<Navigation> | Record<string, any>} data Model input data.
|
|
45
|
+
* @param {object} [options] Extended options (db, etc.)
|
|
45
46
|
*/
|
|
46
|
-
constructor(data = {}) {
|
|
47
|
-
super(data)
|
|
48
|
-
/** @type {string
|
|
49
|
-
/** @type {string
|
|
50
|
-
/** @type {string
|
|
51
|
-
/** @type {string
|
|
52
|
-
/** @type {Navigation[]
|
|
53
|
-
/** @type {boolean
|
|
47
|
+
constructor(data = {}, options = {}) {
|
|
48
|
+
super(data, options)
|
|
49
|
+
/** @type {string} Label for the menu item */ this.title
|
|
50
|
+
/** @type {string} URL or internal app route */ this.href
|
|
51
|
+
/** @type {string} Icon name/ID */ this.icon
|
|
52
|
+
/** @type {string} Display image or thumbnail */ this.image
|
|
53
|
+
/** @type {Navigation[]} Nested sub-menu navigation */ this.children
|
|
54
|
+
/** @type {boolean} Hide from lists/menus */ this.hidden
|
|
54
55
|
|
|
55
56
|
if (this.children) {
|
|
56
57
|
this.children = this.children.map((item) => new Navigation(item))
|