@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.
Files changed (107) hide show
  1. package/README.md +29 -10
  2. package/package.json +18 -22
  3. package/src/Model/index.js +32 -3
  4. package/src/core/Form/Form.js +8 -7
  5. package/src/core/Form/Message.js +1 -1
  6. package/src/core/GeneratorRunner.js +10 -0
  7. package/src/core/Intent.js +21 -5
  8. package/src/core/IntentErrorModel.js +6 -1
  9. package/src/core/Stream.js +16 -5
  10. package/src/core/index.js +1 -1
  11. package/src/domain/FooterModel.js +57 -0
  12. package/src/domain/HeaderModel.js +50 -0
  13. package/src/domain/HeroModel.js +48 -0
  14. package/src/domain/Navigation.js +11 -10
  15. package/src/domain/SandboxModel.js +66 -115
  16. package/src/domain/ShowcaseAppModel.js +133 -50
  17. package/src/domain/components/AccordionModel.js +38 -0
  18. package/src/domain/components/AutocompleteModel.js +11 -21
  19. package/src/domain/components/BannerModel.js +37 -0
  20. package/src/domain/components/BreadcrumbModel.js +11 -9
  21. package/src/domain/components/ButtonModel.js +31 -58
  22. package/src/domain/components/CommentModel.js +44 -0
  23. package/src/domain/components/ConfirmModel.js +26 -33
  24. package/src/domain/components/EmptyStateModel.js +45 -0
  25. package/src/domain/components/FAQModel.js +32 -0
  26. package/src/domain/components/FooterConfigModel.js +26 -0
  27. package/src/domain/components/FooterVisibilityModel.js +48 -0
  28. package/src/domain/components/GalleryModel.js +36 -0
  29. package/src/domain/components/HeaderConfigModel.js +26 -0
  30. package/src/domain/components/HeaderVisibilityModel.js +54 -0
  31. package/src/domain/components/InputModel.js +21 -41
  32. package/src/domain/components/PriceModel.js +30 -0
  33. package/src/domain/components/PricingModel.js +39 -0
  34. package/src/domain/components/PricingSectionModel.js +32 -0
  35. package/src/domain/components/ProfileDropdownModel.js +45 -0
  36. package/src/domain/components/SelectModel.js +11 -21
  37. package/src/domain/components/SpinnerModel.js +11 -26
  38. package/src/domain/components/StatsItemModel.js +38 -0
  39. package/src/domain/components/StatsModel.js +32 -0
  40. package/src/domain/components/TableModel.js +11 -24
  41. package/src/domain/components/TabsModel.js +30 -0
  42. package/src/domain/components/TestimonialModel.js +24 -0
  43. package/src/domain/components/TimelineItemModel.js +38 -0
  44. package/src/domain/components/TimelineModel.js +32 -0
  45. package/src/domain/components/ToastModel.js +24 -51
  46. package/src/domain/components/TreeModel.js +10 -26
  47. package/src/domain/components/index.js +34 -0
  48. package/src/domain/index.js +24 -0
  49. package/src/index.js +2 -0
  50. package/src/testing/GalleryGenerator.js +85 -0
  51. package/src/testing/LogicInspector.js +55 -0
  52. package/src/testing/SnapshotInspector.js +84 -0
  53. package/src/testing/VisualAdapter.js +41 -0
  54. package/src/testing/index.js +3 -0
  55. package/types/Model/index.d.ts +62 -4
  56. package/types/core/Form/Form.d.ts +2 -2
  57. package/types/core/GeneratorRunner.d.ts +4 -0
  58. package/types/core/Intent.d.ts +31 -3
  59. package/types/core/IntentErrorModel.d.ts +4 -0
  60. package/types/core/index.d.ts +1 -1
  61. package/types/domain/FooterModel.d.ts +52 -0
  62. package/types/domain/HeaderModel.d.ts +45 -0
  63. package/types/domain/HeroModel.d.ts +43 -0
  64. package/types/domain/Navigation.d.ts +10 -9
  65. package/types/domain/SandboxModel.d.ts +16 -40
  66. package/types/domain/ShowcaseAppModel.d.ts +26 -54
  67. package/types/domain/components/AccordionModel.d.ts +33 -0
  68. package/types/domain/components/AutocompleteModel.d.ts +10 -29
  69. package/types/domain/components/BannerModel.d.ts +32 -0
  70. package/types/domain/components/BreadcrumbModel.d.ts +13 -6
  71. package/types/domain/components/ButtonModel.d.ts +18 -54
  72. package/types/domain/components/CommentModel.d.ts +39 -0
  73. package/types/domain/components/ConfirmModel.d.ts +20 -35
  74. package/types/domain/components/EmptyStateModel.d.ts +40 -0
  75. package/types/domain/components/FAQModel.d.ts +27 -0
  76. package/types/domain/components/FooterConfigModel.d.ts +21 -0
  77. package/types/domain/components/FooterVisibilityModel.d.ts +43 -0
  78. package/types/domain/components/GalleryModel.d.ts +35 -0
  79. package/types/domain/components/HeaderConfigModel.d.ts +21 -0
  80. package/types/domain/components/HeaderVisibilityModel.d.ts +49 -0
  81. package/types/domain/components/HeroModel.d.ts +24 -0
  82. package/types/domain/components/InputModel.d.ts +19 -59
  83. package/types/domain/components/PriceModel.d.ts +25 -0
  84. package/types/domain/components/PricingModel.d.ts +34 -0
  85. package/types/domain/components/PricingSectionModel.d.ts +27 -0
  86. package/types/domain/components/ProfileDropdownModel.d.ts +40 -0
  87. package/types/domain/components/SelectModel.d.ts +13 -28
  88. package/types/domain/components/ShowcaseAppModel.d.ts +32 -0
  89. package/types/domain/components/SpinnerModel.d.ts +10 -27
  90. package/types/domain/components/StatsItemModel.d.ts +33 -0
  91. package/types/domain/components/StatsModel.d.ts +27 -0
  92. package/types/domain/components/TableModel.d.ts +10 -26
  93. package/types/domain/components/TabsModel.d.ts +28 -0
  94. package/types/domain/components/TestimonialModel.d.ts +18 -0
  95. package/types/domain/components/TimelineItemModel.d.ts +33 -0
  96. package/types/domain/components/TimelineModel.d.ts +27 -0
  97. package/types/domain/components/ToastModel.d.ts +16 -45
  98. package/types/domain/components/TreeModel.d.ts +13 -36
  99. package/types/domain/components/index.d.ts +20 -0
  100. package/types/domain/index.d.ts +4 -1
  101. package/types/index.d.ts +1 -0
  102. package/types/testing/GalleryGenerator.d.ts +1 -0
  103. package/types/testing/LogicInspector.d.ts +22 -0
  104. package/types/testing/SnapshotInspector.d.ts +17 -0
  105. package/types/testing/VisualAdapter.d.ts +15 -0
  106. package/types/testing/index.d.ts +3 -0
  107. 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.size) // ← 1
95
- console.info(errors.get('email')) // ← Invalid email format
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
- UI models are plain data objects managed by `Model` classes.
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
- - `User` – user data
156
+ #### Layout Models
157
+ - `HeaderModel` — title, logo, navigation actions
158
+ - `FooterModel` — copyright, version, social links
159
+ - `HeroModel` — prominent call-to-action
156
160
 
157
- How to use a User model?
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 user = new Model.User({ name: 'Charlie', email: 'charlie@example.com' })
161
- console.info(user.name) // Charlie
162
- console.info(user.email) // ← charlie@example.com
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.8.0",
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
- "import": "./src/Component/index.js",
20
- "types": "./types/Component/index.d.ts"
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.3.0",
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.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:ssg": "node --test --test-timeout=5000 \"ssg/ssg.test.js\"",
78
- "test:e2e": "playwright test --ignore-snapshots e2e/components.spec.js e2e/debug-label.spec.js",
79
- "test:e2e:slow": "E2E_SLOW=1 playwright test e2e/visual.spec.js",
80
- "test:all": "npm run test && npm run test:docs && npm run test:play && npm run test:e2e && npm run knip",
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",
@@ -1,7 +1,36 @@
1
1
  import User from './User/User.js'
2
+ import * as DomainModels from '../domain/index.js'
2
3
 
3
- export { User }
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
@@ -106,10 +106,11 @@ export default class UIForm extends FormMessage {
106
106
  /**
107
107
  * Validates the entire form.
108
108
  *
109
- * @returns {Map<string, string>} Map of validation errors, empty if valid.
109
+ * @returns {any}
110
110
  */
111
111
  validate() {
112
- const errors = new Map()
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.set(field.name, 'This field is required')
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
- for (const [key, err] of Object.entries(fieldErrors)) {
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)),
@@ -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
  }
@@ -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 === true
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
- if (typeof intent.message !== 'string') {
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: '\'{type}\' intent requires a "message" string',
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 ───
@@ -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
- for await (const item of iter) {
49
- if (signal.aborted) {
50
- throw new DOMException('Aborted', 'AbortError')
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
- } else if (item.error) {
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
+ }
@@ -1,4 +1,4 @@
1
- import { Model } from '@nan0web/core'
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|undefined} */ this.title
49
- /** @type {string|undefined} */ this.href
50
- /** @type {string|undefined} */ this.icon
51
- /** @type {string|undefined} */ this.image
52
- /** @type {Navigation[]|undefined} */ this.children
53
- /** @type {boolean|undefined} */ this.hidden
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))