dynim-core 1.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/README.md +290 -0
- package/dist/builder/ai-prompt-popover.d.ts +26 -0
- package/dist/builder/ai-prompt-popover.d.ts.map +1 -0
- package/dist/builder/ai-prompt-popover.js +180 -0
- package/dist/builder/builder-client.d.ts +48 -0
- package/dist/builder/builder-client.d.ts.map +1 -0
- package/dist/builder/builder-client.js +157 -0
- package/dist/builder/builder.d.ts +41 -0
- package/dist/builder/builder.d.ts.map +1 -0
- package/dist/builder/builder.js +537 -0
- package/dist/builder/bundle-manager.d.ts +60 -0
- package/dist/builder/bundle-manager.d.ts.map +1 -0
- package/dist/builder/bundle-manager.js +357 -0
- package/dist/builder/classifier/classname-analyzer.d.ts +6 -0
- package/dist/builder/classifier/classname-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/classname-analyzer.js +107 -0
- package/dist/builder/classifier/index.d.ts +20 -0
- package/dist/builder/classifier/index.d.ts.map +1 -0
- package/dist/builder/classifier/index.js +181 -0
- package/dist/builder/classifier/semantic-analyzer.d.ts +24 -0
- package/dist/builder/classifier/semantic-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/semantic-analyzer.js +94 -0
- package/dist/builder/classifier/size-analyzer.d.ts +7 -0
- package/dist/builder/classifier/size-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/size-analyzer.js +120 -0
- package/dist/builder/classifier/visual-analyzer.d.ts +6 -0
- package/dist/builder/classifier/visual-analyzer.d.ts.map +1 -0
- package/dist/builder/classifier/visual-analyzer.js +158 -0
- package/dist/builder/client.d.ts +22 -0
- package/dist/builder/client.d.ts.map +1 -0
- package/dist/builder/client.js +54 -0
- package/dist/builder/code-client.d.ts +101 -0
- package/dist/builder/code-client.d.ts.map +1 -0
- package/dist/builder/code-client.js +418 -0
- package/dist/builder/diff-state.d.ts +24 -0
- package/dist/builder/diff-state.d.ts.map +1 -0
- package/dist/builder/diff-state.js +134 -0
- package/dist/builder/dom-scanner.d.ts +20 -0
- package/dist/builder/dom-scanner.d.ts.map +1 -0
- package/dist/builder/dom-scanner.js +102 -0
- package/dist/builder/drag-engine.d.ts +41 -0
- package/dist/builder/drag-engine.d.ts.map +1 -0
- package/dist/builder/drag-engine.js +686 -0
- package/dist/builder/editor-overlays.d.ts +31 -0
- package/dist/builder/editor-overlays.d.ts.map +1 -0
- package/dist/builder/editor-overlays.js +202 -0
- package/dist/builder/editor-state.d.ts +50 -0
- package/dist/builder/editor-state.d.ts.map +1 -0
- package/dist/builder/editor-state.js +132 -0
- package/dist/builder/element-utils.d.ts +43 -0
- package/dist/builder/element-utils.d.ts.map +1 -0
- package/dist/builder/element-utils.js +227 -0
- package/dist/builder/fiber-capture.d.ts +28 -0
- package/dist/builder/fiber-capture.d.ts.map +1 -0
- package/dist/builder/fiber-capture.js +264 -0
- package/dist/builder/freeze-overlay.d.ts +26 -0
- package/dist/builder/freeze-overlay.d.ts.map +1 -0
- package/dist/builder/freeze-overlay.js +213 -0
- package/dist/builder/history-state.d.ts +41 -0
- package/dist/builder/history-state.d.ts.map +1 -0
- package/dist/builder/history-state.js +76 -0
- package/dist/builder/index.d.ts +62 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +92 -0
- package/dist/builder/state.d.ts +27 -0
- package/dist/builder/state.d.ts.map +1 -0
- package/dist/builder/state.js +50 -0
- package/dist/builder/style-applier.d.ts +61 -0
- package/dist/builder/style-applier.d.ts.map +1 -0
- package/dist/builder/style-applier.js +311 -0
- package/dist/builder/tree-state.d.ts +71 -0
- package/dist/builder/tree-state.d.ts.map +1 -0
- package/dist/builder/tree-state.js +168 -0
- package/dist/builder/widget.d.ts +29 -0
- package/dist/builder/widget.d.ts.map +1 -0
- package/dist/builder/widget.js +181 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/package.json +25 -0
- package/src/styles/base.css +378 -0
- package/src/styles/builder.css +422 -0
- package/src/styles/editor.css +131 -0
- package/src/styles/themes/dark.css +24 -0
- package/src/styles/themes/light.css +21 -0
- package/src/styles/variables.css +63 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle Manager - Handles loading and swapping React bundles with smooth crossfade transitions
|
|
3
|
+
*
|
|
4
|
+
* Uses dual containers to enable seamless bundle swapping:
|
|
5
|
+
* 1. Load new bundle into inactive container (hidden)
|
|
6
|
+
* 2. Wait for React to mount
|
|
7
|
+
* 3. Crossfade from active to new container
|
|
8
|
+
* 4. Cleanup old container
|
|
9
|
+
*
|
|
10
|
+
* This prevents any flash or blank screen during bundle updates.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Create a bundle manager instance
|
|
14
|
+
*/
|
|
15
|
+
export function createBundleManager(config) {
|
|
16
|
+
const { parent, transitionDuration = 300, mountTimeout = 5000, onLoadStart, onLoadComplete, onError, onUnload, } = config;
|
|
17
|
+
// Create wrapper and containers
|
|
18
|
+
const wrapper = document.createElement('div');
|
|
19
|
+
wrapper.className = 'dynim-preview-wrapper';
|
|
20
|
+
wrapper.style.cssText = `
|
|
21
|
+
position: relative;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
`;
|
|
26
|
+
const containerA = createContainer('dynim-preview-a');
|
|
27
|
+
const containerB = createContainer('dynim-preview-b');
|
|
28
|
+
// B starts hidden
|
|
29
|
+
containerB.style.opacity = '0';
|
|
30
|
+
containerB.style.pointerEvents = 'none';
|
|
31
|
+
wrapper.appendChild(containerA);
|
|
32
|
+
wrapper.appendChild(containerB);
|
|
33
|
+
parent.appendChild(wrapper);
|
|
34
|
+
// State
|
|
35
|
+
let activeContainer = 'a';
|
|
36
|
+
let loading = false;
|
|
37
|
+
let loaded = false;
|
|
38
|
+
let currentBundleUrl = null;
|
|
39
|
+
// Track cleanup functions per container (NOT global!)
|
|
40
|
+
const containerCleanup = new Map();
|
|
41
|
+
function createContainer(id) {
|
|
42
|
+
const container = document.createElement('div');
|
|
43
|
+
container.id = id;
|
|
44
|
+
container.className = 'dynim-preview-container';
|
|
45
|
+
container.style.cssText = `
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: 0;
|
|
48
|
+
transition: opacity ${transitionDuration}ms ease;
|
|
49
|
+
overflow: auto;
|
|
50
|
+
`;
|
|
51
|
+
return container;
|
|
52
|
+
}
|
|
53
|
+
function getActive() {
|
|
54
|
+
return activeContainer === 'a' ? containerA : containerB;
|
|
55
|
+
}
|
|
56
|
+
function getInactive() {
|
|
57
|
+
return activeContainer === 'a' ? containerB : containerA;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Cleanup a container - call bundle cleanup and clear DOM
|
|
61
|
+
*/
|
|
62
|
+
function cleanupContainer(container) {
|
|
63
|
+
// Call THIS container's cleanup function (not the global one!)
|
|
64
|
+
const cleanup = containerCleanup.get(container);
|
|
65
|
+
if (typeof cleanup === 'function') {
|
|
66
|
+
try {
|
|
67
|
+
cleanup();
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
console.warn('[BundleManager] Cleanup error:', e);
|
|
71
|
+
}
|
|
72
|
+
containerCleanup.delete(container);
|
|
73
|
+
}
|
|
74
|
+
// Remove any bundle scripts from this container
|
|
75
|
+
container.querySelectorAll('script[data-dynim-bundle]').forEach(s => s.remove());
|
|
76
|
+
// Remove any styles injected by bundles (these are document-level)
|
|
77
|
+
document.querySelectorAll('style[data-dynim-bundle-styles]').forEach(s => s.remove());
|
|
78
|
+
// Clear container
|
|
79
|
+
container.innerHTML = '';
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Preload bundle code (fetch without executing)
|
|
83
|
+
*/
|
|
84
|
+
async function preloadBundle(url) {
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
credentials: 'include',
|
|
87
|
+
headers: {
|
|
88
|
+
'Accept': 'application/javascript',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`Failed to fetch bundle: ${response.status} ${response.statusText}`);
|
|
93
|
+
}
|
|
94
|
+
return response.text();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Execute bundle code in a container
|
|
98
|
+
*/
|
|
99
|
+
function executeBundle(container, code) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
// Create root element for React
|
|
102
|
+
const root = document.createElement('div');
|
|
103
|
+
root.id = 'root';
|
|
104
|
+
container.appendChild(root);
|
|
105
|
+
// Set up mount detection
|
|
106
|
+
let resolved = false;
|
|
107
|
+
const timeout = setTimeout(() => {
|
|
108
|
+
if (!resolved) {
|
|
109
|
+
resolved = true;
|
|
110
|
+
console.warn('[BundleManager] Mount timeout - assuming bundle mounted');
|
|
111
|
+
// Capture cleanup function for THIS container
|
|
112
|
+
captureCleanup(container);
|
|
113
|
+
resolve();
|
|
114
|
+
}
|
|
115
|
+
}, mountTimeout);
|
|
116
|
+
// Bundle can signal when mounted
|
|
117
|
+
const bw = window;
|
|
118
|
+
bw.__BUNDLE_MOUNTED__ = () => {
|
|
119
|
+
if (!resolved) {
|
|
120
|
+
resolved = true;
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
delete bw.__BUNDLE_MOUNTED__;
|
|
123
|
+
// Capture cleanup function for THIS container
|
|
124
|
+
captureCleanup(container);
|
|
125
|
+
resolve();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// Create and execute script
|
|
129
|
+
const script = document.createElement('script');
|
|
130
|
+
script.setAttribute('data-dynim-bundle', 'true');
|
|
131
|
+
script.textContent = code;
|
|
132
|
+
script.onerror = (e) => {
|
|
133
|
+
if (!resolved) {
|
|
134
|
+
resolved = true;
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
reject(new Error('Script execution failed'));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
container.appendChild(script);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Capture the global cleanup function and store it for a specific container
|
|
144
|
+
*/
|
|
145
|
+
function captureCleanup(container) {
|
|
146
|
+
// Store the global cleanup for this specific container
|
|
147
|
+
const bw = window;
|
|
148
|
+
if (typeof bw.__BUNDLE_CLEANUP__ === 'function') {
|
|
149
|
+
containerCleanup.set(container, bw.__BUNDLE_CLEANUP__);
|
|
150
|
+
// Clear global to prevent accidental calls
|
|
151
|
+
delete bw.__BUNDLE_CLEANUP__;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Crossfade from active to inactive container
|
|
156
|
+
*/
|
|
157
|
+
function crossfade() {
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
const current = getActive();
|
|
160
|
+
const next = getInactive();
|
|
161
|
+
// Start transition
|
|
162
|
+
next.style.opacity = '1';
|
|
163
|
+
next.style.pointerEvents = 'auto';
|
|
164
|
+
current.style.opacity = '0';
|
|
165
|
+
current.style.pointerEvents = 'none';
|
|
166
|
+
// Wait for transition
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
// Swap active
|
|
169
|
+
activeContainer = activeContainer === 'a' ? 'b' : 'a';
|
|
170
|
+
// Cleanup old container (now inactive)
|
|
171
|
+
cleanupContainer(current);
|
|
172
|
+
resolve();
|
|
173
|
+
}, transitionDuration);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Load a bundle with crossfade
|
|
178
|
+
*/
|
|
179
|
+
async function load(bundleUrl) {
|
|
180
|
+
if (loading) {
|
|
181
|
+
console.warn('[BundleManager] Already loading, ignoring request');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Skip if same bundle already loaded
|
|
185
|
+
if (bundleUrl === currentBundleUrl && loaded) {
|
|
186
|
+
console.log('[BundleManager] Same bundle already loaded, skipping');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
loading = true;
|
|
190
|
+
onLoadStart?.(bundleUrl);
|
|
191
|
+
try {
|
|
192
|
+
// 1. Preload bundle code
|
|
193
|
+
console.log('[BundleManager] Preloading:', bundleUrl);
|
|
194
|
+
const code = await preloadBundle(bundleUrl);
|
|
195
|
+
// 2. Prepare inactive container
|
|
196
|
+
const inactive = getInactive();
|
|
197
|
+
cleanupContainer(inactive);
|
|
198
|
+
// 3. Execute bundle in inactive container
|
|
199
|
+
console.log('[BundleManager] Executing bundle');
|
|
200
|
+
await executeBundle(inactive, code);
|
|
201
|
+
// 4. Crossfade to new container
|
|
202
|
+
console.log('[BundleManager] Crossfading');
|
|
203
|
+
await crossfade();
|
|
204
|
+
// 5. Update state
|
|
205
|
+
currentBundleUrl = bundleUrl;
|
|
206
|
+
loaded = true;
|
|
207
|
+
console.log('[BundleManager] Load complete');
|
|
208
|
+
onLoadComplete?.(bundleUrl);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error('[BundleManager] Load failed:', error);
|
|
212
|
+
onError?.(error, bundleUrl);
|
|
213
|
+
// Cleanup failed attempt
|
|
214
|
+
const inactive = getInactive();
|
|
215
|
+
cleanupContainer(inactive);
|
|
216
|
+
inactive.style.opacity = '0';
|
|
217
|
+
inactive.style.pointerEvents = 'none';
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
loading = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Apply inline styles to an element
|
|
226
|
+
*/
|
|
227
|
+
function applyStyles(selector, styles) {
|
|
228
|
+
const el = getActive().querySelector(selector);
|
|
229
|
+
if (el) {
|
|
230
|
+
Object.assign(el.style, styles);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.warn(`[BundleManager] Element not found: ${selector}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Replace className on an element
|
|
238
|
+
*/
|
|
239
|
+
function applyClassName(selector, className) {
|
|
240
|
+
const el = getActive().querySelector(selector);
|
|
241
|
+
if (el) {
|
|
242
|
+
el.className = className;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
console.warn(`[BundleManager] Element not found: ${selector}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Add/remove CSS classes (useful for Tailwind)
|
|
250
|
+
*/
|
|
251
|
+
function toggleClasses(selector, add, remove) {
|
|
252
|
+
const el = getActive().querySelector(selector);
|
|
253
|
+
if (el) {
|
|
254
|
+
remove.forEach(cls => {
|
|
255
|
+
if (cls)
|
|
256
|
+
el.classList.remove(cls);
|
|
257
|
+
});
|
|
258
|
+
add.forEach(cls => {
|
|
259
|
+
if (cls)
|
|
260
|
+
el.classList.add(cls);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.warn(`[BundleManager] Element not found: ${selector}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Query an element in the active container
|
|
269
|
+
*/
|
|
270
|
+
function querySelector(selector) {
|
|
271
|
+
return getActive().querySelector(selector);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Query all elements in the active container
|
|
275
|
+
*/
|
|
276
|
+
function querySelectorAll(selector) {
|
|
277
|
+
return getActive().querySelectorAll(selector);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get the active container
|
|
281
|
+
*/
|
|
282
|
+
function getActiveContainer() {
|
|
283
|
+
return getActive();
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get the root element of the active container
|
|
287
|
+
*/
|
|
288
|
+
function getActiveRoot() {
|
|
289
|
+
return getActive().querySelector('#root');
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Check if a bundle is loaded
|
|
293
|
+
*/
|
|
294
|
+
function isLoaded() {
|
|
295
|
+
return loaded;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Check if currently loading
|
|
299
|
+
*/
|
|
300
|
+
function isLoading() {
|
|
301
|
+
return loading;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Get current bundle URL
|
|
305
|
+
*/
|
|
306
|
+
function getCurrentBundleUrl() {
|
|
307
|
+
return currentBundleUrl;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Unload current bundle
|
|
311
|
+
*/
|
|
312
|
+
function unload() {
|
|
313
|
+
cleanupContainer(containerA);
|
|
314
|
+
cleanupContainer(containerB);
|
|
315
|
+
// Reset state
|
|
316
|
+
containerA.style.opacity = '1';
|
|
317
|
+
containerA.style.pointerEvents = 'auto';
|
|
318
|
+
containerB.style.opacity = '0';
|
|
319
|
+
containerB.style.pointerEvents = 'none';
|
|
320
|
+
activeContainer = 'a';
|
|
321
|
+
loaded = false;
|
|
322
|
+
currentBundleUrl = null;
|
|
323
|
+
onUnload?.();
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Destroy the bundle manager
|
|
327
|
+
*/
|
|
328
|
+
function destroy() {
|
|
329
|
+
unload();
|
|
330
|
+
wrapper.remove();
|
|
331
|
+
}
|
|
332
|
+
// Expose for debugging
|
|
333
|
+
if (typeof window !== 'undefined') {
|
|
334
|
+
window.__bundleManager = {
|
|
335
|
+
getActive,
|
|
336
|
+
getInactive,
|
|
337
|
+
isLoaded,
|
|
338
|
+
isLoading,
|
|
339
|
+
getCurrentBundleUrl,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
load,
|
|
344
|
+
applyStyles,
|
|
345
|
+
applyClassName,
|
|
346
|
+
toggleClasses,
|
|
347
|
+
querySelector,
|
|
348
|
+
querySelectorAll,
|
|
349
|
+
getActiveContainer,
|
|
350
|
+
getActiveRoot,
|
|
351
|
+
isLoaded,
|
|
352
|
+
isLoading,
|
|
353
|
+
getCurrentBundleUrl,
|
|
354
|
+
unload,
|
|
355
|
+
destroy,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class name analyzer - analyzes CSS class names for component patterns
|
|
3
|
+
*/
|
|
4
|
+
import type { Signal, ClassifierInput } from './semantic-analyzer';
|
|
5
|
+
export declare function analyzeClassNames(input: ClassifierInput): Signal[];
|
|
6
|
+
//# sourceMappingURL=classname-analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classname-analyzer.d.ts","sourceRoot":"","sources":["../../../src/builder/classifier/classname-analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAqEnE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,EAAE,CA4ClE"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class name analyzer - analyzes CSS class names for component patterns
|
|
3
|
+
*/
|
|
4
|
+
const BEM_BLOCK_PATTERN = /^([a-z][a-z0-9]*(-[a-z0-9]+)*)$/i;
|
|
5
|
+
const COMPONENT_PATTERNS = [
|
|
6
|
+
{ pattern: /\bcard\b/i, indicator: 'card-class', weight: 0.8 },
|
|
7
|
+
{ pattern: /\bpanel\b/i, indicator: 'card-class', weight: 0.6 },
|
|
8
|
+
{ pattern: /\bbox\b/i, indicator: 'card-class', weight: 0.4 },
|
|
9
|
+
{ pattern: /\btile\b/i, indicator: 'card-class', weight: 0.5 },
|
|
10
|
+
{ pattern: /\bnav(bar|igation)?\b/i, indicator: 'nav-class', weight: 0.85 },
|
|
11
|
+
{ pattern: /\bmenu\b/i, indicator: 'nav-class', weight: 0.6 },
|
|
12
|
+
{ pattern: /\bbreadcrumb/i, indicator: 'nav-class', weight: 0.7 },
|
|
13
|
+
{ pattern: /\btabs?\b/i, indicator: 'nav-class', weight: 0.6 },
|
|
14
|
+
{ pattern: /\bbtn\b/i, indicator: 'btn-class', weight: 0.9 },
|
|
15
|
+
{ pattern: /\bbutton\b/i, indicator: 'btn-class', weight: 0.85 },
|
|
16
|
+
{ pattern: /\bcta\b/i, indicator: 'btn-class', weight: 0.7 },
|
|
17
|
+
{ pattern: /\bmodal\b/i, indicator: 'modal-class', weight: 0.9 },
|
|
18
|
+
{ pattern: /\bdialog\b/i, indicator: 'modal-class', weight: 0.85 },
|
|
19
|
+
{ pattern: /\bpopup\b/i, indicator: 'modal-class', weight: 0.7 },
|
|
20
|
+
{ pattern: /\boverlay\b/i, indicator: 'overlay-class', weight: 0.5 },
|
|
21
|
+
{ pattern: /\bdropdown\b/i, indicator: 'dropdown-class', weight: 0.75 },
|
|
22
|
+
{ pattern: /\bsidebar\b/i, indicator: 'sidebar-class', weight: 0.9 },
|
|
23
|
+
{ pattern: /\bside-nav/i, indicator: 'sidebar-class', weight: 0.85 },
|
|
24
|
+
{ pattern: /\bdrawer\b/i, indicator: 'sidebar-class', weight: 0.7 },
|
|
25
|
+
{ pattern: /\bheader\b/i, indicator: 'header-class', weight: 0.7 },
|
|
26
|
+
{ pattern: /\bfooter\b/i, indicator: 'footer-class', weight: 0.7 },
|
|
27
|
+
{ pattern: /\bhero\b/i, indicator: 'hero-class', weight: 0.75 },
|
|
28
|
+
{ pattern: /\bbanner\b/i, indicator: 'banner-class', weight: 0.65 },
|
|
29
|
+
{ pattern: /\bform(-group|-control|-field)?\b/i, indicator: 'form-class', weight: 0.7 },
|
|
30
|
+
{ pattern: /\binput(-group|-wrapper)?\b/i, indicator: 'input-class', weight: 0.6 },
|
|
31
|
+
{ pattern: /\bfield\b/i, indicator: 'form-class', weight: 0.5 },
|
|
32
|
+
{ pattern: /\blist(-item|-group)?\b/i, indicator: 'list-class', weight: 0.6 },
|
|
33
|
+
{ pattern: /\bgrid\b/i, indicator: 'grid-class', weight: 0.5 },
|
|
34
|
+
{ pattern: /\brow\b/i, indicator: 'grid-class', weight: 0.4 },
|
|
35
|
+
{ pattern: /\bcol(umn)?(-\d+)?\b/i, indicator: 'grid-class', weight: 0.4 },
|
|
36
|
+
{ pattern: /\bcontainer\b/i, indicator: 'container-class', weight: 0.5 },
|
|
37
|
+
{ pattern: /\bwrapper\b/i, indicator: 'container-class', weight: 0.4 },
|
|
38
|
+
{ pattern: /\blayout\b/i, indicator: 'container-class', weight: 0.5 },
|
|
39
|
+
{ pattern: /\bsection\b/i, indicator: 'section-class', weight: 0.5 },
|
|
40
|
+
{ pattern: /\bavatar\b/i, indicator: 'avatar-class', weight: 0.8 },
|
|
41
|
+
{ pattern: /\bbadge\b/i, indicator: 'badge-class', weight: 0.75 },
|
|
42
|
+
{ pattern: /\bchip\b/i, indicator: 'badge-class', weight: 0.7 },
|
|
43
|
+
{ pattern: /\btag\b/i, indicator: 'badge-class', weight: 0.6 },
|
|
44
|
+
{ pattern: /\balert\b/i, indicator: 'alert-class', weight: 0.8 },
|
|
45
|
+
{ pattern: /\btoast\b/i, indicator: 'toast-class', weight: 0.8 },
|
|
46
|
+
{ pattern: /\bnotification\b/i, indicator: 'alert-class', weight: 0.75 },
|
|
47
|
+
{ pattern: /\bspinner\b/i, indicator: 'spinner-class', weight: 0.8 },
|
|
48
|
+
{ pattern: /\bloading\b/i, indicator: 'spinner-class', weight: 0.6 },
|
|
49
|
+
{ pattern: /\bskeleton\b/i, indicator: 'skeleton-class', weight: 0.75 },
|
|
50
|
+
{ pattern: /\bicon\b/i, indicator: 'icon-class', weight: 0.7 },
|
|
51
|
+
{ pattern: /\bfa-/i, indicator: 'icon-class', weight: 0.8 },
|
|
52
|
+
{ pattern: /\bmaterial-icons\b/i, indicator: 'icon-class', weight: 0.8 }
|
|
53
|
+
];
|
|
54
|
+
const TAILWIND_PATTERNS = [
|
|
55
|
+
{ pattern: /\bflex\b/, indicator: 'flex-display', weight: 0.3 },
|
|
56
|
+
{ pattern: /\binline-flex\b/, indicator: 'flex-display', weight: 0.25 },
|
|
57
|
+
{ pattern: /\bgrid\b/, indicator: 'grid-display', weight: 0.3 },
|
|
58
|
+
{ pattern: /\brounded(-\w+)?\b/, indicator: 'has-border-radius-tw', weight: 0.2 },
|
|
59
|
+
{ pattern: /\bshadow(-\w+)?\b/, indicator: 'has-shadow-tw', weight: 0.3 },
|
|
60
|
+
{ pattern: /\bbg-\w+/, indicator: 'has-background-tw', weight: 0.2 },
|
|
61
|
+
{ pattern: /\bp-\d+|\bpx-\d+|\bpy-\d+|\bpt-\d+|\bpb-\d+|\bpl-\d+|\bpr-\d+/, indicator: 'has-padding-tw', weight: 0.15 },
|
|
62
|
+
{ pattern: /\bborder\b|\bborder-\w+/, indicator: 'has-border-tw', weight: 0.15 },
|
|
63
|
+
{ pattern: /\boverflow-hidden\b/, indicator: 'overflow-hidden-tw', weight: 0.1 },
|
|
64
|
+
{ pattern: /\brelative\b/, indicator: 'relative-position-tw', weight: 0.1 },
|
|
65
|
+
{ pattern: /\babsolute\b/, indicator: 'absolute-position-tw', weight: 0.2 },
|
|
66
|
+
{ pattern: /\bfixed\b/, indicator: 'fixed-position-tw', weight: 0.3 }
|
|
67
|
+
];
|
|
68
|
+
export function analyzeClassNames(input) {
|
|
69
|
+
const signals = [];
|
|
70
|
+
const classes = (input.className || '').split(/\s+/).filter(Boolean);
|
|
71
|
+
for (const className of classes) {
|
|
72
|
+
for (const { pattern, indicator, weight } of COMPONENT_PATTERNS) {
|
|
73
|
+
if (pattern.test(className)) {
|
|
74
|
+
signals.push({
|
|
75
|
+
source: 'className',
|
|
76
|
+
indicator,
|
|
77
|
+
weight
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const { pattern, indicator, weight } of TAILWIND_PATTERNS) {
|
|
82
|
+
if (pattern.test(className)) {
|
|
83
|
+
signals.push({
|
|
84
|
+
source: 'className',
|
|
85
|
+
indicator,
|
|
86
|
+
weight
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (BEM_BLOCK_PATTERN.test(className) && className.length > 3 && !className.startsWith('is-') && !className.startsWith('has-')) {
|
|
91
|
+
const weight = Math.min(0.3 + (className.length - 3) * 0.03, 0.5);
|
|
92
|
+
signals.push({
|
|
93
|
+
source: 'className',
|
|
94
|
+
indicator: 'bem-block',
|
|
95
|
+
weight
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const uniqueSignals = new Map();
|
|
100
|
+
for (const signal of signals) {
|
|
101
|
+
const existing = uniqueSignals.get(signal.indicator);
|
|
102
|
+
if (!existing || existing.weight < signal.weight) {
|
|
103
|
+
uniqueSignals.set(signal.indicator, signal);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return Array.from(uniqueSignals.values());
|
|
107
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element classifier - combines all analyzers to classify DOM elements
|
|
3
|
+
*/
|
|
4
|
+
import { type Signal, type ClassifierInput } from './semantic-analyzer';
|
|
5
|
+
import { setViewportSize } from './size-analyzer';
|
|
6
|
+
export type { Signal, ClassifierInput };
|
|
7
|
+
export interface Classification {
|
|
8
|
+
type: string;
|
|
9
|
+
confidence: number;
|
|
10
|
+
signals: Signal[];
|
|
11
|
+
isComponentBoundary: boolean;
|
|
12
|
+
suggestedName: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Classifier {
|
|
15
|
+
setViewportSize: (width: number, height: number) => void;
|
|
16
|
+
classify: (input: ClassifierInput) => Classification;
|
|
17
|
+
}
|
|
18
|
+
export declare function createClassifier(): Classifier;
|
|
19
|
+
export { setViewportSize };
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/builder/classifier/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,KAAK,MAAM,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG1F,OAAO,EAAe,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE/D,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,cAAc,CAAC;CACtD;AAqFD,wBAAgB,gBAAgB,IAAI,UAAU,CAK7C;AAuHD,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element classifier - combines all analyzers to classify DOM elements
|
|
3
|
+
*/
|
|
4
|
+
import { analyzeSemantics } from './semantic-analyzer';
|
|
5
|
+
import { analyzeClassNames } from './classname-analyzer';
|
|
6
|
+
import { analyzeVisual } from './visual-analyzer';
|
|
7
|
+
import { analyzeSize, setViewportSize } from './size-analyzer';
|
|
8
|
+
const COMPONENT_TYPES = [
|
|
9
|
+
'container', 'navigation', 'header', 'footer', 'article', 'card',
|
|
10
|
+
'button', 'form', 'input', 'list', 'list-item', 'image', 'text',
|
|
11
|
+
'link', 'modal', 'sidebar', 'grid', 'flex-container', 'unknown'
|
|
12
|
+
];
|
|
13
|
+
const SIGNAL_TYPE_MAP = {
|
|
14
|
+
'nav-tag': ['navigation'],
|
|
15
|
+
'nav-role': ['navigation'],
|
|
16
|
+
'header-tag': ['header'],
|
|
17
|
+
'header-role': ['header'],
|
|
18
|
+
'footer-tag': ['footer'],
|
|
19
|
+
'footer-role': ['footer'],
|
|
20
|
+
'article-tag': ['article'],
|
|
21
|
+
'section-tag': ['container'],
|
|
22
|
+
'container-tag': ['container'],
|
|
23
|
+
'button-tag': ['button'],
|
|
24
|
+
'button-role': ['button'],
|
|
25
|
+
'form-tag': ['form'],
|
|
26
|
+
'input-tag': ['input'],
|
|
27
|
+
'list-tag': ['list'],
|
|
28
|
+
'list-item-tag': ['list-item'],
|
|
29
|
+
'img-tag': ['image'],
|
|
30
|
+
'media-tag': ['image'],
|
|
31
|
+
'svg-tag': ['image'],
|
|
32
|
+
'link-tag': ['link'],
|
|
33
|
+
'text-tag': ['text'],
|
|
34
|
+
'heading-tag': ['text'],
|
|
35
|
+
'modal-tag': ['modal'],
|
|
36
|
+
'modal-role': ['modal'],
|
|
37
|
+
'sidebar-tag': ['sidebar'],
|
|
38
|
+
'card-class': ['card'],
|
|
39
|
+
'nav-class': ['navigation'],
|
|
40
|
+
'btn-class': ['button'],
|
|
41
|
+
'modal-class': ['modal'],
|
|
42
|
+
'sidebar-class': ['sidebar'],
|
|
43
|
+
'header-class': ['header'],
|
|
44
|
+
'footer-class': ['footer'],
|
|
45
|
+
'hero-class': ['header', 'container'],
|
|
46
|
+
'banner-class': ['header'],
|
|
47
|
+
'form-class': ['form'],
|
|
48
|
+
'input-class': ['input'],
|
|
49
|
+
'list-class': ['list'],
|
|
50
|
+
'grid-class': ['grid'],
|
|
51
|
+
'container-class': ['container'],
|
|
52
|
+
'section-class': ['container'],
|
|
53
|
+
'avatar-class': ['image'],
|
|
54
|
+
'badge-class': ['text'],
|
|
55
|
+
'alert-class': ['container'],
|
|
56
|
+
'toast-class': ['modal'],
|
|
57
|
+
'dropdown-class': ['modal', 'navigation'],
|
|
58
|
+
'icon-class': ['image'],
|
|
59
|
+
'overlay-class': ['modal'],
|
|
60
|
+
'flex-display': ['flex-container'],
|
|
61
|
+
'grid-display': ['grid'],
|
|
62
|
+
'has-shadow': ['card', 'modal', 'button'],
|
|
63
|
+
'has-border-radius': ['card', 'button'],
|
|
64
|
+
'has-border': ['card', 'input', 'container'],
|
|
65
|
+
'has-distinct-background': ['card', 'container', 'button'],
|
|
66
|
+
'has-significant-padding': ['container', 'card'],
|
|
67
|
+
'fixed-position': ['modal', 'navigation', 'header'],
|
|
68
|
+
'sticky-position': ['header', 'navigation'],
|
|
69
|
+
'high-z-index': ['modal'],
|
|
70
|
+
'pointer-cursor': ['button', 'link'],
|
|
71
|
+
'scrollable': ['container', 'list'],
|
|
72
|
+
'button-size': ['button'],
|
|
73
|
+
'icon-size': ['image'],
|
|
74
|
+
'avatar-size': ['image'],
|
|
75
|
+
'card-size': ['card'],
|
|
76
|
+
'banner-size': ['header', 'container'],
|
|
77
|
+
'full-width': ['container', 'header', 'footer'],
|
|
78
|
+
'sidebar-size': ['sidebar'],
|
|
79
|
+
'header-footer-size': ['header', 'footer'],
|
|
80
|
+
'modal-size': ['modal'],
|
|
81
|
+
'container-size': ['container'],
|
|
82
|
+
'input-size': ['input'],
|
|
83
|
+
'inline-size': ['text', 'link']
|
|
84
|
+
};
|
|
85
|
+
const STRONG_BOUNDARY_TYPES = [
|
|
86
|
+
'navigation', 'header', 'footer', 'card', 'modal', 'sidebar', 'article', 'form', 'button'
|
|
87
|
+
];
|
|
88
|
+
export function createClassifier() {
|
|
89
|
+
return {
|
|
90
|
+
setViewportSize,
|
|
91
|
+
classify
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function classify(input) {
|
|
95
|
+
const signals = [];
|
|
96
|
+
const semanticSignals = analyzeSemantics(input);
|
|
97
|
+
const classNameSignals = analyzeClassNames(input);
|
|
98
|
+
const visualSignals = analyzeVisual(input);
|
|
99
|
+
const sizeSignals = analyzeSize(input);
|
|
100
|
+
signals.push(...semanticSignals, ...classNameSignals, ...visualSignals, ...sizeSignals);
|
|
101
|
+
const typeScores = calculateTypeScores(signals);
|
|
102
|
+
let bestType = 'unknown';
|
|
103
|
+
let bestScore = 0;
|
|
104
|
+
for (const [type, score] of Object.entries(typeScores)) {
|
|
105
|
+
if (score > bestScore) {
|
|
106
|
+
bestScore = score;
|
|
107
|
+
bestType = type;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const totalPositiveWeight = signals.filter(s => s.weight > 0).reduce((sum, s) => sum + s.weight, 0);
|
|
111
|
+
const confidence = totalPositiveWeight > 0 ? Math.min(bestScore / Math.max(totalPositiveWeight, 1), 1) : 0;
|
|
112
|
+
const isComponentBoundary = checkIsComponentBoundary(input, signals, bestType, bestScore);
|
|
113
|
+
const suggestedName = generateSuggestedName(input, bestType);
|
|
114
|
+
return {
|
|
115
|
+
type: bestType,
|
|
116
|
+
confidence,
|
|
117
|
+
signals,
|
|
118
|
+
isComponentBoundary,
|
|
119
|
+
suggestedName
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function calculateTypeScores(signals) {
|
|
123
|
+
const scores = {};
|
|
124
|
+
for (const type of COMPONENT_TYPES) {
|
|
125
|
+
scores[type] = 0;
|
|
126
|
+
}
|
|
127
|
+
for (const signal of signals) {
|
|
128
|
+
const types = SIGNAL_TYPE_MAP[signal.indicator];
|
|
129
|
+
if (types) {
|
|
130
|
+
for (const type of types) {
|
|
131
|
+
scores[type] += signal.weight;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return scores;
|
|
136
|
+
}
|
|
137
|
+
function checkIsComponentBoundary(input, signals, type, score) {
|
|
138
|
+
if (STRONG_BOUNDARY_TYPES.includes(type) && score > 0.5) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
const hasVisualIsolation = signals.some(s => s.source === 'visual' &&
|
|
142
|
+
['has-shadow', 'has-distinct-background', 'has-border'].includes(s.indicator) &&
|
|
143
|
+
s.weight > 0.2);
|
|
144
|
+
const hasSpacing = signals.some(s => s.source === 'visual' &&
|
|
145
|
+
s.indicator === 'has-significant-padding' &&
|
|
146
|
+
s.weight > 0.2);
|
|
147
|
+
const hasComponentClass = signals.some(s => s.source === 'className' && s.weight >= 0.5);
|
|
148
|
+
const hasSemanticSignificance = signals.some(s => s.source === 'semantic' && s.weight >= 0.6);
|
|
149
|
+
const strongSignalCount = [hasVisualIsolation, hasSpacing, hasComponentClass, hasSemanticSignificance].filter(Boolean).length;
|
|
150
|
+
return strongSignalCount >= 2 || (strongSignalCount >= 1 && score > 0.6);
|
|
151
|
+
}
|
|
152
|
+
function generateSuggestedName(input, type) {
|
|
153
|
+
if (input.id) {
|
|
154
|
+
return formatName(input.id);
|
|
155
|
+
}
|
|
156
|
+
const classes = (input.className || '').split(/\s+/).filter(Boolean);
|
|
157
|
+
const meaningfulClass = classes.find(c => {
|
|
158
|
+
if (/^(flex|grid|p-|m-|w-|h-|bg-|text-|border-|rounded-|shadow-|overflow-|relative|absolute|fixed|hidden|block|inline)/.test(c)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (c.includes('--') || c.startsWith('is-') || c.startsWith('has-')) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return c.length > 2 && c.length < 30;
|
|
165
|
+
});
|
|
166
|
+
if (meaningfulClass) {
|
|
167
|
+
return formatName(meaningfulClass.split('__')[0]);
|
|
168
|
+
}
|
|
169
|
+
const typeName = type === 'unknown' ? '' : `${type} `;
|
|
170
|
+
return `${typeName}${input.tagName.toLowerCase()}`.trim();
|
|
171
|
+
}
|
|
172
|
+
function formatName(name) {
|
|
173
|
+
return name
|
|
174
|
+
.replace(/[-_]/g, ' ')
|
|
175
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
176
|
+
.split(' ')
|
|
177
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
178
|
+
.join(' ')
|
|
179
|
+
.trim();
|
|
180
|
+
}
|
|
181
|
+
export { setViewportSize };
|