editor-ts 0.0.1 → 0.0.12
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 +197 -318
- package/index.ts +70 -0
- package/package.json +36 -11
- package/src/core/ComponentManager.ts +697 -6
- package/src/core/ComponentPalette.ts +109 -0
- package/src/core/CustomComponentRegistry.ts +74 -0
- package/src/core/KeyboardShortcuts.ts +220 -0
- package/src/core/LayerManager.ts +378 -0
- package/src/core/Page.ts +24 -5
- package/src/core/StorageManager.ts +447 -0
- package/src/core/StyleManager.ts +38 -2
- package/src/core/VersionControl.ts +189 -0
- package/src/core/aiChat.ts +427 -0
- package/src/core/iframeCanvas.ts +672 -0
- package/src/core/init.ts +3081 -248
- package/src/server/bun_server.ts +155 -0
- package/src/server/cf_worker.ts +225 -0
- package/src/server/schema.ts +21 -0
- package/src/server/sync.ts +195 -0
- package/src/types/sqlocal.d.ts +6 -0
- package/src/types.ts +591 -18
- package/src/utils/toolbar.ts +15 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { Component } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface LayerManagerConfig {
|
|
4
|
+
container: HTMLElement;
|
|
5
|
+
onSelect?: (component: Component) => void;
|
|
6
|
+
onReorder?: (componentId: string, newParentId: string | null, newIndex: number) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* LayerManager - Renders a Figma/Photoshop-style layer panel
|
|
11
|
+
* showing component hierarchy with drag-and-drop reordering
|
|
12
|
+
*/
|
|
13
|
+
export class LayerManager {
|
|
14
|
+
private container: HTMLElement;
|
|
15
|
+
private components: Component[] = [];
|
|
16
|
+
private selectedId: string | null = null;
|
|
17
|
+
private onSelect?: (component: Component) => void;
|
|
18
|
+
private onReorder?: (componentId: string, newParentId: string | null, newIndex: number) => void;
|
|
19
|
+
private draggedElement: HTMLElement | null = null;
|
|
20
|
+
private draggedId: string | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(config: LayerManagerConfig) {
|
|
23
|
+
this.container = config.container;
|
|
24
|
+
this.onSelect = config.onSelect;
|
|
25
|
+
this.onReorder = config.onReorder;
|
|
26
|
+
this.injectStyles();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Inject layer panel styles into the document
|
|
31
|
+
*/
|
|
32
|
+
private injectStyles(): void {
|
|
33
|
+
if (document.getElementById('editorts-layer-styles')) return;
|
|
34
|
+
|
|
35
|
+
const style = document.createElement('style');
|
|
36
|
+
style.id = 'editorts-layer-styles';
|
|
37
|
+
style.textContent = `
|
|
38
|
+
.editorts-layer-panel {
|
|
39
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
40
|
+
font-size: 13px;
|
|
41
|
+
user-select: none;
|
|
42
|
+
}
|
|
43
|
+
.editorts-layer-item {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
padding: 6px 8px;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
margin: 2px 0;
|
|
50
|
+
transition: background-color 0.15s;
|
|
51
|
+
}
|
|
52
|
+
.editorts-layer-item:hover {
|
|
53
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
54
|
+
}
|
|
55
|
+
.editorts-layer-item.selected {
|
|
56
|
+
background-color: rgba(59, 130, 246, 0.15);
|
|
57
|
+
outline: 1px solid rgba(59, 130, 246, 0.3);
|
|
58
|
+
}
|
|
59
|
+
.editorts-layer-item.drag-over {
|
|
60
|
+
background-color: rgba(16, 185, 129, 0.15);
|
|
61
|
+
outline: 2px dashed #10b981;
|
|
62
|
+
}
|
|
63
|
+
.editorts-layer-item.dragging {
|
|
64
|
+
opacity: 0.5;
|
|
65
|
+
}
|
|
66
|
+
.editorts-layer-toggle {
|
|
67
|
+
width: 16px;
|
|
68
|
+
height: 16px;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
margin-right: 4px;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
color: #666;
|
|
75
|
+
font-size: 10px;
|
|
76
|
+
}
|
|
77
|
+
.editorts-layer-toggle:hover {
|
|
78
|
+
color: #333;
|
|
79
|
+
}
|
|
80
|
+
.editorts-layer-icon {
|
|
81
|
+
width: 16px;
|
|
82
|
+
height: 16px;
|
|
83
|
+
margin-right: 6px;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
font-size: 12px;
|
|
88
|
+
}
|
|
89
|
+
.editorts-layer-name {
|
|
90
|
+
flex: 1;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
text-overflow: ellipsis;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
color: #333;
|
|
95
|
+
}
|
|
96
|
+
.editorts-layer-type {
|
|
97
|
+
font-size: 11px;
|
|
98
|
+
color: #888;
|
|
99
|
+
margin-left: 8px;
|
|
100
|
+
}
|
|
101
|
+
.editorts-layer-children {
|
|
102
|
+
margin-left: 16px;
|
|
103
|
+
border-left: 1px solid #e5e7eb;
|
|
104
|
+
padding-left: 4px;
|
|
105
|
+
}
|
|
106
|
+
.editorts-layer-children.collapsed {
|
|
107
|
+
display: none;
|
|
108
|
+
}
|
|
109
|
+
.editorts-layer-empty {
|
|
110
|
+
padding: 12px;
|
|
111
|
+
text-align: center;
|
|
112
|
+
color: #888;
|
|
113
|
+
font-style: italic;
|
|
114
|
+
}
|
|
115
|
+
.editorts-drop-indicator {
|
|
116
|
+
height: 2px;
|
|
117
|
+
background-color: #3b82f6;
|
|
118
|
+
margin: 0 8px;
|
|
119
|
+
border-radius: 1px;
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
document.head.appendChild(style);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Update the layer panel with new components
|
|
127
|
+
*/
|
|
128
|
+
update(components: Component[]): void {
|
|
129
|
+
this.components = components;
|
|
130
|
+
this.render();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Set the selected component (highlight in layer panel)
|
|
135
|
+
*/
|
|
136
|
+
setSelected(id: string | null): void {
|
|
137
|
+
this.selectedId = id;
|
|
138
|
+
|
|
139
|
+
// Update visual selection
|
|
140
|
+
this.container.querySelectorAll('.editorts-layer-item').forEach(el => {
|
|
141
|
+
el.classList.toggle('selected', el.getAttribute('data-id') === id);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Render the layer panel
|
|
147
|
+
*/
|
|
148
|
+
private render(): void {
|
|
149
|
+
this.container.innerHTML = '';
|
|
150
|
+
|
|
151
|
+
const panel = document.createElement('div');
|
|
152
|
+
panel.className = 'editorts-layer-panel';
|
|
153
|
+
|
|
154
|
+
if (this.components.length === 0) {
|
|
155
|
+
panel.innerHTML = '<div class="editorts-layer-empty">No components</div>';
|
|
156
|
+
} else {
|
|
157
|
+
this.components.forEach((component, index) => {
|
|
158
|
+
panel.appendChild(this.renderLayerItem(component, 0, null, index));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.container.appendChild(panel);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Render a single layer item with its children
|
|
167
|
+
*/
|
|
168
|
+
private renderLayerItem(component: Component, depth: number, parentId: string | null, index: number): HTMLElement {
|
|
169
|
+
const wrapper = document.createElement('div');
|
|
170
|
+
wrapper.className = 'editorts-layer-wrapper';
|
|
171
|
+
|
|
172
|
+
const id = component.attributes?.id || `component-${depth}-${index}`;
|
|
173
|
+
const hasChildren = component.components && component.components.length > 0;
|
|
174
|
+
|
|
175
|
+
// Layer item
|
|
176
|
+
const item = document.createElement('div');
|
|
177
|
+
item.className = 'editorts-layer-item';
|
|
178
|
+
item.setAttribute('data-id', id);
|
|
179
|
+
item.setAttribute('data-parent-id', parentId || '');
|
|
180
|
+
item.setAttribute('data-index', String(index));
|
|
181
|
+
item.setAttribute('draggable', 'true');
|
|
182
|
+
|
|
183
|
+
if (id === this.selectedId) {
|
|
184
|
+
item.classList.add('selected');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Toggle for children
|
|
188
|
+
const toggle = document.createElement('span');
|
|
189
|
+
toggle.className = 'editorts-layer-toggle';
|
|
190
|
+
if (hasChildren) {
|
|
191
|
+
toggle.textContent = '▼';
|
|
192
|
+
toggle.addEventListener('click', (e) => {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
const children = wrapper.querySelector('.editorts-layer-children');
|
|
195
|
+
if (children) {
|
|
196
|
+
children.classList.toggle('collapsed');
|
|
197
|
+
toggle.textContent = children.classList.contains('collapsed') ? '▶' : '▼';
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
item.appendChild(toggle);
|
|
202
|
+
|
|
203
|
+
// Icon based on component type/tag
|
|
204
|
+
const icon = document.createElement('span');
|
|
205
|
+
icon.className = 'editorts-layer-icon';
|
|
206
|
+
icon.textContent = this.getIconForComponent(component);
|
|
207
|
+
item.appendChild(icon);
|
|
208
|
+
|
|
209
|
+
// Name (id or type)
|
|
210
|
+
const name = document.createElement('span');
|
|
211
|
+
name.className = 'editorts-layer-name';
|
|
212
|
+
name.textContent = component.attributes?.id || component.tagName || 'Component';
|
|
213
|
+
item.appendChild(name);
|
|
214
|
+
|
|
215
|
+
// Type badge
|
|
216
|
+
const type = document.createElement('span');
|
|
217
|
+
type.className = 'editorts-layer-type';
|
|
218
|
+
type.textContent = component.tagName || component.type || '';
|
|
219
|
+
item.appendChild(type);
|
|
220
|
+
|
|
221
|
+
// Click to select
|
|
222
|
+
item.addEventListener('click', () => {
|
|
223
|
+
this.setSelected(id);
|
|
224
|
+
if (this.onSelect) {
|
|
225
|
+
this.onSelect(component);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Drag and drop
|
|
230
|
+
this.setupDragAndDrop(item, id);
|
|
231
|
+
|
|
232
|
+
wrapper.appendChild(item);
|
|
233
|
+
|
|
234
|
+
// Render children
|
|
235
|
+
if (hasChildren) {
|
|
236
|
+
const childrenContainer = document.createElement('div');
|
|
237
|
+
childrenContainer.className = 'editorts-layer-children';
|
|
238
|
+
|
|
239
|
+
component.components!.forEach((child, childIndex) => {
|
|
240
|
+
childrenContainer.appendChild(this.renderLayerItem(child, depth + 1, id, childIndex));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
wrapper.appendChild(childrenContainer);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return wrapper;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Setup drag and drop for a layer item
|
|
251
|
+
*/
|
|
252
|
+
private setupDragAndDrop(element: HTMLElement, id: string): void {
|
|
253
|
+
element.addEventListener('dragstart', (e) => {
|
|
254
|
+
this.draggedElement = element;
|
|
255
|
+
this.draggedId = id;
|
|
256
|
+
element.classList.add('dragging');
|
|
257
|
+
e.dataTransfer?.setData('text/plain', id);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
element.addEventListener('dragend', () => {
|
|
261
|
+
if (this.draggedElement) {
|
|
262
|
+
this.draggedElement.classList.remove('dragging');
|
|
263
|
+
}
|
|
264
|
+
this.container.querySelectorAll('.drag-over').forEach(el => {
|
|
265
|
+
el.classList.remove('drag-over');
|
|
266
|
+
});
|
|
267
|
+
this.draggedElement = null;
|
|
268
|
+
this.draggedId = null;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
element.addEventListener('dragover', (e) => {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
if (this.draggedId === id) return;
|
|
274
|
+
element.classList.add('drag-over');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
element.addEventListener('dragleave', () => {
|
|
278
|
+
element.classList.remove('drag-over');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
element.addEventListener('drop', (e) => {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
element.classList.remove('drag-over');
|
|
284
|
+
|
|
285
|
+
if (!this.draggedId || this.draggedId === id) return;
|
|
286
|
+
|
|
287
|
+
const targetParentId = element.getAttribute('data-parent-id') || null;
|
|
288
|
+
const targetIndex = parseInt(element.getAttribute('data-index') || '0', 10);
|
|
289
|
+
|
|
290
|
+
if (this.onReorder) {
|
|
291
|
+
this.onReorder(this.draggedId, targetParentId, targetIndex);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get icon for component based on type/tag
|
|
298
|
+
*/
|
|
299
|
+
private getIconForComponent(component: Component): string {
|
|
300
|
+
const tag = component.tagName?.toLowerCase();
|
|
301
|
+
const type = component.type?.toLowerCase();
|
|
302
|
+
|
|
303
|
+
// Check tag first
|
|
304
|
+
if (tag) {
|
|
305
|
+
switch (tag) {
|
|
306
|
+
case 'img': return '🖼️';
|
|
307
|
+
case 'video': return '🎬';
|
|
308
|
+
case 'audio': return '🔊';
|
|
309
|
+
case 'a': return '🔗';
|
|
310
|
+
case 'button': return '🔘';
|
|
311
|
+
case 'input': return '📝';
|
|
312
|
+
case 'form': return '📋';
|
|
313
|
+
case 'table': return '📊';
|
|
314
|
+
case 'ul':
|
|
315
|
+
case 'ol': return '📃';
|
|
316
|
+
case 'h1':
|
|
317
|
+
case 'h2':
|
|
318
|
+
case 'h3':
|
|
319
|
+
case 'h4':
|
|
320
|
+
case 'h5':
|
|
321
|
+
case 'h6': return '📰';
|
|
322
|
+
case 'p':
|
|
323
|
+
case 'span': return '📄';
|
|
324
|
+
case 'section':
|
|
325
|
+
case 'article':
|
|
326
|
+
case 'div': return '📦';
|
|
327
|
+
case 'header': return '⬆️';
|
|
328
|
+
case 'footer': return '⬇️';
|
|
329
|
+
case 'nav': return '🧭';
|
|
330
|
+
default: return '📦';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check type
|
|
335
|
+
if (type) {
|
|
336
|
+
switch (type) {
|
|
337
|
+
case 'image': return '🖼️';
|
|
338
|
+
case 'text': return '📄';
|
|
339
|
+
case 'box': return '📦';
|
|
340
|
+
case 'custom-code': return '📜';
|
|
341
|
+
default: return '📦';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return '📦';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Expand all layers
|
|
350
|
+
*/
|
|
351
|
+
expandAll(): void {
|
|
352
|
+
this.container.querySelectorAll('.editorts-layer-children').forEach(el => {
|
|
353
|
+
el.classList.remove('collapsed');
|
|
354
|
+
});
|
|
355
|
+
this.container.querySelectorAll('.editorts-layer-toggle').forEach(el => {
|
|
356
|
+
if (el.textContent) el.textContent = '▼';
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Collapse all layers
|
|
362
|
+
*/
|
|
363
|
+
collapseAll(): void {
|
|
364
|
+
this.container.querySelectorAll('.editorts-layer-children').forEach(el => {
|
|
365
|
+
el.classList.add('collapsed');
|
|
366
|
+
});
|
|
367
|
+
this.container.querySelectorAll('.editorts-layer-toggle').forEach(el => {
|
|
368
|
+
if (el.textContent) el.textContent = '▶';
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Destroy the layer manager
|
|
374
|
+
*/
|
|
375
|
+
destroy(): void {
|
|
376
|
+
this.container.innerHTML = '';
|
|
377
|
+
}
|
|
378
|
+
}
|
package/src/core/Page.ts
CHANGED
|
@@ -22,7 +22,13 @@ export class Page {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// Initialize managers
|
|
25
|
-
this.components = new ComponentManager(this.data.body
|
|
25
|
+
this.components = new ComponentManager(this.data.body, {
|
|
26
|
+
dom: typeof document !== 'undefined'
|
|
27
|
+
? {
|
|
28
|
+
createTemplate: () => document.createElement('template'),
|
|
29
|
+
}
|
|
30
|
+
: null,
|
|
31
|
+
});
|
|
26
32
|
this.styles = new StyleManager(this.data.body);
|
|
27
33
|
this.assets = new AssetManager(this.data.body);
|
|
28
34
|
this.toolbars = new ToolbarManager();
|
|
@@ -60,7 +66,20 @@ export class Page {
|
|
|
60
66
|
* Get the raw HTML
|
|
61
67
|
*/
|
|
62
68
|
getHTML(): string {
|
|
63
|
-
|
|
69
|
+
// Prefer components when available.
|
|
70
|
+
// (ComponentManager keeps body.html synced, but this avoids stale html.)
|
|
71
|
+
const components = this.components.getAll();
|
|
72
|
+
if (components.length > 0) {
|
|
73
|
+
return `<body>${this.components.toHTML()}</body>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const html = this.data.body.html ?? '';
|
|
77
|
+
|
|
78
|
+
if (/<body[\s>]/i.test(html)) {
|
|
79
|
+
return html;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `<body>${html}</body>`;
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
/**
|
|
@@ -74,7 +93,7 @@ export class Page {
|
|
|
74
93
|
* Get the compiled CSS
|
|
75
94
|
*/
|
|
76
95
|
getCSS(): string {
|
|
77
|
-
return this.data.body.css;
|
|
96
|
+
return this.data.body.css ?? '';
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
/**
|
|
@@ -99,7 +118,7 @@ export class Page {
|
|
|
99
118
|
this.components.sync();
|
|
100
119
|
this.styles.sync();
|
|
101
120
|
this.assets.sync();
|
|
102
|
-
|
|
121
|
+
|
|
103
122
|
return JSON.stringify(this.data, null, 2);
|
|
104
123
|
}
|
|
105
124
|
|
|
@@ -111,7 +130,7 @@ export class Page {
|
|
|
111
130
|
this.components.sync();
|
|
112
131
|
this.styles.sync();
|
|
113
132
|
this.assets.sync();
|
|
114
|
-
|
|
133
|
+
|
|
115
134
|
return this.data;
|
|
116
135
|
}
|
|
117
136
|
|