@nan0web/ui 1.9.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 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.9.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"
@@ -73,10 +67,13 @@
73
67
  "test:release": "node --test \"releases/**/*.test.js\"",
74
68
  "test:status": "nan0test status --hide-name",
75
69
  "test:play": "node --test --test-timeout=3333 \"play/**/*.test.js\"",
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",
76
73
  "test:ssg": "node --test --test-timeout=5000 \"src/test/ssg.test.js\"",
77
74
  "test:e2e": "playwright test --ignore-snapshots test/e2e/components.spec.js test/e2e/debug-label.spec.js",
78
75
  "test:e2e:slow": "E2E_SLOW=1 playwright test test/e2e/visual.spec.js",
79
- "test:all": "npm run test && npm run test:docs && npm run test:play && npm run test:e2e && npm run knip",
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",
80
77
  "knip": "knip --production",
81
78
  "precommit": "npm test",
82
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`).
@@ -143,6 +152,11 @@ export function validateIntent(intent) {
143
152
  throw IntentErrorModel.error('intent_missing_message', { type: intent.type })
144
153
  }
145
154
  }
155
+ if (intent.type === 'render') {
156
+ if (typeof intent.component !== 'string' || !intent.component) {
157
+ throw IntentErrorModel.error('render_missing_component')
158
+ }
159
+ }
146
160
  return true
147
161
  }
148
162
 
@@ -166,4 +180,5 @@ export const ask = (field, schema) => {
166
180
 
167
181
  export const progress = (message) => ({ type: 'progress', message })
168
182
  export const log = (level, message, data = {}) => ({ type: 'log', level, message, ...data })
183
+ export const render = (component, props = {}) => ({ type: 'render', component, props })
169
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,85 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import yaml from 'js-yaml'
5
+ import { LogicInspector } from './LogicInspector.js'
6
+ import { VisualAdapter } from './VisualAdapter.js'
7
+ import * as Models from '../domain/index.js'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+ const rootDir = path.resolve(__dirname, '../../')
11
+ const dataDir = path.resolve(rootDir, 'docs/data')
12
+ const snapshotsDir = path.resolve(rootDir, 'snapshots/core')
13
+
14
+ // Clean before generation
15
+ if (fs.existsSync(snapshotsDir)) fs.rmSync(snapshotsDir, { recursive: true, force: true })
16
+ fs.mkdirSync(snapshotsDir, { recursive: true })
17
+
18
+ const groups = {
19
+ Actions: ['Button', 'Toggle'],
20
+ Forms: ['Input', 'Select', 'Slider', 'Autocomplete', 'Color', 'Shadow'],
21
+ Data: ['Accordion', 'Card', 'Sortable', 'Table', 'Tree', 'CodeBlock', 'Markdown', 'Badge'],
22
+ Feedback: ['Alert', 'Confirm', 'Modal', 'ProgressBar', 'Spinner', 'Toast'],
23
+ System: ['LangSelect', 'ThemeToggle'],
24
+ }
25
+
26
+ function getCategory(comp) {
27
+ for (const [cat, comps] of Object.entries(groups)) {
28
+ if (comps.includes(comp)) return cat
29
+ }
30
+ return 'Other'
31
+ }
32
+
33
+ async function generate() {
34
+ const langs = fs.readdirSync(dataDir).filter(d => fs.statSync(path.join(dataDir, d)).isDirectory() && d !== '_')
35
+
36
+ for (const lang of langs) {
37
+ const langDir = path.join(dataDir, lang)
38
+ const components = fs.readdirSync(langDir).filter(f => f.endsWith('.yaml'))
39
+
40
+ for (const file of components) {
41
+ const compName = file.replace('.yaml', '')
42
+ const category = getCategory(compName)
43
+ const text = fs.readFileSync(path.join(langDir, file), 'utf-8')
44
+ const data = yaml.load(text)
45
+
46
+ // Variations are in data.content
47
+ const variations = data.content || []
48
+
49
+ for (let i = 0; i < variations.length; i++) {
50
+ const varData = variations[i][compName] || variations[i]
51
+
52
+ // Get variation name from 'content', 'title', 'message', or fallback to index
53
+ let varName = varData.content || varData.title || varData.message || `var${i + 1}`
54
+ if (typeof varName !== 'string') varName = `var${i + 1}`
55
+
56
+ // Clean filename: allow Ukrainian, but replace spaces/special chars with single underscore
57
+ const safeVarName = varName
58
+ .trim()
59
+ .toLowerCase()
60
+ .replace(/[./\\:]/g, '_') // Replace paths/dots
61
+ .replace(/\s+/g, '_') // Replace spaces
62
+ .replace(/_{2,}/g, '_') // No double underscores
63
+ .slice(0, 50) // Max length
64
+
65
+ // Logic Capture
66
+ /** @type {() => AsyncGenerator<import('../core/Intent.js').Intent, import('../core/Intent.js').ResultIntent, any>} */
67
+ const modelStream = async function* () {
68
+ yield { type: 'render', component: `ui-${compName.toLowerCase()}`, props: varData }
69
+ return { type: 'result', data: { ok: true } }
70
+ }
71
+
72
+ const intents = await LogicInspector.capture(modelStream())
73
+ const snapshot = intents.map(it => VisualAdapter.render(it)).join('\n')
74
+
75
+ const outPath = path.join(snapshotsDir, lang, category, compName)
76
+ if (!fs.existsSync(outPath)) fs.mkdirSync(outPath, { recursive: true })
77
+
78
+ fs.writeFileSync(path.join(outPath, `${safeVarName}.txt`), snapshot)
79
+ console.log(`📸 Generated snapshot for ${lang}/${category}/${compName}/${safeVarName}`)
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ generate().catch(console.error)
@@ -0,0 +1,55 @@
1
+ import { runGenerator } from '../core/GeneratorRunner.js'
2
+
3
+ /**
4
+ * LogicInspector
5
+ *
6
+ * Базовий клас для захоплення "Логічних зліпків" (Intent Stream) будь-яких моделей OLMUI.
7
+ * Дозволяє виконувати чисто-логічне тестування без прив'язки до рендерингу.
8
+ */
9
+ export class LogicInspector {
10
+ /**
11
+ * Виконує генератор моделі та записує послідовність усіх інтенцій.
12
+ * @param {AsyncGenerator<import('../core/Intent.js').Intent, import('../core/Intent.js').ResultIntent, import('../core/Intent.js').IntentResponse>} modelStream - результат виклику model.run()
13
+ * @param {object} options
14
+ * @param {Array<any> | ((locale: string) => Array<any>)} [options.inputs] - черга вхідних значень для askIntent
15
+ * @param {string} [options.locale] - локаль для тестів
16
+ * @param {function} [options.t] - функція перекладу
17
+ * @returns {Promise<Array<any>>} Intent Stream Log
18
+ */
19
+ static async capture(modelStream, { inputs = [], locale = 'uk', t = (k) => k } = {}) {
20
+ const intents = []
21
+ let inputIdx = 0
22
+ const resolvedInputs = typeof inputs === 'function' ? inputs(locale) : inputs
23
+
24
+ const recordingAdapter = {
25
+ /** @param {import('../core/Intent.js').AskIntent} i */
26
+ ask: async (i) => {
27
+ const value = resolvedInputs[inputIdx++]
28
+ const entry = { type: 'ask', field: i.field, schema: i.schema, input: value }
29
+ intents.push(entry)
30
+ return { value }
31
+ },
32
+ /** @param {import('../core/Intent.js').LogIntent} i */
33
+ log: async (i) => {
34
+ intents.push({ type: 'log', level: i.level || 'info', message: i.message })
35
+ },
36
+ /** @param {import('../core/Intent.js').ProgressIntent} i */
37
+ progress: async (i) => {
38
+ intents.push({ type: 'progress', message: i.message })
39
+ },
40
+ /** @param {import('../core/Intent.js').RenderIntent} i */
41
+ render: async (i) => {
42
+ intents.push({ type: 'render', component: i.component, props: i.props })
43
+ },
44
+ /** @param {import('../core/Intent.js').ResultIntent} i */
45
+ result: async (i) => {
46
+ intents.push({ type: 'result', data: i.data })
47
+ },
48
+ t
49
+ }
50
+
51
+ // Викликаємо базовий раннер з нашим записуючим адаптером
52
+ await runGenerator(modelStream, recordingAdapter)
53
+ return intents
54
+ }
55
+ }
@@ -0,0 +1,84 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ /**
5
+ * SnapshotInspector
6
+ *
7
+ * Рев'ювер для автоматичної перевірки Snapshot-зліпків на наявність артефактів,
8
+ * неперекладених ключів та структурних помилок.
9
+ * Реалізує правила "Zero-Hallucination Snapshot Validation".
10
+ */
11
+ export class SnapshotInspector {
12
+ /**
13
+ * Перевіряє вміст одного снепшоту.
14
+ * @param {string} content - Текстовий вміст .txt файлу галереї
15
+ * @param {string} locale - Локаль (uk, en)
16
+ * @param {string} [filename] - Ім'я файлу для перевірки на "глюки" (підкреслення)
17
+ * @returns {object} { score, errors }
18
+ */
19
+ static inspect(content, locale = 'uk', filename = '') {
20
+ const errors = []
21
+ const lines = content.split('\n')
22
+
23
+ // 0. Перевірка імені файлу (на прохання архітектора)
24
+ if (filename) {
25
+ if (/__|--/.test(filename)) {
26
+ errors.push(`Filename "${filename}" has multiple consecutive separators (glitch detected).`)
27
+ }
28
+ if (filename.length < 3) {
29
+ errors.push(`Filename "${filename}" is too short.`)
30
+ }
31
+ }
32
+
33
+ lines.forEach((line, index) => {
34
+ const lineNum = index + 1
35
+ const trimmed = line.trim()
36
+
37
+ // 1. Неперекладені i18n ключі (крапки в словах)
38
+ // Виключаємо імена компонентів, атрибути RENDER-тегів, та числа з крапкою (версії, координати)
39
+ const isAttribute = trimmed.includes('="')
40
+ const isDotNumber = /^-?\d+\.\d+$/.test(trimmed)
41
+
42
+ if (/\w+\.\w+/.test(line) && !line.includes('ui-') && !line.includes('http') && !isAttribute && !isDotNumber) {
43
+ errors.push(`Line ${lineNum}: Possible untranslated key found: "${trimmed}"`)
44
+ }
45
+
46
+ // 2. Технічні артефакти
47
+ if (line.includes('[object Object]')) {
48
+ errors.push(`Line ${lineNum}: Critical artifact "[object Object]" found.`)
49
+ }
50
+ if (line.includes('undefined')) {
51
+ errors.push(`Line ${lineNum}: Critical artifact "undefined" found.`)
52
+ }
53
+ if (line.includes('NaN')) {
54
+ errors.push(`Line ${lineNum}: Critical artifact "NaN" found.`)
55
+ }
56
+
57
+ // 3. Англійські слова в українській локалі (базові)
58
+ if (locale === 'uk') {
59
+ const enWords = ['Back', 'Select', 'Cancel', 'Submit', 'Confirm', 'Delete', 'Information', 'Success', 'Warning', 'Error']
60
+ enWords.forEach(word => {
61
+ const regex = new RegExp(`\\b${word}\\b`, 'i')
62
+ // Перевіряємо тільки якщо це не частина тегу <ui-...> або атрибут
63
+ if (regex.test(line)) {
64
+ const isTag = line.includes(`<ui-${word.toLowerCase()}`) || line.includes(`[RENDER] <ui-`)
65
+ const isAsk = line.includes(`[ASK] ${word}`)
66
+ if (!isTag && !isAsk && !isAttribute) {
67
+ errors.push(`Line ${lineNum}: English word "${word}" found in "uk" locale.`)
68
+ }
69
+ }
70
+ })
71
+ }
72
+
73
+ // 4. Помилки роутингу
74
+ if (line.includes('🚨 Path not found')) {
75
+ errors.push(`Line ${lineNum}: Routing error "Path not found".`)
76
+ }
77
+ })
78
+
79
+ return {
80
+ score: errors.length === 0 ? 100 : Math.max(0, 100 - errors.length * 10),
81
+ errors
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * VisualAdapter (Base)
3
+ *
4
+ * Базовий клас для візуальної трансформації інтенцій OLMUI.
5
+ * Використовувана у @nan0web/ui як фундамент для спеціалізованих рендерерів.
6
+ */
7
+ export class VisualAdapter {
8
+ /**
9
+ * Конвертує одну інтенцію у просте текстове представлення.
10
+ * @param {object} intent - Intent entry from LogicInspector
11
+ * @param {function} [t] - i18n translate function
12
+ * @returns {string} Raw description
13
+ */
14
+ static render(intent, t = (k) => k) {
15
+ switch (intent.type) {
16
+ case 'ask':
17
+ return `[ASK] ${intent.field}: ${intent.input !== undefined ? intent.input : '...'}`
18
+ case 'progress':
19
+ return `[PROGRESS] ${intent.message || ''}`
20
+ case 'log':
21
+ return `[LOG ${intent.level?.toUpperCase() || 'INFO'}] ${typeof intent.message === 'object' ? JSON.stringify(intent.message) : intent.message}`
22
+ case 'render': {
23
+ const { content, ...propsData } = intent.props || {}
24
+ const props = Object.entries(propsData)
25
+ .map(([k, v]) => ` ${k}="${typeof v === 'object' ? JSON.stringify(v) : v}"`)
26
+ .join('\n')
27
+
28
+ if (content) {
29
+ const attrs = props ? `\n${props}\n` : ' '
30
+ return `[RENDER] <${intent.component}${attrs}>${content}</${intent.component}>`
31
+ }
32
+
33
+ return `[RENDER] <${intent.component}${props ? '\n' + props + '\n' : ''}>`
34
+ }
35
+ case 'result':
36
+ return `[RESULT] ${JSON.stringify(intent.data)}`
37
+ default:
38
+ return `[UNKNOWN: ${intent.type}] ${JSON.stringify(intent)}`
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,3 @@
1
+ export * from './LogicInspector.js'
2
+ export * from './VisualAdapter.js'
3
+ export * from './SnapshotInspector.js'
@@ -1,6 +1,64 @@
1
1
  export { User };
2
- declare namespace _default {
3
- export { User };
4
- }
5
- export default _default;
2
+ export const HeaderModel: typeof DomainModels.HeaderModel;
3
+ export const FooterModel: typeof DomainModels.FooterModel;
4
+ export const HeroModel: typeof DomainModels.HeroModel;
5
+ export const ButtonModel: typeof DomainModels.ButtonModel;
6
+ export const ConfirmModel: typeof DomainModels.ConfirmModel;
7
+ export const InputModel: typeof DomainModels.InputModel;
8
+ export const SpinnerModel: typeof DomainModels.SpinnerModel;
9
+ export const TableModel: typeof DomainModels.TableModel;
10
+ export const ToastModel: typeof DomainModels.ToastModel;
11
+ export const SelectModel: typeof DomainModels.SelectModel;
12
+ export const AutocompleteModel: typeof DomainModels.AutocompleteModel;
13
+ export const TreeModel: typeof DomainModels.TreeModel;
14
+ export const TabsModel: typeof DomainModels.TabsModel;
15
+ export const AccordionModel: typeof DomainModels.AccordionModel;
16
+ export const GalleryModel: typeof DomainModels.GalleryModel;
17
+ export const PriceModel: typeof DomainModels.PriceModel;
18
+ export const PricingModel: typeof DomainModels.PricingModel;
19
+ export const CommentModel: typeof DomainModels.CommentModel;
20
+ export const TestimonialModel: typeof DomainModels.TestimonialModel;
21
+ export const StatsItemModel: typeof DomainModels.StatsItemModel;
22
+ export const StatsModel: typeof DomainModels.StatsModel;
23
+ export const TimelineItemModel: typeof DomainModels.TimelineItemModel;
24
+ export const TimelineModel: typeof DomainModels.TimelineModel;
25
+ export default Model;
6
26
  import User from './User/User.js';
27
+ import * as DomainModels from '../domain/index.js';
28
+ declare const Model: {
29
+ SandboxModel: typeof DomainModels.SandboxModel;
30
+ ShowcaseAppModel: typeof DomainModels.ShowcaseAppModel;
31
+ Navigation: typeof DomainModels.Navigation;
32
+ Language: any;
33
+ HeaderModel: typeof DomainModels.HeaderModel;
34
+ FooterModel: typeof DomainModels.FooterModel;
35
+ HeroModel: typeof DomainModels.HeroModel;
36
+ ButtonModel: typeof DomainModels.ButtonModel;
37
+ ConfirmModel: typeof DomainModels.ConfirmModel;
38
+ InputModel: typeof DomainModels.InputModel;
39
+ SpinnerModel: typeof DomainModels.SpinnerModel;
40
+ TableModel: typeof DomainModels.TableModel;
41
+ ToastModel: typeof DomainModels.ToastModel;
42
+ SelectModel: typeof DomainModels.SelectModel;
43
+ AutocompleteModel: typeof DomainModels.AutocompleteModel;
44
+ TreeModel: typeof DomainModels.TreeModel;
45
+ TabsModel: typeof DomainModels.TabsModel;
46
+ AccordionModel: typeof DomainModels.AccordionModel;
47
+ GalleryModel: typeof DomainModels.GalleryModel;
48
+ PriceModel: typeof DomainModels.PriceModel;
49
+ PricingModel: typeof DomainModels.PricingModel;
50
+ CommentModel: typeof DomainModels.CommentModel;
51
+ TestimonialModel: typeof DomainModels.TestimonialModel;
52
+ StatsItemModel: typeof DomainModels.StatsItemModel;
53
+ StatsModel: typeof DomainModels.StatsModel;
54
+ TimelineItemModel: typeof DomainModels.TimelineItemModel;
55
+ TimelineModel: typeof DomainModels.TimelineModel;
56
+ HeaderVisibilityModel: typeof DomainModels.HeaderVisibilityModel;
57
+ HeaderConfigModel: typeof DomainModels.HeaderConfigModel;
58
+ FooterVisibilityModel: typeof DomainModels.FooterVisibilityModel;
59
+ FooterConfigModel: typeof DomainModels.FooterConfigModel;
60
+ EmptyStateModel: typeof DomainModels.EmptyStateModel;
61
+ BannerModel: typeof DomainModels.BannerModel;
62
+ ProfileDropdownModel: typeof DomainModels.ProfileDropdownModel;
63
+ User: typeof User;
64
+ };
@@ -89,9 +89,9 @@ export default class UIForm extends FormMessage {
89
89
  /**
90
90
  * Validates the entire form.
91
91
  *
92
- * @returns {Map<string, string>} Map of validation errors, empty if valid.
92
+ * @returns {any}
93
93
  */
94
- validate(): Map<string, string>;
94
+ validate(): any;
95
95
  /**
96
96
  * Validates a single field.
97
97
  *
@@ -32,6 +32,10 @@ export type AdapterHandlers = {
32
32
  * Handler for 'log' intents. Optional (defaults to no-op).
33
33
  */
34
34
  log?: ((intent: import("./Intent.js").LogIntent) => void | Promise<void>) | undefined;
35
+ /**
36
+ * Handler for 'render' intents (visual component injection). Optional.
37
+ */
38
+ render?: ((intent: import("./Intent.js").RenderIntent) => void | Promise<void>) | undefined;
35
39
  /**
36
40
  * Handler for the final 'result'. Optional (defaults to no-op).
37
41
  */
@@ -53,9 +53,17 @@ export function validateIntent(intent: any): intent is Intent;
53
53
  * @property {'result'} type
54
54
  * @property {*} data - The raw result data (JSON-serializable).
55
55
  */
56
+ /**
57
+ * Model requests rendering of a pure UI component (Header, Footer, Static Map).
58
+ * No response expected from the logic loop.
59
+ * @typedef {Object} RenderIntent
60
+ * @property {'render'} type
61
+ * @property {string} component - Component name (e.g. 'App.Layout.Header').
62
+ * @property {object} props - Static props for the component.
63
+ */
56
64
  /**
57
65
  * Union of all possible yielded intents.
58
- * @typedef {AskIntent | ProgressIntent | LogIntent} Intent
66
+ * @typedef {AskIntent | ProgressIntent | LogIntent | RenderIntent} Intent
59
67
  */
60
68
  /**
61
69
  * Response to an AskIntent. Adapter provides the collected value.
@@ -82,7 +90,7 @@ export function validateIntent(intent: any): intent is Intent;
82
90
  * Union of all possible responses an Adapter can send back via iterator.next().
83
91
  * @typedef {AskResponse | AbortResponse | undefined} IntentResponse
84
92
  */
85
- export const INTENT_TYPES: readonly ["ask", "progress", "log"];
93
+ export const INTENT_TYPES: readonly ["ask", "progress", "log", "render"];
86
94
  export function ask(field: string, schema: object | Function): AskIntent;
87
95
  export function progress(message: any): {
88
96
  type: string;
@@ -93,6 +101,11 @@ export function log(level: any, message: any, data?: {}): {
93
101
  level: any;
94
102
  message: any;
95
103
  };
104
+ export function render(component: any, props?: {}): {
105
+ type: string;
106
+ component: any;
107
+ props: {};
108
+ };
96
109
  export function result(data: any): {
97
110
  type: string;
98
111
  data: any;
@@ -186,10 +199,25 @@ export type ResultIntent = {
186
199
  */
187
200
  data: any;
188
201
  };
202
+ /**
203
+ * Model requests rendering of a pure UI component (Header, Footer, Static Map).
204
+ * No response expected from the logic loop.
205
+ */
206
+ export type RenderIntent = {
207
+ type: "render";
208
+ /**
209
+ * - Component name (e.g. 'App.Layout.Header').
210
+ */
211
+ component: string;
212
+ /**
213
+ * - Static props for the component.
214
+ */
215
+ props: object;
216
+ };
189
217
  /**
190
218
  * Union of all possible yielded intents.
191
219
  */
192
- export type Intent = AskIntent | ProgressIntent | LogIntent;
220
+ export type Intent = AskIntent | ProgressIntent | LogIntent | RenderIntent;
193
221
  /**
194
222
  * Response to an AskIntent. Adapter provides the collected value.
195
223
  * The value MUST conform to the type described in the requested FieldSchema.
@@ -19,6 +19,10 @@ export class IntentErrorModel {
19
19
  help: string;
20
20
  error: string;
21
21
  };
22
+ static render_missing_component: {
23
+ help: string;
24
+ error: string;
25
+ };
22
26
  static adapter_missing_ask: {
23
27
  help: string;
24
28
  error: string;
@@ -13,4 +13,4 @@ import UIForm from './Form/Form.js';
13
13
  export { UIStream, UIStream as UiStream, StreamEntry, StreamEntry as UiStreamEntry, UIForm, UIForm as UiForm };
14
14
  export { default as Error, CancelError } from "./Error/index.js";
15
15
  export { runFlow, flow, View, Prompt, Stream, Alert, Toast, Badge, Text, Table, Input, Select, Confirm, Multiselect, Mask, Password, Spinner, Progress, default as Flow } from "./Flow.js";
16
- export { validateIntent, ask, progress, log, result, INTENT_TYPES, isModelSchema } from "./Intent.js";
16
+ export { validateIntent, ask, progress, log, render, result, INTENT_TYPES, isModelSchema } from "./Intent.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * LogicInspector
3
+ *
4
+ * Базовий клас для захоплення "Логічних зліпків" (Intent Stream) будь-яких моделей OLMUI.
5
+ * Дозволяє виконувати чисто-логічне тестування без прив'язки до рендерингу.
6
+ */
7
+ export class LogicInspector {
8
+ /**
9
+ * Виконує генератор моделі та записує послідовність усіх інтенцій.
10
+ * @param {AsyncGenerator<import('../core/Intent.js').Intent, import('../core/Intent.js').ResultIntent, import('../core/Intent.js').IntentResponse>} modelStream - результат виклику model.run()
11
+ * @param {object} options
12
+ * @param {Array<any> | ((locale: string) => Array<any>)} [options.inputs] - черга вхідних значень для askIntent
13
+ * @param {string} [options.locale] - локаль для тестів
14
+ * @param {function} [options.t] - функція перекладу
15
+ * @returns {Promise<Array<any>>} Intent Stream Log
16
+ */
17
+ static capture(modelStream: AsyncGenerator<import("../core/Intent.js").Intent, import("../core/Intent.js").ResultIntent, import("../core/Intent.js").IntentResponse>, { inputs, locale, t }?: {
18
+ inputs?: any[] | ((locale: string) => Array<any>) | undefined;
19
+ locale?: string | undefined;
20
+ t?: Function | undefined;
21
+ }): Promise<Array<any>>;
22
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * SnapshotInspector
3
+ *
4
+ * Рев'ювер для автоматичної перевірки Snapshot-зліпків на наявність артефактів,
5
+ * неперекладених ключів та структурних помилок.
6
+ * Реалізує правила "Zero-Hallucination Snapshot Validation".
7
+ */
8
+ export class SnapshotInspector {
9
+ /**
10
+ * Перевіряє вміст одного снепшоту.
11
+ * @param {string} content - Текстовий вміст .txt файлу галереї
12
+ * @param {string} locale - Локаль (uk, en)
13
+ * @param {string} [filename] - Ім'я файлу для перевірки на "глюки" (підкреслення)
14
+ * @returns {object} { score, errors }
15
+ */
16
+ static inspect(content: string, locale?: string, filename?: string): object;
17
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * VisualAdapter (Base)
3
+ *
4
+ * Базовий клас для візуальної трансформації інтенцій OLMUI.
5
+ * Використовувана у @nan0web/ui як фундамент для спеціалізованих рендерерів.
6
+ */
7
+ export class VisualAdapter {
8
+ /**
9
+ * Конвертує одну інтенцію у просте текстове представлення.
10
+ * @param {object} intent - Intent entry from LogicInspector
11
+ * @param {function} [t] - i18n translate function
12
+ * @returns {string} Raw description
13
+ */
14
+ static render(intent: object, t?: Function): string;
15
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./LogicInspector.js";
2
+ export * from "./VisualAdapter.js";
3
+ export * from "./SnapshotInspector.js";
package/src/README.md.js DELETED
@@ -1,436 +0,0 @@
1
- import { describe, it, before, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import FS from '@nan0web/db-fs'
4
- import { NoConsole } from '@nan0web/log'
5
- import { DatasetParser, DocsParser, runSpawn } from '@nan0web/test'
6
- import { Frame, Model, OutputMessage, View, FormInput, UiMessage, UiForm } from './index.js'
7
- import { Welcome } from './Component/index.js'
8
-
9
- const fs = new FS()
10
- let pkg
11
-
12
- // Load package.json once before tests
13
- before(async () => {
14
- const doc = await fs.loadDocument('package.json', {})
15
- pkg = doc || {}
16
- })
17
-
18
- let console = new NoConsole()
19
-
20
- beforeEach((info) => {
21
- console = new NoConsole()
22
- })
23
-
24
- // Core test suite that also serves as the source for README generation.
25
- // The block comments inside each `it` block are extracted to build
26
- // the final `README.md`. Keeping the comments here ensures the
27
- // documentation stays close to the code.
28
- function testRender() {
29
- /**
30
- * @docs
31
- * # @nan0web/ui
32
- *
33
- * 🏴󠁧󠁢󠁥󠁮󠁧󠁿 [English](./README.md) | 🇺🇦 [Українською](./docs/uk/README.md)
34
- *
35
- * <!-- %PACKAGE_STATUS% -->
36
- *
37
- * A lightweight, agnostic UI framework designed with the **nan0web philosophy**
38
- * — one application logic, many UI implementations.
39
- *
40
- * This library provides core classes and utilities for building structured user interfaces.
41
- * It supports:
42
- *
43
- * - Messaging (Input/Output)
44
- * - Forms with validation
45
- * - Progress tracking
46
- * - Component rendering
47
- * - View management with Frame rendering
48
- * - App structure with core and user apps
49
- *
50
- * Built to work in sync or async, terminal-based or web-based apps,
51
- * focusing on type safety, minimalism, and pure JavaScript design.
52
- *
53
- * ## Installation
54
- */
55
- it('How to install with npm?', () => {
56
- /**
57
- * ```bash
58
- * npm install @nan0web/ui
59
- * ```
60
- */
61
- assert.equal(pkg.name, '@nan0web/ui')
62
- })
63
- /**
64
- * @docs
65
- */
66
- it('How to install with pnpm?', () => {
67
- /**
68
- * ```bash
69
- * pnpm add @nan0web/ui
70
- * ```
71
- */
72
- assert.equal(pkg.name, '@nan0web/ui')
73
- })
74
- /**
75
- * @docs
76
- */
77
- it('How to install with yarn?', () => {
78
- /**
79
- * ```bash
80
- * yarn add @nan0web/ui
81
- * ```
82
- */
83
- assert.equal(pkg.name, '@nan0web/ui')
84
- })
85
-
86
- /**
87
- * @docs
88
- * ## Concepts & Architecture
89
- *
90
- * ### Message Flow
91
- *
92
- * UI communication is built around messages:
93
- *
94
- * - **`UiMessage`** – abstract message base class
95
- * - **`OutputMessage`** – system output (content, error, priority)
96
- *
97
- * Messages are simple, serializable data containers. They help build
98
- * decoupled communication systems between UI components.
99
- */
100
- it('How to create input and output messages?', () => {
101
- //import { InputMessage, OutputMessage } from '@nan0web/ui'
102
-
103
- const input = UiMessage.from({ body: 'Hello User' })
104
- const output = OutputMessage.from({ content: ['Welcome to @nan0web/ui'] })
105
- console.info(input) // ← Message { body: "Hello User", head: {}, id: "....", type: "" }
106
- console.info(String(output)) // ← Welcome to @nan0web/ui
107
- assert.deepStrictEqual(console.output()[0][1].body, 'Hello User')
108
- assert.deepStrictEqual(console.output()[0][1].head, {})
109
- assert.deepStrictEqual(console.output()[0][1].type, '')
110
- assert.ok(console.output()[0][1].id)
111
- assert.ok(console.output()[1][1].endsWith('Welcome to @nan0web/ui'))
112
- })
113
-
114
- /**
115
- * @docs
116
- * ### Forms
117
- *
118
- * `UiForm` supports field definitions, data management, and schema validation.
119
- * Every form includes a title, fields, and current state.
120
- *
121
- * Field types include:
122
- *
123
- * - `text`
124
- * - `email`
125
- * - `number`
126
- * - `select`
127
- * - `checkbox`
128
- * - `textarea`
129
- */
130
- it('How to define and validate a UiForm?', () => {
131
- //import { UiForm } from '@nan0web/ui'
132
-
133
- const form = new UiForm({
134
- title: 'Contact Form',
135
- fields: [
136
- FormInput.from({ name: 'email', label: 'Email Address', type: 'email', required: true }),
137
- FormInput.from({
138
- name: 'message',
139
- label: 'Your Message',
140
- type: 'textarea',
141
- required: true,
142
- }),
143
- ],
144
- state: {
145
- email: 'invalid-email',
146
- message: 'Hello!',
147
- },
148
- })
149
-
150
- const errors = form.validate()
151
- console.info(errors.size) // ← 1
152
- console.info(errors.get('email')) // ← Invalid email format
153
-
154
- assert.equal(console.output()[0][1], 1)
155
- assert.equal(console.output()[1][1], 'Invalid email format')
156
- })
157
-
158
- /**
159
- * @docs
160
- * ### Components
161
- *
162
- * Components render data as frame-ready output.
163
- *
164
- * - `Welcome` – greets user by name
165
- * - `Process` – shows progress bar and time
166
- */
167
- it('How to render the Welcome component?', () => {
168
- //import { Welcome } from '@nan0web/ui'
169
-
170
- const frame = Welcome({ user: { name: 'Alice' } })
171
- const firstLine = frame[0].join('')
172
- console.info(firstLine) // ← Welcome Alice!
173
- assert.equal(console.output()[0][1], 'Welcome Alice!')
174
- })
175
-
176
- /**
177
- * @docs
178
- * ### View Manager
179
- *
180
- * `View` combines components and renders frames.
181
- *
182
- * Every view has:
183
- *
184
- * - Locale – formatted text, numbers, currency
185
- * - StdIn / StdOut – input/output streams
186
- * - Frame – output buffer with visual properties
187
- */
188
- it('How to render frame with View?', () => {
189
- //import { View } from '@nan0web/ui'
190
-
191
- const view = new View()
192
- view.render(1)(['Hello, world'])
193
- console.info(String(view.frame)) // ← "\rHello, world"
194
- assert.ok(String(view.frame).includes('Hello, world'))
195
- })
196
-
197
- /**
198
- * @docs
199
- * ### Frame Rendering
200
- *
201
- * `Frame` manages visual rendering with width and height limits.
202
- * Useful for fixed-size terminals or UI blocks.
203
- *
204
- * Render methods:
205
- *
206
- * - `APPEND` – adds content after previous frame
207
- * - `REPLACE` – erases and replaces full frame area
208
- * - `VISIBLE` – renders only visible part of frame
209
- */
210
- it('How to create a Frame with fixed size?', () => {
211
- //import { Frame } from '@nan0web/ui'
212
-
213
- const frame = new Frame({
214
- value: [['Frame content']],
215
- width: 20,
216
- height: 5,
217
- renderMethod: Frame.RenderMethod.APPEND,
218
- })
219
-
220
- const rendered = frame.render()
221
- console.info(rendered.includes('Frame content')) // ← true
222
- assert.ok(rendered.includes('Frame content'))
223
- })
224
- it('How to create a Frame with different render methods?', () => {
225
- //import { Frame } from '@nan0web/ui'
226
-
227
- const frame = new Frame({
228
- value: [['Frame content']],
229
- width: 20,
230
- height: 5,
231
- })
232
-
233
- frame.renderMethod = Frame.RenderMethod.REPLACE
234
- const renderedReplace = frame.render()
235
- assert.ok(renderedReplace.includes('Frame content'))
236
-
237
- frame.renderMethod = Frame.RenderMethod.VISIBLE
238
- const renderedVisible = frame.render()
239
- assert.ok(renderedVisible.includes('Frame content'))
240
- })
241
-
242
- /**
243
- * @docs
244
- * ### Models
245
- *
246
- * UI models are plain data objects managed by `Model` classes.
247
- *
248
- * - `User` – user data
249
- */
250
- it('How to use a User model?', () => {
251
- //import { Model } from '@nan0web/ui'
252
-
253
- const user = new Model.User({ name: 'Charlie', email: 'charlie@example.com' })
254
- console.info(user.name) // ← Charlie
255
- console.info(user.email) // ← charlie@example.com
256
- assert.equal(user.name, 'Charlie')
257
- assert.equal(user.email, 'charlie@example.com')
258
- })
259
-
260
- /**
261
- * @docs
262
- * ### Testing UI
263
- *
264
- * Core unit-tested to ensure stability in different environments.
265
- *
266
- * All components, adapters, and models are designed to be testable
267
- * with minimal setup.
268
- */
269
- it('How to test UI components with assertions?', () => {
270
- //import { Welcome } from '@nan0web/ui'
271
-
272
- const output = Welcome({ user: { name: 'Test' } })
273
- console.info(output) // ← Welcome Test!
274
- assert.deepStrictEqual(console.output()[0][1], [
275
- ['Welcome', ' ', 'Test', '!'],
276
- ['What can we do today great?'],
277
- [''],
278
- ])
279
- })
280
-
281
- /**
282
- * @docs
283
- * ### Master IDE (Component Sandbox)
284
- *
285
- * The Master IDE (OlmuiInspector) provides a unified environment for testing and documenting
286
- * web components across platforms. It supports:
287
- *
288
- * - **NaN0 Spec** — a concise YAML-based shorthand for declaring component variations.
289
- * - **OlmuiInspector** — unified UI for exploring component models and props.
290
- * - **Live Preview** — real-time rendering of component states.
291
- * - **i18n UI** — fully localized interface (UK/EN) for global developers.
292
- * - **Theme Editor** — Bootstrap-like CSS variable system with live preview.
293
- *
294
- * It follows the **Olmui** core pattern: *One Logic — Many UI* (same manifest powers both CLI and Web).
295
- *
296
- * #### Theme Editor (CSS Variables)
297
- *
298
- * Professional-grade theming with live preview. Supports:
299
- *
300
- * - **Palette**: primary, secondary, success, warning, danger, info
301
- * - **Geometry**: border-radius (sm/md/lg/pill/circle), spacing (sm/md/lg)
302
- * - **Type-safe inputs**: `type="color"` for colors, number inputs for dimensions
303
- *
304
- * #### Component Rendering Architecture
305
- *
306
- * The IDE handles data transformation between YAML models and web components:
307
- *
308
- * - **Table**: `rows[][] + columns[]` → `data[]` (array of objects)
309
- * - **Tree**: `data` → `items` mapping with 4-level taxonomy
310
- * - **Markdown**: Raw markdown → HTML via `_md2html()` converter
311
- * - **ProgressBar**: Tag alias (`ui-progress-bar` → `ui-progress`), variant colors
312
- * - **LangSelect**: `string[]` → `{code,title}[]` conversion
313
- * - **Hyphenated props**: Auto `camelCase` conversion (`show-label` → `showLabel`)
314
- *
315
- * #### NaN0 Spec (YAML)
316
- *
317
- * Concise format for defining variations:
318
- */
319
- it('How to define a component variation using NaN0 Spec?', () => {
320
- /**
321
- * ```yaml
322
- * - Button: Primary
323
- * $variant: brand
324
- * $outline: true
325
- * ```
326
- */
327
- assert.ok(pkg.name === '@nan0web/ui')
328
- })
329
-
330
- /**
331
- * @docs
332
- * #### Documentation Site
333
- *
334
- * The IDE includes an auto-generated documentation site.
335
- * HTML pages are generated from `ide.html` template via `generate-pages.js`:
336
- *
337
- * - Per-language pages (`/uk/Data/Table.html`, `/en/Feedback/Alert.html`)
338
- * - SEO-optimized with `<title>` and `<meta>` per component
339
- * - Category-based URL routing (`/Data/`, `/Feedback/`, `/Forms/`, `/Actions/`, `/System/`)
340
- * - i18n navbar with `data-i18n` attributes
341
- */
342
- it('How to run the documentation site?', () => {
343
- /**
344
- * ```bash
345
- * npm run docs:dev
346
- * ```
347
- */
348
- assert.ok(pkg.scripts?.['docs:dev'])
349
- })
350
-
351
- /**
352
- * @docs
353
- * ## Playground Demos
354
- *
355
- * The library includes rich playground demos:
356
- *
357
- * - [Registration Form](./play/registration.form.js)
358
- * - [Currency Exchange](./play/currency.exchange.js)
359
- * - [Mobile Top-up](./play/topup.telephone.js)
360
- * - [Language Selector](./play/language.form.js)
361
- *
362
- * Run to explore live functionality:
363
- */
364
- it('How to run the playground?', async () => {
365
- /**
366
- * ```bash
367
- * # Clone repository and run playground
368
- * git clone https://github.com/nan0web/ui.git
369
- * cd ui
370
- * npm install
371
- * npm run play
372
- * ```
373
- */
374
- assert.ok(String(pkg.scripts?.play).includes('node play'))
375
- const response = await runSpawn('git', ['remote', 'get-url', 'origin'])
376
- assert.ok(response.code === 0, 'git command fails (e.g., not in a git repo)')
377
- assert.ok(response.text.trim().endsWith(':nan0web/ui.git'))
378
- })
379
-
380
- /**
381
- * @docs
382
- * ## API Documentation
383
- *
384
- * Detailed API docs are available in each class JSDoc.
385
- * Explore:
386
- *
387
- * - [Messages](./src/core/Message/)
388
- * - [Forms](./src/core/Form/)
389
- * - [Stream](./src/core/Stream.js)
390
- * - [Components](./src/Component/)
391
- * - [View](./src/View/)
392
- * - [App](./src/App/)
393
- * - [Models](./src/Model/)
394
- *
395
- * ## Project Architecture & Specs
396
- *
397
- * How the universal block spec is designed? - [check Universal Blocks Spec (`project.md`)](./project.md)
398
- *
399
- * ## Contributing
400
- */
401
- it('How to contribute? - [check here](./CONTRIBUTING.md)', async () => {
402
- assert.equal(pkg.scripts?.precommit, 'npm test')
403
- assert.equal(pkg.scripts?.prepush, 'npm test')
404
- assert.equal(pkg.scripts?.prepare, 'husky')
405
- const str = await fs.loadDocumentAs('.txt', 'CONTRIBUTING.md')
406
- assert.ok(str.includes('# Contributing'))
407
- })
408
-
409
- /**
410
- * @docs
411
- * ## License
412
- */
413
- it('How to license ISC? - [check here](./LICENSE)', async () => {
414
- /** @docs */
415
- const text = await fs.loadDocumentAs('.txt', 'LICENSE')
416
- assert.ok(text.includes('ISC'))
417
- })
418
- }
419
-
420
- describe('README.md testing', testRender)
421
-
422
- describe('Rendering README.md', async () => {
423
- let text = ''
424
- const format = new Intl.NumberFormat('en-US').format
425
- const parser = new DocsParser()
426
- const source = await fs.loadDocument('src/README.md.js')
427
- text = String(parser.decode(source))
428
- await fs.saveDocument('README.md', { content: text })
429
- const dataset = DatasetParser.parse(text, pkg?.name ?? '@nan0web/ui')
430
- await fs.saveDocument('.datasets/README.dataset.jsonl', dataset)
431
-
432
- it(`document is rendered in README.md [${format(Buffer.byteLength(text))}b]`, async () => {
433
- const text = await fs.loadDocumentAs('.txt', 'README.md')
434
- assert.ok(text.includes('## License'))
435
- })
436
- })