@nan0web/ui 1.6.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +89 -85
- package/src/core/GeneratorRunner.js +213 -0
- package/src/core/Intent.js +168 -0
- package/src/core/IntentErrorModel.js +94 -0
- package/src/core/MaskHandler.js +125 -0
- package/src/core/index.js +7 -0
- package/src/domain/SandboxModel.js +193 -0
- package/src/domain/ShowcaseAppModel.js +88 -0
- package/src/domain/components/AutocompleteModel.js +58 -0
- package/src/domain/components/BreadcrumbModel.js +265 -0
- package/src/domain/components/ButtonModel.js +92 -0
- package/src/domain/components/ConfirmModel.js +64 -0
- package/src/domain/components/InputModel.js +142 -0
- package/src/domain/components/SelectModel.js +59 -0
- package/src/domain/components/SpinnerModel.js +58 -0
- package/src/domain/components/TableModel.js +60 -0
- package/src/domain/components/ToastModel.js +77 -0
- package/src/domain/components/TreeModel.js +53 -0
- package/src/domain/components/index.js +11 -0
- package/src/domain/index.js +16 -0
- package/src/format.js +21 -0
- package/src/index.js +6 -0
- package/types/core/GeneratorRunner.d.ts +51 -0
- package/types/core/Intent.d.ts +227 -85
- package/types/core/IntentErrorModel.d.ts +55 -0
- package/types/core/MaskHandler.d.ts +33 -0
- package/types/core/index.d.ts +4 -0
- package/types/domain/SandboxModel.d.ts +59 -0
- package/types/domain/ShowcaseAppModel.d.ts +62 -0
- package/types/domain/components/AutocompleteModel.d.ts +47 -0
- package/types/domain/components/BreadcrumbModel.d.ts +164 -0
- package/types/domain/components/ButtonModel.d.ts +81 -0
- package/types/domain/components/ConfirmModel.d.ts +54 -0
- package/types/domain/components/InputModel.d.ts +121 -0
- package/types/domain/components/SelectModel.d.ts +48 -0
- package/types/domain/components/SpinnerModel.d.ts +45 -0
- package/types/domain/components/TableModel.d.ts +44 -0
- package/types/domain/components/ToastModel.d.ts +62 -0
- package/types/domain/components/TreeModel.d.ts +49 -0
- package/types/domain/components/index.d.ts +10 -0
- package/types/domain/index.d.ts +3 -0
- package/types/format.d.ts +5 -0
- package/types/index.d.ts +4 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { Model } from '@nan0web/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} BreadcrumbItem
|
|
5
|
+
* @property {string} label - Human-readable display name (e.g. "Sandbox", "Кнопка")
|
|
6
|
+
* @property {string} path - URL-safe segment (e.g. "sandbox", "button")
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} BreadcrumbData
|
|
11
|
+
* @property {BreadcrumbItem[]} [items]
|
|
12
|
+
* @property {string} [separator]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Model-as-Schema for Breadcrumb navigation.
|
|
17
|
+
*
|
|
18
|
+
* Each breadcrumb item has a `label` (display) and a `path` (URL segment).
|
|
19
|
+
* The full path is the join of all segments, mirroring both:
|
|
20
|
+
* - Web URL: /sandbox/button/export
|
|
21
|
+
* - DBFS URI: sandbox/button/export/index (relative to db.root)
|
|
22
|
+
* - Data path: {db.root}/sandbox/button/export/index.yaml
|
|
23
|
+
* - CLI nav: 🏖 Sandbox › Button › Export
|
|
24
|
+
*
|
|
25
|
+
* This is the universal "where am I?" model for any OLMUI application.
|
|
26
|
+
*
|
|
27
|
+
* ESC/Back = pop() one item. Empty stack = app exit.
|
|
28
|
+
* Ctrl+C = always exit (adapter responsibility).
|
|
29
|
+
*/
|
|
30
|
+
export class BreadcrumbModel extends Model {
|
|
31
|
+
// ==========================================
|
|
32
|
+
// 1. MODEL AS SCHEMA (Static Definition)
|
|
33
|
+
// ==========================================
|
|
34
|
+
|
|
35
|
+
static items = {
|
|
36
|
+
help: 'Navigation path segments forming the breadcrumb trail',
|
|
37
|
+
type: 'array',
|
|
38
|
+
default: [],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static separator = {
|
|
42
|
+
help: 'Visual separator between breadcrumb segments',
|
|
43
|
+
default: '›',
|
|
44
|
+
type: 'string',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {BreadcrumbData | any} [data]
|
|
49
|
+
*/
|
|
50
|
+
constructor(data = {}) {
|
|
51
|
+
super(data)
|
|
52
|
+
/** @type {BreadcrumbItem[]} */ this.items
|
|
53
|
+
/** @type {string} */ this.separator
|
|
54
|
+
// Normalize: if items were passed as plain strings, convert to {label, path}
|
|
55
|
+
if (Array.isArray(this.items)) {
|
|
56
|
+
this.items = this.items.map((item) =>
|
|
57
|
+
typeof item === 'string'
|
|
58
|
+
? { label: item, path: BreadcrumbModel.slugify(item) }
|
|
59
|
+
: item
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ==========================================
|
|
65
|
+
// 2. NAVIGATION API (Imperative)
|
|
66
|
+
// ==========================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Push a new level onto the navigation stack.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} label - Display label (e.g. "Button", "Кнопка")
|
|
72
|
+
* @param {string} [path] - URL segment. Auto-slugified from label if omitted.
|
|
73
|
+
* @returns {this}
|
|
74
|
+
*/
|
|
75
|
+
push(label, path) {
|
|
76
|
+
this.items.push({
|
|
77
|
+
label,
|
|
78
|
+
path: path || BreadcrumbModel.slugify(label),
|
|
79
|
+
})
|
|
80
|
+
return this
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pop the last level from the navigation stack.
|
|
85
|
+
*
|
|
86
|
+
* @returns {BreadcrumbItem|undefined} The removed item, or undefined if stack was empty.
|
|
87
|
+
*/
|
|
88
|
+
pop() {
|
|
89
|
+
return this.items.pop()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether the user can navigate back (stack has > 0 items after pop).
|
|
94
|
+
*
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
canGoBack() {
|
|
98
|
+
return this.items.length > 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Navigate to a specific depth by truncating the stack.
|
|
103
|
+
*
|
|
104
|
+
* @param {number} depth - Target depth (0 = root only).
|
|
105
|
+
* @returns {this}
|
|
106
|
+
*/
|
|
107
|
+
navigateTo(depth) {
|
|
108
|
+
this.items = this.items.slice(0, depth + 1)
|
|
109
|
+
return this
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ==========================================
|
|
113
|
+
// 3. PATH / URL API (Serialization)
|
|
114
|
+
// ==========================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Full URL-style path: `/sandbox/button/export`
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
get path() {
|
|
121
|
+
if (this.items.length === 0) return '/'
|
|
122
|
+
return '/' + this.items.map((i) => i.path).join('/')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Just the path segments array: `['sandbox', 'button', 'export']`
|
|
127
|
+
* @returns {string[]}
|
|
128
|
+
*/
|
|
129
|
+
get segments() {
|
|
130
|
+
return this.items.map((i) => i.path)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Just the display labels: `['Sandbox', 'Button', 'Export']`
|
|
135
|
+
* @returns {string[]}
|
|
136
|
+
*/
|
|
137
|
+
get labels() {
|
|
138
|
+
return this.items.map((i) => i.label)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Current (last) breadcrumb item.
|
|
143
|
+
* @returns {BreadcrumbItem|undefined}
|
|
144
|
+
*/
|
|
145
|
+
get current() {
|
|
146
|
+
return this.items[this.items.length - 1]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Current navigation depth (0 = no items).
|
|
151
|
+
* @returns {number}
|
|
152
|
+
*/
|
|
153
|
+
get depth() {
|
|
154
|
+
return this.items.length
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Display string: `Sandbox › Button › Export`
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
toString() {
|
|
162
|
+
return this.labels.join(` ${this.separator} `)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Serialize to URL query param value: `sandbox/button/export`
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
toURL() {
|
|
170
|
+
return this.segments.join('/')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* DBFS document URI — the key you pass to `db.fetch()` or `db.get()`.
|
|
175
|
+
* Relative to `db.root`, without extension (DBFS resolves `.yaml`/`.json` automatically).
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* nav.push('sandbox').push('button')
|
|
179
|
+
* nav.toURI() // → 'sandbox/button/index'
|
|
180
|
+
* db.fetch(nav.toURI()) // ← DBFS resolves to {root}/sandbox/button/index.yaml
|
|
181
|
+
*
|
|
182
|
+
* @param {string} [leaf='index'] - Document name without extension.
|
|
183
|
+
* @returns {string}
|
|
184
|
+
*/
|
|
185
|
+
toURI(leaf = 'index') {
|
|
186
|
+
if (this.items.length === 0) return leaf
|
|
187
|
+
return `${this.segments.join('/')}/${leaf}`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Full filesystem path relative to db.root: `sandbox/button/export/index.yaml`
|
|
192
|
+
* This is what DBFS resolves to on disk: `{cwd}/{root}/{toDataPath()}`.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} [filename='index.yaml'] - Leaf filename with extension.
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
toDataPath(filename = 'index.yaml') {
|
|
198
|
+
if (this.items.length === 0) return filename
|
|
199
|
+
return `${this.segments.join('/')}/${filename}`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Reconstruct a BreadcrumbModel from a URL path string.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} urlPath - e.g. "/sandbox/button" or "sandbox/button"
|
|
206
|
+
* @param {Record<string,string>} [labelMap={}] - Optional map of path→label for display names.
|
|
207
|
+
* @returns {BreadcrumbModel}
|
|
208
|
+
*/
|
|
209
|
+
static fromPath(urlPath, labelMap = {}) {
|
|
210
|
+
const segments = urlPath
|
|
211
|
+
.replace(/^\//, '')
|
|
212
|
+
.split('/')
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
|
|
215
|
+
const items = segments.map((seg) => ({
|
|
216
|
+
label: labelMap[seg] || seg,
|
|
217
|
+
path: seg,
|
|
218
|
+
}))
|
|
219
|
+
|
|
220
|
+
return new BreadcrumbModel({ items })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a URL-safe slug from any label.
|
|
225
|
+
* Handles Unicode (Cyrillic, etc.) by lowercasing and replacing spaces/special chars.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} label
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
static slugify(label) {
|
|
231
|
+
return label
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(/\s+/g, '-')
|
|
234
|
+
.replace(/[^\p{L}\p{N}\-]/gu, '')
|
|
235
|
+
.replace(/-+/g, '-')
|
|
236
|
+
.replace(/^-|-$/g, '')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ==========================================
|
|
240
|
+
// 4. AGNOSTIC LOGIC (Async Generator)
|
|
241
|
+
// ==========================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Yields a log intent with the current breadcrumb path.
|
|
245
|
+
* This is a "display-only" run — it shows the navigation state.
|
|
246
|
+
*/
|
|
247
|
+
async *run() {
|
|
248
|
+
yield /** @type {any} */ ({
|
|
249
|
+
type: 'log',
|
|
250
|
+
level: 'info',
|
|
251
|
+
message: this.toString(),
|
|
252
|
+
component: 'Breadcrumbs',
|
|
253
|
+
model: /** @type {any} */ (this),
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
type: 'result',
|
|
258
|
+
data: {
|
|
259
|
+
path: this.path,
|
|
260
|
+
items: this.items,
|
|
261
|
+
depth: this.depth,
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Model } from '@nan0web/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {'primary'|'secondary'|'info'|'ok'|'warn'|'err'|'ghost'} ButtonVariant
|
|
5
|
+
* @typedef {'sm'|'md'|'lg'} ButtonSize
|
|
6
|
+
* @typedef {Object} ButtonData
|
|
7
|
+
* @property {string} [content]
|
|
8
|
+
* @property {ButtonVariant} [variant]
|
|
9
|
+
* @property {ButtonSize} [size]
|
|
10
|
+
* @property {boolean} [outline]
|
|
11
|
+
* @property {boolean} [disabled]
|
|
12
|
+
* @property {boolean} [loading]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Model-as-Schema for Button component.
|
|
17
|
+
* Represents the intention and state of a Button interaction.
|
|
18
|
+
* Used exclusively for schema definition and editor validation.
|
|
19
|
+
*/
|
|
20
|
+
export class ButtonModel extends Model {
|
|
21
|
+
// ==========================================
|
|
22
|
+
// 1. MODEL AS SCHEMA (Static Definition)
|
|
23
|
+
// ==========================================
|
|
24
|
+
|
|
25
|
+
static content = {
|
|
26
|
+
help: 'Text or content inside the button',
|
|
27
|
+
default: 'Click Me',
|
|
28
|
+
type: 'string',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static variant = {
|
|
32
|
+
help: 'Visual importance and semantic meaning',
|
|
33
|
+
default: 'primary',
|
|
34
|
+
options: ['primary', 'secondary', 'info', 'ok', 'warn', 'err', 'ghost'],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static size = {
|
|
38
|
+
help: 'Size of the button',
|
|
39
|
+
default: 'md',
|
|
40
|
+
options: ['sm', 'md', 'lg'],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static outline = {
|
|
44
|
+
help: 'Whether the button has a transparent background with border',
|
|
45
|
+
default: false,
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static disabled = {
|
|
50
|
+
help: 'Whether the button is disabled and unclickable',
|
|
51
|
+
default: false,
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static loading = {
|
|
56
|
+
help: 'Whether the button shows a loading spinner instead of content',
|
|
57
|
+
default: false,
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {ButtonData | any} [data]
|
|
63
|
+
*/
|
|
64
|
+
constructor(data = {}) {
|
|
65
|
+
super(data)
|
|
66
|
+
/** @type {string|undefined} */ this.content
|
|
67
|
+
/** @type {ButtonVariant|undefined} */ this.variant
|
|
68
|
+
/** @type {ButtonSize|undefined} */ this.size
|
|
69
|
+
/** @type {boolean|undefined} */ this.outline
|
|
70
|
+
/** @type {boolean|undefined} */ this.disabled
|
|
71
|
+
/** @type {boolean|undefined} */ this.loading
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ==========================================
|
|
75
|
+
// 2. AGNOSTIC LOGIC (Async Generator)
|
|
76
|
+
// ==========================================
|
|
77
|
+
|
|
78
|
+
async *run() {
|
|
79
|
+
// A basic button interaction intention:
|
|
80
|
+
// We simply yield ourselves as a 'button_click' intent.
|
|
81
|
+
// Adaptors will render the button and wait for the click event.
|
|
82
|
+
const response = yield {
|
|
83
|
+
type: 'ask',
|
|
84
|
+
field: 'action',
|
|
85
|
+
schema: { help: 'Click the button to proceed' },
|
|
86
|
+
component: 'Button',
|
|
87
|
+
model: /** @type {any} */ (this),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { type: 'result', data: { clicked: true, ...response } }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Model } from '@nan0web/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} ConfirmData
|
|
5
|
+
* @property {string} [message]
|
|
6
|
+
* @property {string} [confirmText]
|
|
7
|
+
* @property {string} [cancelText]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Model-as-Schema for Confirm component.
|
|
12
|
+
*/
|
|
13
|
+
export class ConfirmModel extends Model {
|
|
14
|
+
// ==========================================
|
|
15
|
+
// 1. MODEL AS SCHEMA (Static Definition)
|
|
16
|
+
// ==========================================
|
|
17
|
+
|
|
18
|
+
static message = {
|
|
19
|
+
help: 'Dialog message displayed to the user',
|
|
20
|
+
default: 'Are you sure?',
|
|
21
|
+
type: 'string',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static confirmText = {
|
|
25
|
+
help: 'Label for the positive confirmation button',
|
|
26
|
+
default: 'Yes',
|
|
27
|
+
type: 'string',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static cancelText = {
|
|
31
|
+
help: 'Label for the negative rejection button',
|
|
32
|
+
default: 'No',
|
|
33
|
+
type: 'string',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {ConfirmData | any} [data]
|
|
38
|
+
*/
|
|
39
|
+
constructor(data = {}) {
|
|
40
|
+
super(data)
|
|
41
|
+
/** @type {string|undefined} */ this.message
|
|
42
|
+
/** @type {string|undefined} */ this.confirmText
|
|
43
|
+
/** @type {string|undefined} */ this.cancelText
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ==========================================
|
|
47
|
+
// 2. AGNOSTIC LOGIC (Async Generator)
|
|
48
|
+
// ==========================================
|
|
49
|
+
|
|
50
|
+
async *run() {
|
|
51
|
+
const response = yield {
|
|
52
|
+
type: 'ask',
|
|
53
|
+
field: 'confirmed',
|
|
54
|
+
schema: {
|
|
55
|
+
help: this.message,
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
},
|
|
58
|
+
component: 'Confirm',
|
|
59
|
+
model: /** @type {any} */ (this), // Attached for richer UI metadata
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { type: 'result', data: { confirmed: !!response.value } }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Model } from '@nan0web/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {'text'|'email'|'password'|'number'|'tel'|'url'|'date'} InputType
|
|
5
|
+
* @typedef {Object} InputData
|
|
6
|
+
* @property {InputType} [type]
|
|
7
|
+
* @property {string} [label]
|
|
8
|
+
* @property {string} [placeholder]
|
|
9
|
+
* @property {boolean} [required]
|
|
10
|
+
* @property {string} [pattern]
|
|
11
|
+
* @property {string} [min]
|
|
12
|
+
* @property {string} [max]
|
|
13
|
+
* @property {string} [step]
|
|
14
|
+
* @property {string} [hint]
|
|
15
|
+
* @property {boolean} [disabled]
|
|
16
|
+
* @property {string} [content]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Model-as-Schema for Input component.
|
|
21
|
+
* Used exclusively for schema definition, validation, and editor reflection.
|
|
22
|
+
*/
|
|
23
|
+
export class InputModel extends Model {
|
|
24
|
+
// ==========================================
|
|
25
|
+
// 1. MODEL AS SCHEMA (Static Definition)
|
|
26
|
+
// ==========================================
|
|
27
|
+
|
|
28
|
+
static type = {
|
|
29
|
+
help: 'HTML5 Input type attribute',
|
|
30
|
+
default: 'text',
|
|
31
|
+
options: ['text', 'email', 'password', 'number', 'tel', 'url', 'date'],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static label = {
|
|
35
|
+
help: 'Label displayed above the input',
|
|
36
|
+
default: '',
|
|
37
|
+
type: 'string',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static placeholder = {
|
|
41
|
+
help: 'Placeholder text shown when empty',
|
|
42
|
+
default: '',
|
|
43
|
+
type: 'string',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static required = {
|
|
47
|
+
help: 'Whether the field must be filled out',
|
|
48
|
+
default: false,
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static pattern = {
|
|
53
|
+
help: 'RegExp pattern for validation (e.g. [A-Z]{3})',
|
|
54
|
+
default: '',
|
|
55
|
+
type: 'string',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static min = {
|
|
59
|
+
help: 'Minimum value (for number/date types)',
|
|
60
|
+
default: '',
|
|
61
|
+
type: 'string',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static max = {
|
|
65
|
+
help: 'Maximum value (for number/date types)',
|
|
66
|
+
default: '',
|
|
67
|
+
type: 'string',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static step = {
|
|
71
|
+
help: 'Step interval (for number/date types)',
|
|
72
|
+
default: '',
|
|
73
|
+
type: 'string',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static hint = {
|
|
77
|
+
help: 'Helper text displayed below the input',
|
|
78
|
+
default: '',
|
|
79
|
+
type: 'string',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static disabled = {
|
|
83
|
+
help: 'Whether the input is greyed out and uneditable',
|
|
84
|
+
default: false,
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static content = {
|
|
89
|
+
help: 'The actual value of the input',
|
|
90
|
+
default: '',
|
|
91
|
+
type: 'string',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {InputData | any} [data]
|
|
96
|
+
*/
|
|
97
|
+
constructor(data = {}) {
|
|
98
|
+
super(data)
|
|
99
|
+
/** @type {InputType|undefined} */ this.type
|
|
100
|
+
/** @type {string|undefined} */ this.label
|
|
101
|
+
/** @type {string|undefined} */ this.placeholder
|
|
102
|
+
/** @type {boolean|undefined} */ this.required
|
|
103
|
+
/** @type {string|undefined} */ this.pattern
|
|
104
|
+
/** @type {string|undefined} */ this.min
|
|
105
|
+
/** @type {string|undefined} */ this.max
|
|
106
|
+
/** @type {string|undefined} */ this.step
|
|
107
|
+
/** @type {string|undefined} */ this.hint
|
|
108
|
+
/** @type {boolean|undefined} */ this.disabled
|
|
109
|
+
/** @type {string|undefined} */ this.content
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ==========================================
|
|
113
|
+
// 2. AGNOSTIC LOGIC (Async Generator)
|
|
114
|
+
// ==========================================
|
|
115
|
+
|
|
116
|
+
async *run() {
|
|
117
|
+
const response = yield {
|
|
118
|
+
type: 'ask',
|
|
119
|
+
field: 'content',
|
|
120
|
+
schema: {
|
|
121
|
+
help: this.label || this.placeholder || 'Enter value',
|
|
122
|
+
validate: (val) => {
|
|
123
|
+
if (this.required && !val) return 'This field is required'
|
|
124
|
+
if (this.pattern && val) {
|
|
125
|
+
try {
|
|
126
|
+
const re = new RegExp(`^${this.pattern}$`)
|
|
127
|
+
if (!re.test(val)) return 'Invalid format'
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// fallback if pattern is malformed
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return true
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
component: 'Input',
|
|
136
|
+
model: /** @type {any} */ (this),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.content = response.value
|
|
140
|
+
return { type: 'result', data: { value: this.content } }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Model } from '@nan0web/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} SelectData
|
|
5
|
+
* @property {string} [content]
|
|
6
|
+
* @property {string[]} [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Model-as-Schema for Select component.
|
|
11
|
+
* Represents a dropdown choice selection.
|
|
12
|
+
*/
|
|
13
|
+
export class SelectModel extends Model {
|
|
14
|
+
// ==========================================
|
|
15
|
+
// 1. MODEL AS SCHEMA (Static Definition)
|
|
16
|
+
// ==========================================
|
|
17
|
+
|
|
18
|
+
static content = {
|
|
19
|
+
help: 'Currently selected item or default placeholder',
|
|
20
|
+
default: 'Choose option',
|
|
21
|
+
type: 'string',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static options = {
|
|
25
|
+
help: 'List of available options for selection',
|
|
26
|
+
default: ['Alpha', 'Beta', 'Gamma'],
|
|
27
|
+
type: 'string[]',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {SelectData | any} [data]
|
|
32
|
+
*/
|
|
33
|
+
constructor(data = {}) {
|
|
34
|
+
super(data)
|
|
35
|
+
/** @type {string|undefined} */ this.content
|
|
36
|
+
/** @type {string[]|undefined} */ this.options
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ==========================================
|
|
40
|
+
// 2. AGNOSTIC LOGIC (Async Generator)
|
|
41
|
+
// ==========================================
|
|
42
|
+
|
|
43
|
+
async *run() {
|
|
44
|
+
const response = yield {
|
|
45
|
+
type: 'ask',
|
|
46
|
+
field: 'content',
|
|
47
|
+
schema: {
|
|
48
|
+
help: 'Select an option',
|
|
49
|
+
options: this.options,
|
|
50
|
+
validate: (val) => this.options?.includes(val) || 'Invalid option selected',
|
|
51
|
+
},
|
|
52
|
+
component: 'Select',
|
|
53
|
+
model: /** @type {any} */ (this),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.content = response.value
|
|
57
|
+
return { type: 'result', data: { selected: this.content } }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Model } from '@nan0web/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {'sm'|'md'|'lg'} SpinnerSize
|
|
5
|
+
* @typedef {Object} SpinnerData
|
|
6
|
+
* @property {SpinnerSize} [size]
|
|
7
|
+
* @property {string} [color]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Model-as-Schema for Spinner component.
|
|
12
|
+
* Represents a loading or progress state without user interaction.
|
|
13
|
+
*/
|
|
14
|
+
export class SpinnerModel extends Model {
|
|
15
|
+
// ==========================================
|
|
16
|
+
// 1. MODEL AS SCHEMA (Static Definition)
|
|
17
|
+
// ==========================================
|
|
18
|
+
|
|
19
|
+
static size = {
|
|
20
|
+
help: 'Spinner diameter',
|
|
21
|
+
default: 'md',
|
|
22
|
+
options: ['sm', 'md', 'lg'],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static color = {
|
|
26
|
+
help: 'Override for base color token',
|
|
27
|
+
type: 'color',
|
|
28
|
+
default: '',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {SpinnerData | any} [data]
|
|
33
|
+
*/
|
|
34
|
+
constructor(data = {}) {
|
|
35
|
+
super(data)
|
|
36
|
+
/** @type {SpinnerSize|undefined} */ this.size
|
|
37
|
+
/** @type {string|undefined} */ this.color
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ==========================================
|
|
41
|
+
// 2. AGNOSTIC LOGIC (Async Generator)
|
|
42
|
+
// ==========================================
|
|
43
|
+
|
|
44
|
+
async *run() {
|
|
45
|
+
// A spinner does not ask for anything, it simply indicates progress.
|
|
46
|
+
// However, as a pure component it doesn't do any work itself,
|
|
47
|
+
// so running it just means declaring its state.
|
|
48
|
+
yield {
|
|
49
|
+
type: 'progress',
|
|
50
|
+
message: 'Loading...',
|
|
51
|
+
component: 'Spinner',
|
|
52
|
+
model: /** @type {any} */ (this),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Instant exit since it performs no async task internally
|
|
56
|
+
return { type: 'result', data: { completed: true } }
|
|
57
|
+
}
|
|
58
|
+
}
|