@vanilla-bean/components 1.1.0 → 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 +226 -40
- package/Component/Component.scenarios.js +2 -2
- package/Component/Component.test.js +427 -40
- package/Component/README.md +480 -0
- package/Component/observeElementConnection.js +1 -1
- package/Elem/README.md +373 -0
- package/README.md +2 -2
- package/components/BottomSheet/BottomSheet.js +8 -16
- package/components/BottomSheet/README.md +1 -1
- package/components/Button/Button.js +12 -18
- package/components/Button/Button.lld.md +1 -1
- 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/Code/Code.lld.md +1 -1
- package/components/ColorPicker/ColorPicker.js +58 -58
- package/components/Dialog/Dialog.js +79 -73
- package/components/Dialog/Dialog.lld.md +1 -1
- package/components/Dialog/README.md +10 -10
- package/components/Form/Form.js +21 -17
- package/components/Form/Form.lld.md +2 -2
- package/components/Form/README.md +3 -3
- package/components/Icon/Icon.js +19 -11
- package/components/Input/Input.js +85 -67
- package/components/Input/README.md +7 -9
- package/components/Keyboard/Key.js +7 -10
- package/components/Keyboard/Keyboard.js +38 -46
- package/components/Keyboard/Keyboard.lld.md +1 -1
- 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/Notify/Notify.lld.md +2 -2
- package/components/Page/Page.lld.md +1 -1
- package/components/Popover/Popover.js +49 -44
- package/components/RadioButton/RadioButton.js +33 -29
- package/components/RadioButton/RadioButton.lld.md +1 -1
- package/components/Router/README.md +12 -12
- package/components/Router/Router.js +19 -21
- package/components/Select/README.md +5 -5
- package/components/Select/Select.js +25 -31
- package/components/Table/README.md +4 -4
- package/components/Table/Table.js +55 -36
- package/components/Table/Table.lld.md +1 -1
- package/components/TagList/Tag.js +16 -14
- package/components/TagList/TagList.js +15 -8
- package/components/TagList/TagList.lld.md +1 -1
- package/components/Tooltip/Tooltip.js +12 -25
- package/components/TooltipWrapper/TooltipWrapper.js +55 -49
- package/components/TooltipWrapper/TooltipWrapper.lld.md +2 -2
- package/components/Whiteboard/Whiteboard.js +45 -43
- package/components/Whiteboard/Whiteboard.lld.md +1 -1
- 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 +120 -62
- package/package.json +23 -22
- package/spellcheck.config.cjs +3 -0
- package/styled/README.md +329 -0
- package/theme/.test.js +2 -2
- package/theme/README.md +8 -8
- package/theme/colors.js +16 -13
- package/theme/colors.test.js +28 -0
- package/utils/browser.js +1 -1
- package/utils/element.js +2 -2
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
|
@@ -29,61 +29,67 @@ const StyledIcon = styled(
|
|
|
29
29
|
* @returns {TooltipWrapper} TooltipWrapper component instance
|
|
30
30
|
*/
|
|
31
31
|
export default class TooltipWrapper extends StyledIcon {
|
|
32
|
-
static
|
|
33
|
-
tooltip
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
static schema = {
|
|
33
|
+
tooltip: {
|
|
34
|
+
set(value) {
|
|
35
|
+
this._applyTooltip(value);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this._tooltip = new Tooltip({
|
|
41
|
-
appendTo: this.elem,
|
|
42
|
-
...tooltipOptions,
|
|
43
|
-
});
|
|
40
|
+
_applyTooltip(value) {
|
|
41
|
+
if (value == null) return;
|
|
42
|
+
const tooltipOptions = typeof value === 'object' ? value : { textContent: value };
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
if (this._tooltip) {
|
|
45
|
+
this._tooltip.setOptions(tooltipOptions);
|
|
46
|
+
} else {
|
|
47
|
+
this._tooltip = new Tooltip({
|
|
48
|
+
appendTo: this.elem,
|
|
49
|
+
...tooltipOptions,
|
|
50
|
+
});
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
targetEvent: 'pointerover',
|
|
49
|
-
callback: ({ clientX, clientY }) => {
|
|
50
|
-
this.tooltipTimeout = setTimeout(() => this._tooltip?.show({ x: clientX, y: clientY }), 700);
|
|
51
|
-
},
|
|
52
|
-
});
|
|
52
|
+
this.addClass('has-tooltip');
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
54
|
+
this.on({
|
|
55
|
+
targetEvent: 'pointerover',
|
|
56
|
+
callback: ({ clientX, clientY }) => {
|
|
57
|
+
this.tooltipTimeout = setTimeout(() => this._tooltip?.show({ x: clientX, y: clientY }), 700);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
61
|
+
this.on({
|
|
62
|
+
targetEvent: 'pointerout',
|
|
63
|
+
callback: () => {
|
|
64
|
+
clearTimeout(this.tooltipTimeout);
|
|
65
|
+
if (!this.elem.contains(document.activeElement)) this._tooltip?.hide();
|
|
66
|
+
},
|
|
67
|
+
});
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
69
|
+
this.on({
|
|
70
|
+
targetEvent: 'focus',
|
|
71
|
+
callback: () => {
|
|
72
|
+
clearTimeout(this.tooltipTimeout);
|
|
73
|
+
const { left, bottom } = this.elem.getBoundingClientRect();
|
|
74
|
+
this._tooltip?.show({ x: left, y: bottom });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this._tooltip = null;
|
|
83
|
-
}
|
|
78
|
+
this.on({
|
|
79
|
+
targetEvent: 'blur',
|
|
80
|
+
callback: () => {
|
|
84
81
|
clearTimeout(this.tooltipTimeout);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
this._tooltip?.hide();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this.replaceCleanup('tooltip', () => {
|
|
87
|
+
if (this._tooltip) {
|
|
88
|
+
this._tooltip.destroy?.();
|
|
89
|
+
this._tooltip = null;
|
|
90
|
+
}
|
|
91
|
+
clearTimeout(this.tooltipTimeout);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
89
95
|
}
|
|
@@ -9,13 +9,13 @@ Component that adds a tooltip to whatever it wraps. The Tooltip is created durin
|
|
|
9
9
|
- the Tooltip component is created when the `tooltip` option is processed during render; after construction it exists as `this._tooltip`
|
|
10
10
|
- does the Tooltip component exist after construction with a tooltip option?
|
|
11
11
|
|
|
12
|
-
## Hover shows after a delay
|
|
12
|
+
## Hover shows after a delay - brief mouse-overs do not trigger the tooltip
|
|
13
13
|
|
|
14
14
|
- pointerover registers a 700ms timer; pointerout cancels it and hides immediately; hovering for longer than 700ms shows the tooltip
|
|
15
15
|
- does hovering for longer than 700ms show the tooltip?
|
|
16
16
|
- does leaving before 700ms prevent the tooltip from appearing?
|
|
17
17
|
|
|
18
|
-
## Cleanup removes the tooltip element
|
|
18
|
+
## Cleanup removes the tooltip element - nothing outlives the component
|
|
19
19
|
|
|
20
20
|
- when the TooltipWrapper is destroyed, the Tooltip component is also destroyed and removed from the DOM
|
|
21
21
|
- does destroying the wrapper also remove the tooltip from the DOM?
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { throttle } from '../../utils/data';
|
|
2
2
|
import { Component } from '../../Component';
|
|
3
3
|
|
|
4
|
-
const defaultOptions = {
|
|
5
|
-
tag: 'canvas',
|
|
6
|
-
background: '#FFF',
|
|
7
|
-
color: '#000',
|
|
8
|
-
lineWidth: 3,
|
|
9
|
-
width: '200px',
|
|
10
|
-
height: '200px',
|
|
11
|
-
readOnly: false,
|
|
12
|
-
registeredEvents: new Set(['line', 'draw']),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
4
|
/**
|
|
16
5
|
* Interactive drawing canvas component with multi-touch support and line rendering.
|
|
17
6
|
*
|
|
@@ -31,22 +20,52 @@ const defaultOptions = {
|
|
|
31
20
|
* @returns {Whiteboard} Whiteboard component instance
|
|
32
21
|
*/
|
|
33
22
|
export default class Whiteboard extends Component {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
style: {
|
|
42
|
-
cursor: 'crosshair',
|
|
43
|
-
touchAction: 'none',
|
|
44
|
-
borderRadius: '3px',
|
|
45
|
-
...options.style,
|
|
46
|
-
},
|
|
23
|
+
static schema = {
|
|
24
|
+
tag: { default: 'canvas' },
|
|
25
|
+
width: {
|
|
26
|
+
default: '200px',
|
|
27
|
+
set(value) {
|
|
28
|
+
this.elem.width = Number.parseInt(value);
|
|
29
|
+
this.elem.style.width = value;
|
|
47
30
|
},
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
},
|
|
32
|
+
height: {
|
|
33
|
+
default: '200px',
|
|
34
|
+
set(value) {
|
|
35
|
+
this.elem.height = Number.parseInt(value);
|
|
36
|
+
this.elem.style.height = value;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
background: {
|
|
40
|
+
default: '#FFF',
|
|
41
|
+
set(value) {
|
|
42
|
+
this.elem.style.background = value;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
lines: {
|
|
46
|
+
set(value) {
|
|
47
|
+
value.forEach(line => this.drawLine(line));
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
// Drawing config read from this.options during draw interactions
|
|
51
|
+
color: { default: '#000' },
|
|
52
|
+
lineWidth: { default: 3 },
|
|
53
|
+
readOnly: { default: false },
|
|
54
|
+
drawThrottle: {},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
static events = ['line', 'draw'];
|
|
58
|
+
|
|
59
|
+
static prepareOptions(options) {
|
|
60
|
+
return {
|
|
61
|
+
...options,
|
|
62
|
+
style: {
|
|
63
|
+
cursor: 'crosshair',
|
|
64
|
+
touchAction: 'none',
|
|
65
|
+
borderRadius: '3px',
|
|
66
|
+
...options.style,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
build() {
|
|
@@ -59,23 +78,6 @@ export default class Whiteboard extends Component {
|
|
|
59
78
|
this.replaceCleanup('pointerdown', () => this.elem.removeEventListener('pointerdown', boundInteractionInit));
|
|
60
79
|
}
|
|
61
80
|
|
|
62
|
-
static handlers = {
|
|
63
|
-
width(value) {
|
|
64
|
-
this.elem.width = Number.parseInt(value);
|
|
65
|
-
this.elem.style.width = value;
|
|
66
|
-
},
|
|
67
|
-
height(value) {
|
|
68
|
-
this.elem.height = Number.parseInt(value);
|
|
69
|
-
this.elem.style.height = value;
|
|
70
|
-
},
|
|
71
|
-
lines(value) {
|
|
72
|
-
value.forEach(line => this.drawLine(line));
|
|
73
|
-
},
|
|
74
|
-
background(value) {
|
|
75
|
-
this.elem.style.background = value;
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
81
|
getPosition({ offsetX, offsetY }) {
|
|
80
82
|
return { x: offsetX.toFixed(3), y: offsetY.toFixed(3) };
|
|
81
83
|
}
|
|
@@ -10,7 +10,7 @@ Multi-touch drawing canvas that tracks each pointer independently. The design de
|
|
|
10
10
|
- the whiteboard tracks each active pointer by its ID
|
|
11
11
|
- does a second finger touching the canvas start a separate line from the first?
|
|
12
12
|
|
|
13
|
-
## Draw throttle rate adapts to line width
|
|
13
|
+
## Draw throttle rate adapts to line width - no separate configuration needed
|
|
14
14
|
|
|
15
15
|
- the throttle delay is derived from `lineWidth + 3` (clamped to a range); thicker lines are visually coarser and tolerate a longer delay
|
|
16
16
|
- callers set `lineWidth` and the draw rate adjusts automatically; they do not configure throttle separately
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { watch } from 'fs';
|
|
2
|
+
import { buildLoader as asText } from '../plugins/asText';
|
|
3
|
+
import { buildLoader as markdownLoader } from '../plugins/markdownLoader';
|
|
4
|
+
|
|
5
|
+
const build = async () => {
|
|
6
|
+
console.log('Building...');
|
|
7
|
+
|
|
8
|
+
const buildResults = await Bun.build({
|
|
9
|
+
entrypoints: ['demo/index.html'],
|
|
10
|
+
outdir: 'demo/build',
|
|
11
|
+
define: {
|
|
12
|
+
'process.env.AUTOPREFIXER_GRID': 'undefined',
|
|
13
|
+
'process.cwd': 'String',
|
|
14
|
+
},
|
|
15
|
+
plugins: [asText, markdownLoader],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
console.log(buildResults.success ? 'build.success' : buildResults.logs);
|
|
19
|
+
|
|
20
|
+
return buildResults;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const enableWatcher = process.argv[2] === '--watch';
|
|
24
|
+
const watcherIgnore = /\.asText$|^demo\/build|^\.|^img\/|^devTools\//;
|
|
25
|
+
|
|
26
|
+
if (enableWatcher) {
|
|
27
|
+
console.log(`Initializing watcher`);
|
|
28
|
+
|
|
29
|
+
const watcher = watch(`${import.meta.dir}/..`, { recursive: true }, (event, filename) => {
|
|
30
|
+
if (watcherIgnore.test(filename)) return;
|
|
31
|
+
|
|
32
|
+
console.log(`Detected ${event} in ${filename}`);
|
|
33
|
+
|
|
34
|
+
build();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
process.on('SIGINT', () => {
|
|
38
|
+
watcher.close();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await build();
|
|
@@ -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();
|