@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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MaskHandler — Universal interactive mask controller.
|
|
3
|
+
*
|
|
4
|
+
* Supports masks where:
|
|
5
|
+
* `9`, `0`, `#` = digit placeholder
|
|
6
|
+
* `A` = letter placeholder
|
|
7
|
+
* `_` = any character placeholder
|
|
8
|
+
* anything else = literal (displayed as-is)
|
|
9
|
+
*
|
|
10
|
+
* Recognises the mask's static prefix (e.g. "+38" in "+38 (099) 999 9999")
|
|
11
|
+
* and strips it from raw user input to avoid duplication.
|
|
12
|
+
*
|
|
13
|
+
* @module core/MaskHandler
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} ch - single mask character
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isPlaceholder(ch) {
|
|
21
|
+
return ch === '9' || ch === '0' || ch === '#' || ch === '_' || ch === 'A'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class MaskHandler {
|
|
25
|
+
constructor(mask) {
|
|
26
|
+
this.mask = mask // e.g. "+38 (099) 999 9999"
|
|
27
|
+
this.raw = '' // raw user digits (without mask prefix)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** How many placeholder positions the mask has */
|
|
31
|
+
get _slotCount() {
|
|
32
|
+
let n = 0
|
|
33
|
+
for (const ch of this.mask) if (isPlaceholder(ch)) n++
|
|
34
|
+
return n
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Static prefix of the mask (literal characters before first placeholder) */
|
|
38
|
+
get _prefix() {
|
|
39
|
+
let p = ''
|
|
40
|
+
for (const ch of this.mask) {
|
|
41
|
+
if (isPlaceholder(ch)) break
|
|
42
|
+
p += ch
|
|
43
|
+
}
|
|
44
|
+
return p
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get isComplete() {
|
|
48
|
+
return this.raw.length >= this._slotCount
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get formatted() {
|
|
52
|
+
let result = ''
|
|
53
|
+
let rawIndex = 0
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < this.mask.length; i++) {
|
|
56
|
+
const m = this.mask[i]
|
|
57
|
+
|
|
58
|
+
if (isPlaceholder(m)) {
|
|
59
|
+
if (rawIndex < this.raw.length) {
|
|
60
|
+
result += this.raw[rawIndex++]
|
|
61
|
+
} else {
|
|
62
|
+
result += '_' // visual unfilled placeholder
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Literal character in the mask
|
|
66
|
+
result += m
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Append a digit/letter character.
|
|
74
|
+
* Only accepts characters that fit into placeholders.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} char
|
|
77
|
+
* @returns {boolean} true if accepted
|
|
78
|
+
*/
|
|
79
|
+
input(char) {
|
|
80
|
+
if (this.raw.length >= this._slotCount) return false // already full
|
|
81
|
+
|
|
82
|
+
if (/[0-9a-zA-Z]/.test(char)) {
|
|
83
|
+
this.raw += char
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove last character
|
|
91
|
+
*/
|
|
92
|
+
backspace() {
|
|
93
|
+
if (this.raw.length > 0) {
|
|
94
|
+
this.raw = this.raw.slice(0, -1)
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set a full value, intelligently stripping the mask's static prefix
|
|
102
|
+
* if the user pasted or injected the full formatted number.
|
|
103
|
+
*
|
|
104
|
+
* e.g. setValue('+380660848404') with mask '+38 (099) 999 9999'
|
|
105
|
+
* strips "+38" → raw = '0660848404'
|
|
106
|
+
*
|
|
107
|
+
* @param {string} val
|
|
108
|
+
*/
|
|
109
|
+
setValue(val) {
|
|
110
|
+
this.raw = ''
|
|
111
|
+
// Strip all non-alphanumeric from the value
|
|
112
|
+
let clean = String(val).replace(/[^a-zA-Z0-9]/g, '')
|
|
113
|
+
|
|
114
|
+
// Strip mask's static prefix digits if present
|
|
115
|
+
const prefixDigits = this._prefix.replace(/[^a-zA-Z0-9]/g, '')
|
|
116
|
+
if (prefixDigits && clean.startsWith(prefixDigits)) {
|
|
117
|
+
clean = clean.substring(prefixDigits.length)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Feed only as many characters as we have slots
|
|
121
|
+
for (const c of clean) {
|
|
122
|
+
if (!this.input(c)) break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/core/index.js
CHANGED
|
@@ -39,3 +39,10 @@ export {
|
|
|
39
39
|
Progress,
|
|
40
40
|
} from './Flow.js'
|
|
41
41
|
export { default as Flow } from './Flow.js'
|
|
42
|
+
|
|
43
|
+
// OLMUI Generator Engine — Intent-based Model→Adapter contract
|
|
44
|
+
export { validateIntent, ask, progress, log, result, INTENT_TYPES, isModelSchema } from './Intent.js'
|
|
45
|
+
export { IntentErrorModel } from './IntentErrorModel.js'
|
|
46
|
+
export { runGenerator } from './GeneratorRunner.js'
|
|
47
|
+
|
|
48
|
+
export { MaskHandler } from './MaskHandler.js'
|
|
@@ -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
|
+
}
|