@vanilla-bean/components 1.1.1 → 2.0.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/Component/Component.js +222 -36
- package/Component/Component.test.js +425 -38
- package/Component/README.md +480 -0
- package/Elem/README.md +373 -0
- package/README.md +1 -1
- package/components/BottomSheet/BottomSheet.js +8 -16
- package/components/Button/Button.js +12 -18
- package/components/Calendar/Calendar.js +139 -59
- package/components/Calendar/CalendarEvent.js +9 -4
- package/components/Calendar/Toolbar.js +28 -20
- package/components/Calendar/index.js +1 -0
- package/components/Code/Code.js +26 -30
- package/components/ColorPicker/ColorPicker.js +57 -57
- package/components/Dialog/Dialog.js +78 -72
- package/components/Form/Form.js +12 -8
- package/components/Icon/Icon.js +19 -11
- package/components/Input/Input.js +85 -67
- package/components/Input/README.md +0 -2
- package/components/Keyboard/Key.js +7 -10
- package/components/Keyboard/Keyboard.js +38 -46
- package/components/Label/Label.js +52 -50
- package/components/Link/Link.js +14 -20
- package/components/List/List.js +28 -30
- package/components/Menu/Menu.js +19 -10
- package/components/Menu/Menu.lld.md +2 -2
- package/components/Notify/Notify.js +29 -19
- package/components/Popover/Popover.js +49 -44
- package/components/RadioButton/RadioButton.js +33 -29
- package/components/Router/Router.js +19 -21
- package/components/Select/Select.js +24 -30
- package/components/Table/Table.js +55 -36
- package/components/TagList/Tag.js +16 -14
- package/components/TagList/TagList.js +15 -8
- package/components/Tooltip/Tooltip.js +12 -25
- package/components/TooltipWrapper/TooltipWrapper.js +55 -49
- package/components/Whiteboard/Whiteboard.js +45 -43
- package/devTools/build.js +43 -0
- package/devTools/buildTypes.js +322 -0
- package/devTools/createComponent.js +155 -0
- package/devTools/extractJSDoc.js +395 -0
- package/devTools/processTemplate.js +500 -0
- package/devTools/updateComponentIndex.js +16 -0
- package/devTools/updateDemoViewIndex.js +90 -0
- package/eslint.config.cjs +5 -2
- package/index.d.ts +117 -59
- package/package.json +23 -22
- package/spellcheck.config.cjs +3 -0
- package/styled/README.md +329 -0
- package/theme/colors.js +15 -12
- package/theme/colors.test.js +28 -0
- package/utils/element.js +2 -2
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
|
6
|
+
|
|
7
|
+
import { extractJSDoc } from './extractJSDoc.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const root = join(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
// Components need a DOM to import; happy-dom stands in so the schema statics can be read
|
|
13
|
+
GlobalRegistrator.register({ width: 1920, height: 1080 });
|
|
14
|
+
|
|
15
|
+
const library = await import(join(root, 'index.js'));
|
|
16
|
+
const { Component } = library;
|
|
17
|
+
|
|
18
|
+
const GENERATED_START = '// ── GENERATED: Built-in components ────────────────────────────────────────────';
|
|
19
|
+
const GENERATED_END = '// ── END GENERATED ────────────────────────────────────────────────────────────';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert a JSDoc type string (e.g. `{string|Function}`) to a TypeScript type string.
|
|
23
|
+
* @param {string} type - Raw JSDoc type including surrounding braces
|
|
24
|
+
* @returns {string} TypeScript-compatible type string
|
|
25
|
+
*/
|
|
26
|
+
function jsDocTypeToTS(type) {
|
|
27
|
+
if (!type) return 'any';
|
|
28
|
+
|
|
29
|
+
return type
|
|
30
|
+
.trim()
|
|
31
|
+
.replace(/^\{|\}$/g, '')
|
|
32
|
+
.replace(/\*/g, 'any')
|
|
33
|
+
.replace(/\bFunction\b/g, '(...args: any[]) => any')
|
|
34
|
+
.replace(/\bobject\b/g, 'Record<string, any>')
|
|
35
|
+
.replace(/\bArray\b(?!<)/g, 'Array<any>')
|
|
36
|
+
.replace(/\|/g, ' | ')
|
|
37
|
+
.replace(/\s{2,}/g, ' ')
|
|
38
|
+
.trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const COMPONENT_OPTIONS_KEYS = new Set([
|
|
42
|
+
'tag',
|
|
43
|
+
'autoRender',
|
|
44
|
+
'styles',
|
|
45
|
+
'uniqueId',
|
|
46
|
+
'style',
|
|
47
|
+
'attributes',
|
|
48
|
+
'className',
|
|
49
|
+
'id',
|
|
50
|
+
'textContent',
|
|
51
|
+
'innerText',
|
|
52
|
+
'innerHTML',
|
|
53
|
+
'content',
|
|
54
|
+
'appendTo',
|
|
55
|
+
'prependTo',
|
|
56
|
+
'append',
|
|
57
|
+
'prepend',
|
|
58
|
+
'before',
|
|
59
|
+
'disabled',
|
|
60
|
+
'onclick',
|
|
61
|
+
'onConnected',
|
|
62
|
+
'onDisconnected',
|
|
63
|
+
'onRendered',
|
|
64
|
+
'onPointerPress',
|
|
65
|
+
'onHover',
|
|
66
|
+
'onPointerOver',
|
|
67
|
+
'onPointerEnter',
|
|
68
|
+
'onPointerDown',
|
|
69
|
+
'onPointerMove',
|
|
70
|
+
'onPointerUp',
|
|
71
|
+
'onPointerLeave',
|
|
72
|
+
'onPointerCancel',
|
|
73
|
+
'onContextmenu',
|
|
74
|
+
'onChange',
|
|
75
|
+
'onKeydown',
|
|
76
|
+
'onKeyup',
|
|
77
|
+
'onInput',
|
|
78
|
+
'onBlur',
|
|
79
|
+
'onSearch',
|
|
80
|
+
'addClass',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Merges static schema declarations up a component's constructor chain.
|
|
85
|
+
* Parent schemas apply first so child classes override per descriptor field,
|
|
86
|
+
* mirroring the runtime's nearest-class-wins semantics.
|
|
87
|
+
* @param {Function} klass - Component subclass to walk
|
|
88
|
+
* @param {Function} [stopAt] - Ancestor class to stop before (exclusive), so subclasses only report their own keys
|
|
89
|
+
* @returns {object} Merged descriptor info keyed by option name
|
|
90
|
+
*/
|
|
91
|
+
function collectSchema(klass, stopAt = Component) {
|
|
92
|
+
const chain = [];
|
|
93
|
+
let current = klass;
|
|
94
|
+
|
|
95
|
+
while (current && current !== stopAt && current !== Component) {
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(current, 'schema') && current.schema) chain.unshift(current.schema);
|
|
97
|
+
current = Object.getPrototypeOf(current);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const merged = {};
|
|
101
|
+
|
|
102
|
+
for (const schema of chain) {
|
|
103
|
+
for (const [key, descriptor] of Object.entries(schema)) {
|
|
104
|
+
if (!descriptor) continue;
|
|
105
|
+
const info = (merged[key] ??= {});
|
|
106
|
+
if ('default' in descriptor) {
|
|
107
|
+
info.hasDefault = true;
|
|
108
|
+
try {
|
|
109
|
+
info.default = descriptor.default;
|
|
110
|
+
} catch {
|
|
111
|
+
info.hasDefault = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if ('enum' in descriptor) info.enum = descriptor.enum;
|
|
115
|
+
if ('data' in descriptor) info.data = !!descriptor.data;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return merged;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Derive a TypeScript type for a schema key from its enum or default value.
|
|
124
|
+
* @param {object} info - Merged descriptor info from collectSchema
|
|
125
|
+
* @returns {string} TypeScript type string
|
|
126
|
+
*/
|
|
127
|
+
function schemaKeyType(info) {
|
|
128
|
+
if (info.enum?.length) return info.enum.map(value => JSON.stringify(value)).join(' | ');
|
|
129
|
+
|
|
130
|
+
if (info.hasDefault) {
|
|
131
|
+
const type = typeof info.default;
|
|
132
|
+
if (type === 'string' || type === 'number' || type === 'boolean') return type;
|
|
133
|
+
if (type === 'function') return '(...args: any[]) => any';
|
|
134
|
+
if (Array.isArray(info.default)) return 'Array<any>';
|
|
135
|
+
if (type === 'object' && info.default !== null) return 'Record<string, any>';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return 'any';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Format a schema default as a doc-comment suffix, primitives only.
|
|
143
|
+
* @param {object} info - Merged descriptor info from collectSchema
|
|
144
|
+
* @returns {string} ' (default: ...)' or empty string
|
|
145
|
+
*/
|
|
146
|
+
function defaultSuffix(info) {
|
|
147
|
+
if (!info?.hasDefault) return '';
|
|
148
|
+
const type = typeof info.default;
|
|
149
|
+
if (type !== 'string' && type !== 'number' && type !== 'boolean') return '';
|
|
150
|
+
|
|
151
|
+
return ` (default: ${JSON.stringify(info.default)})`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate TypeScript interface and class declaration for a single component.
|
|
156
|
+
* The schema supplies key existence, defaults, and enum unions; JSDoc supplies types and descriptions.
|
|
157
|
+
* Interfaces and class declarations extend the component's nearest exported ancestor,
|
|
158
|
+
* mirroring the runtime hierarchy so inherited options and methods carry through.
|
|
159
|
+
* @param {string} name - Component class name (e.g. "Button")
|
|
160
|
+
* @param {object} jsDoc - Parsed JSDoc object from extractJSDoc
|
|
161
|
+
* @param {object} [schema] - Merged descriptor info from collectSchema
|
|
162
|
+
* @param {object} [bases] - Base names for the generated declarations
|
|
163
|
+
* @param {string} [bases.baseClass] - Class the declared class extends
|
|
164
|
+
* @param {string} [bases.baseInterface] - Interface the options interface extends
|
|
165
|
+
* @returns {string} TypeScript declaration block
|
|
166
|
+
*/
|
|
167
|
+
function generateComponentInterface(
|
|
168
|
+
name,
|
|
169
|
+
jsDoc,
|
|
170
|
+
schema = {},
|
|
171
|
+
{ baseClass = 'Component', baseInterface = 'ComponentOptions' } = {},
|
|
172
|
+
) {
|
|
173
|
+
const lines = [];
|
|
174
|
+
const componentOptions = jsDoc.options.filter(
|
|
175
|
+
opt => (opt.name !== 'options' || opt.isSubProperty) && !COMPONENT_OPTIONS_KEYS.has(opt.name),
|
|
176
|
+
);
|
|
177
|
+
const documentedKeys = new Set(componentOptions.map(opt => opt.name));
|
|
178
|
+
const schemaOnlyKeys = Object.keys(schema).filter(
|
|
179
|
+
key => !documentedKeys.has(key) && !COMPONENT_OPTIONS_KEYS.has(key),
|
|
180
|
+
);
|
|
181
|
+
const methods = jsDoc.methods.filter(m => !m.name.startsWith('_'));
|
|
182
|
+
|
|
183
|
+
if (componentOptions.length > 0 || schemaOnlyKeys.length > 0) {
|
|
184
|
+
lines.push(`export interface ${name}Options extends ${baseInterface} {`);
|
|
185
|
+
for (const opt of componentOptions) {
|
|
186
|
+
const info = schema[opt.name];
|
|
187
|
+
// The schema is ground truth for valid values and defaults; JSDoc supplies types and descriptions
|
|
188
|
+
const tsType = info?.enum?.length ? schemaKeyType(info) : jsDocTypeToTS(opt.type);
|
|
189
|
+
const description = `${opt.description || ''}${defaultSuffix(info)}`.trim();
|
|
190
|
+
if (description) lines.push(`\t/** ${description} */`);
|
|
191
|
+
lines.push(`\t${opt.name}?: ${tsType};`);
|
|
192
|
+
}
|
|
193
|
+
for (const key of schemaOnlyKeys) {
|
|
194
|
+
const info = schema[key];
|
|
195
|
+
const description = defaultSuffix(info).trim();
|
|
196
|
+
if (description) lines.push(`\t/** ${description.replace(/^\(|\)$/g, '')} */`);
|
|
197
|
+
lines.push(`\t${key}?: ${schemaKeyType(info)};`);
|
|
198
|
+
}
|
|
199
|
+
lines.push('}');
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const getters = jsDoc.properties.filter(p => p.access === 'readonly' && p.type !== 'unknown');
|
|
204
|
+
|
|
205
|
+
const optionsType = componentOptions.length > 0 || schemaOnlyKeys.length > 0 ? `${name}Options` : baseInterface;
|
|
206
|
+
lines.push(`export declare class ${name} extends ${baseClass} {`);
|
|
207
|
+
lines.push(`\tconstructor(options?: ${optionsType}, ...children: Array<Elem | HTMLElement | string>);`);
|
|
208
|
+
|
|
209
|
+
for (const getter of getters) {
|
|
210
|
+
const tsType = jsDocTypeToTS(`{${getter.type}}`);
|
|
211
|
+
if (getter.description) lines.push(`\t/** ${getter.description} */`);
|
|
212
|
+
lines.push(`\treadonly ${getter.name}: ${tsType};`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const method of methods) {
|
|
216
|
+
const params = method.parameters.map(p => `${p.name}${p.optional ? '?' : ''}: ${jsDocTypeToTS(p.type)}`).join(', ');
|
|
217
|
+
const ret = method.returns ? jsDocTypeToTS(method.returns.type) : 'void';
|
|
218
|
+
if (method.description) lines.push(`\t/** ${method.description} */`);
|
|
219
|
+
lines.push(`\t${method.name}(${params}): ${ret};`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
lines.push('}');
|
|
223
|
+
lines.push('');
|
|
224
|
+
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Scan the components directory and build the generated type declaration block.
|
|
230
|
+
* @returns {{ block: string, count: number }} Generated TS source and component count
|
|
231
|
+
*/
|
|
232
|
+
function buildComponentTypes() {
|
|
233
|
+
const componentsDir = join(root, 'components');
|
|
234
|
+
const dirs = readdirSync(componentsDir, { withFileTypes: true })
|
|
235
|
+
.filter(d => d.isDirectory() && existsSync(join(componentsDir, d.name, `${d.name}.js`)))
|
|
236
|
+
.map(d => d.name)
|
|
237
|
+
.sort();
|
|
238
|
+
|
|
239
|
+
// Map exported component classes to their names so subclasses can extend
|
|
240
|
+
// their nearest exported ancestor instead of flattening to Component
|
|
241
|
+
const exportedClasses = new Map();
|
|
242
|
+
for (const dir of dirs) {
|
|
243
|
+
if (typeof library[dir] === 'function') exportedClasses.set(library[dir], dir);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const nearestAncestor = klass => {
|
|
247
|
+
let current = Object.getPrototypeOf(klass);
|
|
248
|
+
while (current && current !== Component) {
|
|
249
|
+
if (exportedClasses.has(current)) return current;
|
|
250
|
+
current = Object.getPrototypeOf(current);
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// First pass: gather each component's JSDoc, own-schema (below its exported ancestor), and parentage
|
|
256
|
+
const components = dirs.map(dir => {
|
|
257
|
+
const klass = library[dir];
|
|
258
|
+
const parentClass = typeof klass === 'function' ? nearestAncestor(klass) : null;
|
|
259
|
+
const schema = typeof klass === 'function' ? collectSchema(klass, parentClass ?? Component) : {};
|
|
260
|
+
|
|
261
|
+
return { dir, jsDoc: extractJSDoc(join(componentsDir, dir, `${dir}.js`)), schema, parentClass };
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const hasInterface = new Set(
|
|
265
|
+
components
|
|
266
|
+
.filter(({ jsDoc, schema }) => {
|
|
267
|
+
const documented = jsDoc.options.filter(
|
|
268
|
+
opt => (opt.name !== 'options' || opt.isSubProperty) && !COMPONENT_OPTIONS_KEYS.has(opt.name),
|
|
269
|
+
);
|
|
270
|
+
return documented.length > 0 || Object.keys(schema).some(key => !COMPONENT_OPTIONS_KEYS.has(key));
|
|
271
|
+
})
|
|
272
|
+
.map(({ dir }) => dir),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const generated = [
|
|
276
|
+
GENERATED_START,
|
|
277
|
+
'// This section is generated by devTools/buildTypes.js - do not edit manually.',
|
|
278
|
+
'// Run `bun run build:types` to regenerate from static schema declarations and JSDoc annotations.',
|
|
279
|
+
'',
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (const { dir, jsDoc, schema, parentClass } of components) {
|
|
283
|
+
const parentName = parentClass ? exportedClasses.get(parentClass) : null;
|
|
284
|
+
|
|
285
|
+
generated.push(
|
|
286
|
+
generateComponentInterface(dir, jsDoc, schema, {
|
|
287
|
+
baseClass: parentName || 'Component',
|
|
288
|
+
baseInterface: parentName && hasInterface.has(parentName) ? `${parentName}Options` : 'ComponentOptions',
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
generated.push(GENERATED_END);
|
|
294
|
+
|
|
295
|
+
return { block: generated.join('\n'), count: dirs.length };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Read index.d.ts, replace the generated component block, and write it back.
|
|
300
|
+
*/
|
|
301
|
+
function updateIndexDts() {
|
|
302
|
+
const indexDtsPath = join(root, 'index.d.ts');
|
|
303
|
+
const content = readFileSync(indexDtsPath, 'utf8');
|
|
304
|
+
const { block, count } = buildComponentTypes();
|
|
305
|
+
|
|
306
|
+
let updated;
|
|
307
|
+
if (content.includes(GENERATED_START) && content.includes(GENERATED_END)) {
|
|
308
|
+
const start = content.indexOf(GENERATED_START);
|
|
309
|
+
const end = content.indexOf(GENERATED_END) + GENERATED_END.length;
|
|
310
|
+
updated = content.slice(0, start) + block + content.slice(end);
|
|
311
|
+
} else {
|
|
312
|
+
const builtInMarker = '// ── Built-in components ──';
|
|
313
|
+
const markerIndex = content.indexOf(builtInMarker);
|
|
314
|
+
updated =
|
|
315
|
+
markerIndex !== -1 ? content.slice(0, markerIndex) + block + '\n' : content.trimEnd() + '\n\n' + block + '\n';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
writeFileSync(indexDtsPath, updated, 'utf8');
|
|
319
|
+
console.log(`✓ index.d.ts updated - ${count} components`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
updateIndexDts();
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import { capitalize } from '../utils/string';
|
|
4
|
+
|
|
5
|
+
if (!process.argv[2]) {
|
|
6
|
+
console.error('Usage: bun run create:component <ComponentName> [optionsJSON]');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let options = {};
|
|
11
|
+
try {
|
|
12
|
+
options = process.argv[3] ? JSON.parse(process.argv[3]) : {};
|
|
13
|
+
} catch {
|
|
14
|
+
console.error('Invalid JSON options:', process.argv[3]);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const name = capitalize(process.argv[2]);
|
|
19
|
+
const componentFolder = `components/${name}`;
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(componentFolder)) {
|
|
22
|
+
console.error(`Component '${name}' already exists at ${componentFolder}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fs.mkdirSync(componentFolder);
|
|
27
|
+
|
|
28
|
+
const demoOptions = Object.entries(options)
|
|
29
|
+
.map(
|
|
30
|
+
([key, value]) =>
|
|
31
|
+
`${key}: ${
|
|
32
|
+
typeof value === 'object'
|
|
33
|
+
? JSON.stringify(value)
|
|
34
|
+
.replaceAll(/"(\w+)":/g, ` $1: `)
|
|
35
|
+
.replaceAll('"', `'`)
|
|
36
|
+
: value
|
|
37
|
+
},`,
|
|
38
|
+
)
|
|
39
|
+
.join('\n');
|
|
40
|
+
|
|
41
|
+
const testFile = `import { ${name} } from '.';
|
|
42
|
+
|
|
43
|
+
describe('${name}', () => {
|
|
44
|
+
test('renders', () => {
|
|
45
|
+
new ${name}({ appendTo: container });
|
|
46
|
+
|
|
47
|
+
expect(container.firstElementChild).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const demoFile = `import DemoView from '../../demo/DemoView';
|
|
53
|
+
import { ${name} } from '.';
|
|
54
|
+
|
|
55
|
+
export default class Demo extends DemoView {
|
|
56
|
+
build() {
|
|
57
|
+
this.component = new ${name}({
|
|
58
|
+
${demoOptions}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const readMeFile = `# ${name}
|
|
65
|
+
|
|
66
|
+
[[extract-description ${name}.js]]
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
\`\`\`js
|
|
71
|
+
[[import ./demo.js]]/^(\\t*).+component = new.+\\(\\s*{(.|\\n)+?\\1}?\\);/gm
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
[[extract-options ${name}.js]]
|
|
77
|
+
|
|
78
|
+
## Methods
|
|
79
|
+
|
|
80
|
+
[[extract-methods ${name}.js]]
|
|
81
|
+
|
|
82
|
+
## Properties
|
|
83
|
+
|
|
84
|
+
[[extract-properties ${name}.js]]
|
|
85
|
+
|
|
86
|
+
## Events
|
|
87
|
+
|
|
88
|
+
[[extract-events ${name}.js]]
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
[[extract-imports ${name}.js]]
|
|
93
|
+
|
|
94
|
+
## Design
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
const componentFile = `import { Component } from '../../Component';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* [Component description - describe what this component does and its primary purpose]
|
|
103
|
+
*
|
|
104
|
+
* [Optional: Additional context about when to use this component or special behaviors]
|
|
105
|
+
*
|
|
106
|
+
* @param {object} [options={}] - Component configuration options
|
|
107
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
108
|
+
* @returns {${name}} Component instance with reactive options
|
|
109
|
+
* @example
|
|
110
|
+
* // Basic usage
|
|
111
|
+
* new ${name}({
|
|
112
|
+
* // Add example options here
|
|
113
|
+
* });
|
|
114
|
+
*/
|
|
115
|
+
export default class ${name} extends Component {
|
|
116
|
+
/**
|
|
117
|
+
* Creates component structure before options are processed.
|
|
118
|
+
* Assign child elements to instance properties here so handlers can reference them.
|
|
119
|
+
*/
|
|
120
|
+
build() {
|
|
121
|
+
// Create child elements here - this runs before options are processed
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Declare the option schema once with static schema - what can exist, the defaults,
|
|
125
|
+
// and how each key routes. Descriptor fields:
|
|
126
|
+
// default - initial value, merged automatically (child classes override per-key)
|
|
127
|
+
// set(value, next) - change handler; omit next() to own the key, call it to continue
|
|
128
|
+
// enum: [...] - valid values; anything else throws
|
|
129
|
+
// attribute: true / priority: true - setAttribute routing / processed first
|
|
130
|
+
// Declared keys with no DOM match live in this.options silently - a bare {} declares
|
|
131
|
+
// a data-only key. data: true exists only to force store-only on DOM-name collisions.
|
|
132
|
+
//
|
|
133
|
+
// static schema = {
|
|
134
|
+
// label: {
|
|
135
|
+
// default: 'Untitled',
|
|
136
|
+
// set(value) {
|
|
137
|
+
// this.myChild.elem.textContent = value;
|
|
138
|
+
// },
|
|
139
|
+
// },
|
|
140
|
+
// items: {},
|
|
141
|
+
// };
|
|
142
|
+
//
|
|
143
|
+
// Custom events: static events = ['select'];
|
|
144
|
+
// Computed defaults (no constructor needed): static prepareOptions(options, children) { return options; }
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
await Bun.write(`${componentFolder}/${name}.js`, componentFile);
|
|
149
|
+
await Bun.write(`${componentFolder}/.test.js`, testFile);
|
|
150
|
+
await Bun.write(`${componentFolder}/demo.js`, demoFile);
|
|
151
|
+
await Bun.write(`${componentFolder}/README.md`, readMeFile);
|
|
152
|
+
await Bun.write(`${componentFolder}/index.js`, `export { default as ${name} } from './${name}';`);
|
|
153
|
+
|
|
154
|
+
await Bun.spawn(['bun', 'run', 'build:index']).exited;
|
|
155
|
+
await Bun.spawn(['bun', 'run', 'format']).exited;
|