editor-ts 0.0.10 → 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.
@@ -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
- return this.data.body.html;
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