@nan0web/ui 1.5.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.
Files changed (53) hide show
  1. package/README.md +36 -1
  2. package/package.json +89 -84
  3. package/src/ArchitectureMap/ArchitectureMap.js +111 -0
  4. package/src/ArchitectureMap/index.js +1 -0
  5. package/src/InterfaceTemplate/InterfaceTemplate.js +95 -0
  6. package/src/InterfaceTemplate/index.js +1 -0
  7. package/src/README.md.js +42 -1
  8. package/src/core/GeneratorRunner.js +213 -0
  9. package/src/core/Intent.js +168 -0
  10. package/src/core/IntentErrorModel.js +94 -0
  11. package/src/core/MaskHandler.js +125 -0
  12. package/src/core/index.js +7 -0
  13. package/src/domain/SandboxModel.js +193 -0
  14. package/src/domain/ShowcaseAppModel.js +88 -0
  15. package/src/domain/components/AutocompleteModel.js +58 -0
  16. package/src/domain/components/BreadcrumbModel.js +265 -0
  17. package/src/domain/components/ButtonModel.js +92 -0
  18. package/src/domain/components/ConfirmModel.js +64 -0
  19. package/src/domain/components/InputModel.js +142 -0
  20. package/src/domain/components/SelectModel.js +59 -0
  21. package/src/domain/components/SpinnerModel.js +58 -0
  22. package/src/domain/components/TableModel.js +60 -0
  23. package/src/domain/components/ToastModel.js +77 -0
  24. package/src/domain/components/TreeModel.js +53 -0
  25. package/src/domain/components/index.js +11 -0
  26. package/src/domain/index.js +16 -0
  27. package/src/format.js +21 -0
  28. package/src/index.js +6 -0
  29. package/types/ArchitectureMap/ArchitectureMap.d.ts +70 -0
  30. package/types/ArchitectureMap/index.d.ts +1 -0
  31. package/types/InterfaceTemplate/InterfaceTemplate.d.ts +67 -0
  32. package/types/InterfaceTemplate/index.d.ts +1 -0
  33. package/types/core/GeneratorRunner.d.ts +51 -0
  34. package/types/core/Intent.d.ts +227 -85
  35. package/types/core/IntentErrorModel.d.ts +55 -0
  36. package/types/core/MaskHandler.d.ts +33 -0
  37. package/types/core/index.d.ts +4 -0
  38. package/types/domain/SandboxModel.d.ts +59 -0
  39. package/types/domain/ShowcaseAppModel.d.ts +62 -0
  40. package/types/domain/components/AutocompleteModel.d.ts +47 -0
  41. package/types/domain/components/BreadcrumbModel.d.ts +164 -0
  42. package/types/domain/components/ButtonModel.d.ts +81 -0
  43. package/types/domain/components/ConfirmModel.d.ts +54 -0
  44. package/types/domain/components/InputModel.d.ts +121 -0
  45. package/types/domain/components/SelectModel.d.ts +48 -0
  46. package/types/domain/components/SpinnerModel.d.ts +45 -0
  47. package/types/domain/components/TableModel.d.ts +44 -0
  48. package/types/domain/components/ToastModel.d.ts +62 -0
  49. package/types/domain/components/TreeModel.d.ts +49 -0
  50. package/types/domain/components/index.d.ts +10 -0
  51. package/types/domain/index.d.ts +3 -0
  52. package/types/format.d.ts +5 -0
  53. package/types/index.d.ts +4 -0
@@ -0,0 +1,193 @@
1
+ import { Model } from '@nan0web/core'
2
+ import * as ComponentModels from './components/index.js'
3
+ import { BreadcrumbModel } from './components/BreadcrumbModel.js'
4
+
5
+ /**
6
+ * @typedef {Object} SandboxData
7
+ * @property {string[]} [components]
8
+ * @property {string} [selectedComponent]
9
+ * @property {string} [themeFormat]
10
+ */
11
+
12
+ /**
13
+ * Model-as-Schema for the UI Sandbox environment.
14
+ * Represents a tool wrapping standard OLMUI components, allowing
15
+ * users to inspect their models, tweak variables interactively,
16
+ * and export the configuration as themes for the Marketplace.
17
+ *
18
+ * Navigation uses BreadcrumbModel:
19
+ * ESC = pop one level (if stack has no parent → exit app)
20
+ * Ctrl+C = always exit (handled by prompts.js wrapper)
21
+ *
22
+ * URL mapping:
23
+ * /sandbox → Select Component
24
+ * /sandbox/button → Edit Button properties
25
+ * /sandbox/button/export → Choose export format
26
+ */
27
+ export class SandboxModel extends Model {
28
+ // ==========================================
29
+ // 1. MODEL AS SCHEMA (Static Definition)
30
+ // ==========================================
31
+
32
+ static components = {
33
+ help: 'List of registered UI components available for inspection',
34
+ type: 'string[]',
35
+ default: []
36
+ }
37
+
38
+ static selectedComponent = {
39
+ help: 'The specific component chosen by the user for theming',
40
+ type: 'string',
41
+ }
42
+
43
+ static themeFormat = {
44
+ help: 'The file format chosen to export the custom theme configuration',
45
+ options: ['yaml', 'css', 'json'],
46
+ default: 'yaml'
47
+ }
48
+
49
+ /**
50
+ * @param {SandboxData | any} [data]
51
+ */
52
+ constructor(data = {}) {
53
+ super(data)
54
+ /** @type {string[]|undefined} */ this.components
55
+ /** @type {string|undefined} */ this.selectedComponent
56
+ /** @type {string|undefined} */ this.themeFormat
57
+ }
58
+
59
+ // ==========================================
60
+ // 2. AGNOSTIC LOGIC (Async Generator)
61
+ // ==========================================
62
+
63
+ async *run() {
64
+ /** @type {any} */
65
+ let targetInstance = null
66
+
67
+ // ── BreadcrumbModel as navigation stack ──
68
+ const nav = new BreadcrumbModel()
69
+ nav.push('🏖 Sandbox', 'sandbox')
70
+
71
+ while (true) {
72
+ // ── Level 1: Select Component ──
73
+ if (!this.selectedComponent) {
74
+ // Show breadcrumb
75
+ yield /** @type {any} */ ({
76
+ type: 'log', level: 'info',
77
+ message: `\n${nav}`,
78
+ component: 'Breadcrumbs',
79
+ model: /** @type {any} */ (nav),
80
+ })
81
+
82
+ // ESC here = CancelError not caught → bubbles out → app exits
83
+ const listResponse = yield {
84
+ type: 'ask',
85
+ field: 'selectedComponent',
86
+ schema: {
87
+ help: 'Select a component to inspect and theme',
88
+ options: this.components || [],
89
+ validate: (/** @type {string} */ val) =>
90
+ (this.components || []).includes(val) || 'Component not found in sandbox registry',
91
+ },
92
+ component: 'Select',
93
+ model: /** @type {any} */ (this),
94
+ }
95
+ this.selectedComponent = listResponse.value
96
+ nav.push(/** @type {string} */ (this.selectedComponent))
97
+
98
+ // Instantiate the selected model class
99
+ const Ctor = ComponentModels[`${this.selectedComponent}Model`]
100
+ targetInstance = Ctor ? new Ctor() : this
101
+ }
102
+
103
+ // ── Level 2: Edit Component Properties ──
104
+ // URL: /sandbox/button Data: data/sandbox/button/index.yaml
105
+ /** @type {any} */
106
+ let configResponse
107
+ try {
108
+ yield /** @type {any} */ ({
109
+ type: 'log', level: 'info',
110
+ message: `\n${nav}`,
111
+ component: 'Breadcrumbs',
112
+ model: /** @type {any} */ (nav),
113
+ })
114
+
115
+ configResponse = yield {
116
+ type: 'ask',
117
+ field: 'componentThemeConfig',
118
+ schema: ComponentModels[`${this.selectedComponent}Model`] || {
119
+ help: `Configure properties for ${this.selectedComponent} to create a theme variation`,
120
+ },
121
+ component: 'SandboxWrapper',
122
+ model: true,
123
+ instance: /** @type {any} */ (targetInstance),
124
+ }
125
+ } catch (e) {
126
+ const err = /** @type {Error} */ (e)
127
+ if (err.name === 'CancelError') {
128
+ // Pop: Level 2 → Level 1
129
+ nav.pop()
130
+ this.selectedComponent = undefined
131
+ continue
132
+ }
133
+ throw e
134
+ }
135
+
136
+ // Persist edits for potential back-navigation
137
+ targetInstance = configResponse.value
138
+
139
+ // ── Level 3: Choose Export Format ──
140
+ // URL: /sandbox/button/export Data: data/sandbox/button/export/index.yaml
141
+ /** @type {any} */
142
+ let formatResponse
143
+ try {
144
+ nav.push('Export', 'export')
145
+ yield /** @type {any} */ ({
146
+ type: 'log', level: 'info',
147
+ message: `\n${nav}`,
148
+ component: 'Breadcrumbs',
149
+ model: /** @type {any} */ (nav),
150
+ })
151
+
152
+ formatResponse = yield {
153
+ type: 'ask',
154
+ field: 'themeFormat',
155
+ schema: {
156
+ help: 'Choose how to export the theme configuration',
157
+ options: SandboxModel.themeFormat.options,
158
+ },
159
+ component: 'Select',
160
+ model: /** @type {any} */ (this),
161
+ }
162
+ } catch (e) {
163
+ const err = /** @type {Error} */ (e)
164
+ if (err.name === 'CancelError') {
165
+ // Pop: Level 3 → Level 2 (same targetInstance preserved)
166
+ nav.pop()
167
+ continue
168
+ }
169
+ throw e
170
+ }
171
+
172
+ this.themeFormat = formatResponse.value
173
+
174
+ // 4. Success notification
175
+ yield /** @type {any} */ ({
176
+ type: 'log',
177
+ level: 'success',
178
+ message: `Theme exported as ${(this.themeFormat || 'json').toUpperCase()}! Path: ${nav.path}`,
179
+ })
180
+
181
+ // 5. Return result with navigation context
182
+ return {
183
+ type: 'result',
184
+ data: {
185
+ targetComponent: this.selectedComponent,
186
+ themeConfig: configResponse.value,
187
+ exportFormat: this.themeFormat,
188
+ breadcrumb: nav.path,
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,88 @@
1
+ import { InputModel, ConfirmModel, SpinnerModel, ToastModel, TableModel, ButtonModel, AutocompleteModel, SelectModel } from './components/index.js'
2
+
3
+ /**
4
+ * Model-as-Schema for the entire UI Sandbox Showcase.
5
+ * Represents a complete User Journey demonstrating all components.
6
+ * Showcases OLMUI Scenario Testing capabilities.
7
+ */
8
+ export class ShowcaseAppModel {
9
+ // ==========================================
10
+ // 1. MODEL AS SCHEMA (Static Definition)
11
+ // ==========================================
12
+
13
+ static appName = {
14
+ help: 'Name of the showcase application',
15
+ default: 'Component Showcase (Zero Hallucination)',
16
+ type: 'string',
17
+ }
18
+
19
+ constructor() {
20
+ this.appName = ShowcaseAppModel.appName.default
21
+ }
22
+
23
+ // ==========================================
24
+ // 2. AGNOSTIC LOGIC (Async Generator)
25
+ // ==========================================
26
+
27
+ async *run() {
28
+ // 1. Initial interaction: User clicks "Start Showcase" Button
29
+ const btnIntent = yield* new ButtonModel({ content: 'Start Showcase', variant: 'primary', size: 'lg' }).run()
30
+
31
+ if (!btnIntent.data.clicked) {
32
+ return { type: 'result', data: { success: false, reason: 'start_cancelled' } }
33
+ }
34
+
35
+ // 2. Ask user for their name via Input
36
+ const { data: nameData } = yield* new InputModel({
37
+ label: 'Enter your name to begin',
38
+ placeholder: 'e.g. Yaroslav',
39
+ required: true,
40
+ pattern: '.{3,}'
41
+ }).run()
42
+
43
+ const userName = /** @type {string} */ (nameData.value)
44
+
45
+ // 3. Ask user to select their role via Select
46
+ const { data: roleData } = yield* new SelectModel({
47
+ options: ['Developer', 'Designer', 'Manager']
48
+ }).run()
49
+
50
+ const role = /** @type {string} */ (roleData.selected)
51
+
52
+ // 4. Ask for their favorite tool via Autocomplete
53
+ const { data: toolData } = yield* new AutocompleteModel({
54
+ options: ['React', 'Lit', 'Node.js', 'Playwright', 'Vitest', 'Vite']
55
+ }).run()
56
+
57
+ const tool = /** @type {string} */ (toolData.selected)
58
+
59
+ // 5. Ask for confirmation before proceeding to heavy calculation
60
+ const confirmIntent = yield* new ConfirmModel({ message: `Ready to generate profile for ${userName} (${role})?` }).run()
61
+
62
+ if (!confirmIntent.data.confirmed) {
63
+ yield* new ToastModel({ message: 'Operation aborted.', variant: 'warning', duration: 0 }).run()
64
+ return { type: 'result', data: { success: false, reason: 'user_aborted' } }
65
+ }
66
+
67
+ // 6. Demonstrate a long running progress via Spinner
68
+ yield* new SpinnerModel({ size: 'md' }).run()
69
+
70
+ // Simulate business logic processing delay if we were not mocked
71
+ // But in generators this just happens immediately between yields
72
+
73
+ yield* new ToastModel({ message: 'Profile generated successfully!', variant: 'success', duration: 0 }).run()
74
+
75
+ // 7. Display the result of the showcase in a Table
76
+ const { data: tableData } = yield* new TableModel({
77
+ columns: ['Property', 'Value'],
78
+ rows: [
79
+ ['Name', userName],
80
+ ['Role', role],
81
+ ['Favorite Tool', tool],
82
+ ['Status', 'Active']
83
+ ]
84
+ }).run()
85
+
86
+ return { type: 'result', data: { success: true, profile: { userName, role, tool }, rowsDisplayed: tableData.rowsCount } }
87
+ }
88
+ }
@@ -0,0 +1,58 @@
1
+ import { Model } from '@nan0web/core'
2
+
3
+ /**
4
+ * @typedef {Object} AutocompleteData
5
+ * @property {string} [content]
6
+ * @property {string[]} [options]
7
+ */
8
+
9
+ /**
10
+ * Model-as-Schema for Autocomplete component.
11
+ * Represents a text input with search suggestions.
12
+ */
13
+ export class AutocompleteModel extends Model {
14
+ // ==========================================
15
+ // 1. MODEL AS SCHEMA (Static Definition)
16
+ // ==========================================
17
+
18
+ static content = {
19
+ help: 'Current search text',
20
+ default: '',
21
+ type: 'string',
22
+ }
23
+
24
+ static options = {
25
+ help: 'List of suggestions based on input',
26
+ default: [],
27
+ type: 'string[]',
28
+ }
29
+
30
+ /**
31
+ * @param {AutocompleteData | 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: 'Search or enter text',
49
+ options: this.options,
50
+ },
51
+ component: 'Autocomplete',
52
+ model: /** @type {any} */ (this),
53
+ }
54
+
55
+ this.content = response.value
56
+ return { type: 'result', data: { selected: this.content } }
57
+ }
58
+ }
@@ -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
+ }