@wire-dsl/web 0.0.8 → 0.1.1
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/.turbo/turbo-build.log +103 -7
- package/CHANGELOG.md +20 -0
- package/LICENSE +0 -15
- package/README.md +15 -17
- package/dist/assets/abap-DLDM7-KI.js +1 -0
- package/dist/assets/apex-DNDY2TF8.js +1 -0
- package/dist/assets/azcli-Y6nb8tq_.js +1 -0
- package/dist/assets/bat-BwHxbl9M.js +1 -0
- package/dist/assets/bicep-CFznDFnq.js +2 -0
- package/dist/assets/cameligo-Bf6VGUru.js +1 -0
- package/dist/assets/clojure-Dnu-v4kV.js +1 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/coffee-Bd8akH9Z.js +1 -0
- package/dist/assets/cpp-BbWJElDN.js +1 -0
- package/dist/assets/csharp-Co3qMtFm.js +1 -0
- package/dist/assets/csp-D-4FJmMZ.js +1 -0
- package/dist/assets/css-DdJfP1eB.js +3 -0
- package/dist/assets/css.worker-DBVD8oXr.js +93 -0
- package/dist/assets/cssMode-BrWliq3h.js +1 -0
- package/dist/assets/cypher-cTPe9QuQ.js +1 -0
- package/dist/assets/dart-BOtBlQCF.js +1 -0
- package/dist/assets/dockerfile-BG73LgW2.js +1 -0
- package/dist/assets/ecl-BEgZUVRK.js +1 -0
- package/dist/assets/elixir-BkW5O-1t.js +1 -0
- package/dist/assets/flow9-BeJ5waoc.js +1 -0
- package/dist/assets/freemarker2-Co3UwGNz.js +3 -0
- package/dist/assets/fsharp-PahG7c26.js +1 -0
- package/dist/assets/go-acbASCJo.js +1 -0
- package/dist/assets/graphql-BxJiqAUM.js +1 -0
- package/dist/assets/handlebars-D8lKCbT6.js +1 -0
- package/dist/assets/hcl-DtV1sZF8.js +1 -0
- package/dist/assets/html-PxDJOY2h.js +1 -0
- package/dist/assets/html.worker-CwpTb9lJ.js +470 -0
- package/dist/assets/htmlMode-BP-Aj81M.js +1 -0
- package/dist/assets/index-BU4hYlZS.js +1595 -0
- package/dist/assets/index-CmAJnnOw.css +1 -0
- package/dist/assets/ini-Kd9XrMLS.js +1 -0
- package/dist/assets/java-CXBNlu9o.js +1 -0
- package/dist/assets/javascript-BkTbkArn.js +1 -0
- package/dist/assets/json.worker-BoL8UZqY.js +58 -0
- package/dist/assets/jsonMode-C9MupgLd.js +7 -0
- package/dist/assets/julia-cl7-CwDS.js +1 -0
- package/dist/assets/kotlin-s7OhZKlX.js +1 -0
- package/dist/assets/less-9HpZscsL.js +2 -0
- package/dist/assets/lexon-OrD6JF1K.js +1 -0
- package/dist/assets/liquid-DNReYZ4N.js +1 -0
- package/dist/assets/lspLanguageFeatures-CePGAFz3.js +4 -0
- package/dist/assets/lua-Cyyb5UIc.js +1 -0
- package/dist/assets/m3-B8OfTtLu.js +1 -0
- package/dist/assets/markdown-BFxVWTOG.js +1 -0
- package/dist/assets/mdx-B0gIbLLv.js +1 -0
- package/dist/assets/mips-CiqrrVzr.js +1 -0
- package/dist/assets/msdax-DmeGPVcC.js +1 -0
- package/dist/assets/mysql-C_tMU-Nz.js +1 -0
- package/dist/assets/objective-c-BDtDVThU.js +1 -0
- package/dist/assets/pascal-vHIfCaH5.js +1 -0
- package/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
- package/dist/assets/perl-Ub6l9XKa.js +1 -0
- package/dist/assets/pgsql-BlNEE0v7.js +1 -0
- package/dist/assets/php-BBUBE1dy.js +1 -0
- package/dist/assets/pla-DSh2-awV.js +1 -0
- package/dist/assets/postiats-CocnycG-.js +1 -0
- package/dist/assets/powerquery-tScXyioY.js +1 -0
- package/dist/assets/powershell-COWaemsV.js +1 -0
- package/dist/assets/protobuf-Brw8urJB.js +2 -0
- package/dist/assets/pug-8SOpv6rk.js +1 -0
- package/dist/assets/python-EvgFBOf1.js +1 -0
- package/dist/assets/qsharp-Bw9ernYp.js +1 -0
- package/dist/assets/r-j7ic8hl3.js +1 -0
- package/dist/assets/razor-Cl3cdObV.js +1 -0
- package/dist/assets/redis-Bu5POkcn.js +1 -0
- package/dist/assets/redshift-Bs9aos_-.js +1 -0
- package/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
- package/dist/assets/ruby-zBfavPgS.js +1 -0
- package/dist/assets/rust-BzKRNQWT.js +1 -0
- package/dist/assets/sb-BBc9UKZt.js +1 -0
- package/dist/assets/scala-D9hQfWCl.js +1 -0
- package/dist/assets/scheme-BPhDTwHR.js +1 -0
- package/dist/assets/scss-CBJaRo0y.js +3 -0
- package/dist/assets/shell-DiJ1NA_G.js +1 -0
- package/dist/assets/solidity-Db0IVjzk.js +1 -0
- package/dist/assets/sophia-CnS9iZB_.js +1 -0
- package/dist/assets/sparql-CJmd_6j2.js +1 -0
- package/dist/assets/sql-ClhHkBeG.js +1 -0
- package/dist/assets/st-CHwy0fLd.js +1 -0
- package/dist/assets/swift-CnmFD0ga.js +1 -0
- package/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
- package/dist/assets/tcl-Dm6ycUr_.js +1 -0
- package/dist/assets/ts.worker-BH9nVgjN.js +67718 -0
- package/dist/assets/tsMode-BjwsOLQ1.js +11 -0
- package/dist/assets/twig-Csy3S7wG.js +1 -0
- package/dist/assets/typescript-BciYDL9m.js +1 -0
- package/dist/assets/typespec-Btyra-wh.js +1 -0
- package/dist/assets/vb-Db0cS2oM.js +1 -0
- package/dist/assets/wgsl-BTesnYfV.js +298 -0
- package/dist/assets/xml-ub5NEfyc.js +1 -0
- package/dist/assets/yaml-D1FNAoSy.js +1 -0
- package/dist/examples/admin-dashboard.wire +95 -0
- package/dist/examples/analytics-dashboard.wire +65 -0
- package/dist/examples/form-example.wire +50 -0
- package/dist/examples/simple-dashboard.wire +40 -0
- package/dist/examples/simple-multi-screen.wire +30 -0
- package/dist/index.html +2 -2
- package/package.json +5 -3
- package/postcss.config.mjs +3 -1
- package/public/examples/admin-dashboard.wire +95 -0
- package/public/examples/analytics-dashboard.wire +65 -0
- package/public/examples/form-example.wire +50 -0
- package/public/examples/simple-dashboard.wire +40 -0
- package/public/examples/simple-multi-screen.wire +30 -0
- package/src/App.tsx +3 -13
- package/src/components/MonacoEditorComponent.tsx +112 -0
- package/src/components/WireLiveEditor.tsx +729 -0
- package/src/components/WireLiveHeader.tsx +502 -0
- package/src/components/index.ts +5 -0
- package/src/hooks/useCanvasZoom.ts +137 -0
- package/src/hooks/useFileSystemAccess.ts +123 -0
- package/src/hooks/useWireParser.ts +222 -0
- package/src/index.css +1 -3
- package/src/main.tsx +7 -5
- package/src/monaco/wireLanguage.ts +370 -0
- package/src/store/editorStore.ts +196 -0
- package/src/store/index.ts +2 -0
- package/tailwind.config.js +2 -1
- package/vite.config.ts +17 -0
- package/dist/assets/index-CHiOjnNN.js +0 -9
- package/dist/assets/index-CUIy2zPc.css +0 -1
- package/src/App.js +0 -4
- package/src/main.js +0 -6
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { MonacoEditorComponent } from './MonacoEditorComponent';
|
|
3
|
+
import { WireLiveHeader } from './WireLiveHeader';
|
|
4
|
+
import { useEditorStore } from '../store/editorStore';
|
|
5
|
+
import { useWireParser } from '../hooks/useWireParser';
|
|
6
|
+
import { useCanvasZoom } from '../hooks/useCanvasZoom';
|
|
7
|
+
import { useFileSystemAccess } from '../hooks/useFileSystemAccess';
|
|
8
|
+
|
|
9
|
+
export const WireLiveEditor: React.FC = () => {
|
|
10
|
+
const { fileHandle, openFile, saveFile, saveFileAs } = useFileSystemAccess();
|
|
11
|
+
|
|
12
|
+
// Subscribe to both files and currentFileId to ensure re-renders
|
|
13
|
+
const files = useEditorStore((state) => state.files);
|
|
14
|
+
const currentFileId = useEditorStore((state) => state.currentFileId);
|
|
15
|
+
const previewMode = useEditorStore((state) => state.previewMode);
|
|
16
|
+
const selectedScreen = useEditorStore((state) => state.selectedScreen);
|
|
17
|
+
const createFile = useEditorStore((state) => state.createFile);
|
|
18
|
+
const updateFileContent = useEditorStore((state) => state.updateFileContent);
|
|
19
|
+
const renameFile = useEditorStore((state) => state.renameFile);
|
|
20
|
+
const markFileSaved = useEditorStore((state) => state.markFileSaved);
|
|
21
|
+
const setPreviewMode = useEditorStore((state) => state.setPreviewMode);
|
|
22
|
+
const setSelectedScreen = useEditorStore((state) => state.setSelectedScreen);
|
|
23
|
+
const getCurrentFile = useEditorStore((state) => state.getCurrentFile);
|
|
24
|
+
|
|
25
|
+
// Get current file - now guaranteed to be fresh
|
|
26
|
+
const currentFile = files.get(currentFileId);
|
|
27
|
+
const [diagnosticsVisible, setDiagnosticsVisible] = useState(false);
|
|
28
|
+
const currentFileHandleRef = useRef<any>(null);
|
|
29
|
+
const previewContainerRef = useRef<HTMLDivElement | null>(null);
|
|
30
|
+
const svgContainerRef = useRef<HTMLDivElement | null>(null);
|
|
31
|
+
|
|
32
|
+
// Parser hook - real integration with wire-dsl/engine
|
|
33
|
+
// Pass both currentFile.content directly to ensure reactivity
|
|
34
|
+
const { renderState, renderResult, diagnostics } = useWireParser(
|
|
35
|
+
currentFile?.content || '',
|
|
36
|
+
selectedScreen
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Canvas zoom hook - no size params needed, extracts from DOM
|
|
40
|
+
const {
|
|
41
|
+
zoom,
|
|
42
|
+
fitZoom,
|
|
43
|
+
originalWidth,
|
|
44
|
+
originalHeight,
|
|
45
|
+
zoomIn,
|
|
46
|
+
zoomOut,
|
|
47
|
+
resetZoom,
|
|
48
|
+
handleWheel,
|
|
49
|
+
setPreviewRef,
|
|
50
|
+
extractAndInitialize,
|
|
51
|
+
} = useCanvasZoom();
|
|
52
|
+
|
|
53
|
+
// Update preview ref when container is ready
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setPreviewRef(previewContainerRef.current);
|
|
56
|
+
}, [setPreviewRef]);
|
|
57
|
+
|
|
58
|
+
// Reset zoom when file changes
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
resetZoom();
|
|
61
|
+
}, [currentFileId, resetZoom]);
|
|
62
|
+
|
|
63
|
+
// Extract SVG dimensions and initialize zoom when SVG is rendered
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
// Use setTimeout to ensure SVG is in DOM before extracting
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
extractAndInitialize();
|
|
68
|
+
}, 0);
|
|
69
|
+
return () => clearTimeout(timer);
|
|
70
|
+
}, [renderResult?.svg, extractAndInitialize]);
|
|
71
|
+
|
|
72
|
+
// Apply zoom to SVG element - using ORIGINAL dimensions like the extension
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!previewContainerRef.current || !svgContainerRef.current) return;
|
|
75
|
+
if (originalWidth <= 0 || originalHeight <= 0) return;
|
|
76
|
+
|
|
77
|
+
// Find SVG element
|
|
78
|
+
const svgElement = svgContainerRef.current.querySelector('svg');
|
|
79
|
+
if (!svgElement) return;
|
|
80
|
+
|
|
81
|
+
// Calculate new dimensions using original size * zoom scale
|
|
82
|
+
// This is exactly what the extension does
|
|
83
|
+
const newWidth = originalWidth * zoom;
|
|
84
|
+
const newHeight = originalHeight * zoom;
|
|
85
|
+
|
|
86
|
+
// Set container size
|
|
87
|
+
svgContainerRef.current.style.width = newWidth + 'px';
|
|
88
|
+
svgContainerRef.current.style.height = newHeight + 'px';
|
|
89
|
+
|
|
90
|
+
// Set SVG size
|
|
91
|
+
svgElement.style.width = newWidth + 'px';
|
|
92
|
+
svgElement.style.height = newHeight + 'px';
|
|
93
|
+
|
|
94
|
+
// Center scroll after DOM update (exactly like the extension)
|
|
95
|
+
// Use setTimeout to ensure layout is updated
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
if (!previewContainerRef.current) return;
|
|
98
|
+
|
|
99
|
+
const container = previewContainerRef.current;
|
|
100
|
+
const scrollX = (container.scrollWidth - container.clientWidth) / 2;
|
|
101
|
+
const scrollY = (container.scrollHeight - container.clientHeight) / 2;
|
|
102
|
+
|
|
103
|
+
container.scrollLeft = Math.max(0, scrollX);
|
|
104
|
+
container.scrollTop = Math.max(0, scrollY);
|
|
105
|
+
}, 0);
|
|
106
|
+
}, [zoom, originalWidth, originalHeight]);
|
|
107
|
+
|
|
108
|
+
const handleFileChange = (content: string) => {
|
|
109
|
+
updateFileContent(currentFileId, content);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleNew = () => {
|
|
113
|
+
const newFileName = `untitled-${Date.now()}.wire`;
|
|
114
|
+
createFile(newFileName);
|
|
115
|
+
// Clear file handle for new files
|
|
116
|
+
currentFileHandleRef.current = null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleOpen = async () => {
|
|
120
|
+
// Try FileSystemAccess API first, fallback to input element
|
|
121
|
+
if (typeof window !== 'undefined' && 'showOpenFilePicker' in window) {
|
|
122
|
+
const result = await openFile();
|
|
123
|
+
if (result) {
|
|
124
|
+
createFile(result.name, result.content);
|
|
125
|
+
// Save the file handle directly from the result for later use in save
|
|
126
|
+
currentFileHandleRef.current = result.handle;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Fallback to input element - clears file handle
|
|
132
|
+
currentFileHandleRef.current = null;
|
|
133
|
+
const fileInput = document.createElement('input');
|
|
134
|
+
fileInput.type = 'file';
|
|
135
|
+
fileInput.accept = '.wire,.txt';
|
|
136
|
+
fileInput.onchange = (e: any) => {
|
|
137
|
+
const file = e.target.files?.[0];
|
|
138
|
+
if (file) {
|
|
139
|
+
const reader = new FileReader();
|
|
140
|
+
reader.onload = (ev) => {
|
|
141
|
+
const content = ev.target?.result as string;
|
|
142
|
+
createFile(file.name, content);
|
|
143
|
+
};
|
|
144
|
+
reader.readAsText(file);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
fileInput.click();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const handleExport = () => {
|
|
151
|
+
if (!renderResult?.svg) {
|
|
152
|
+
alert('No SVG content to export');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const element = document.createElement('a');
|
|
157
|
+
element.setAttribute(
|
|
158
|
+
'href',
|
|
159
|
+
'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(renderResult.svg)
|
|
160
|
+
);
|
|
161
|
+
// Remove .wire extension if present before adding .svg
|
|
162
|
+
const fileNameWithoutExt = currentFile?.name?.replace(/\.wire$/, '') || 'export';
|
|
163
|
+
element.setAttribute('download', `${fileNameWithoutExt}.svg`);
|
|
164
|
+
element.style.display = 'none';
|
|
165
|
+
document.body.appendChild(element);
|
|
166
|
+
element.click();
|
|
167
|
+
document.body.removeChild(element);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleExampleSelect = async (exampleName: string) => {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(`/examples/${exampleName}.wire`);
|
|
173
|
+
const content = await response.text();
|
|
174
|
+
createFile(`${exampleName}.wire`, content);
|
|
175
|
+
// Clear file handle for example files
|
|
176
|
+
currentFileHandleRef.current = null;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('Error loading example:', error);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleSave = async () => {
|
|
183
|
+
if (!currentFile) return;
|
|
184
|
+
|
|
185
|
+
// 1. Si existe handle, guardar con él
|
|
186
|
+
if (currentFileHandleRef.current) {
|
|
187
|
+
try {
|
|
188
|
+
const success = await saveFile(currentFile.content);
|
|
189
|
+
if (success) {
|
|
190
|
+
markFileSaved(currentFileId);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('Error saving:', err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. Si no existe handle, intentar abrir picker
|
|
199
|
+
if (typeof window !== 'undefined' && 'showSaveFilePicker' in window) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await saveFileAs(currentFile.content, currentFile.name);
|
|
202
|
+
|
|
203
|
+
// Usuario canceló el diálogo - no hacer nada
|
|
204
|
+
if (result === false) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Guardado exitoso
|
|
209
|
+
if (result) {
|
|
210
|
+
renameFile(currentFileId, result.name);
|
|
211
|
+
markFileSaved(currentFileId);
|
|
212
|
+
currentFileHandleRef.current = result.handle;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Error real (result === null) - continuar al fallback
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error('Error in save as:', err);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 3. Fallback: descarga
|
|
223
|
+
const element = document.createElement('a');
|
|
224
|
+
element.setAttribute(
|
|
225
|
+
'href',
|
|
226
|
+
'data:text/plain;charset=utf-8,' + encodeURIComponent(currentFile.content)
|
|
227
|
+
);
|
|
228
|
+
element.setAttribute('download', currentFile.name);
|
|
229
|
+
element.style.display = 'none';
|
|
230
|
+
document.body.appendChild(element);
|
|
231
|
+
element.click();
|
|
232
|
+
document.body.removeChild(element);
|
|
233
|
+
markFileSaved(currentFileId);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleSaveAs = async () => {
|
|
237
|
+
if (!currentFile) return;
|
|
238
|
+
|
|
239
|
+
// Try FileSystemAccess first
|
|
240
|
+
if (typeof window !== 'undefined' && 'showSaveFilePicker' in window) {
|
|
241
|
+
const result = await saveFileAs(currentFile.content, currentFile.name);
|
|
242
|
+
|
|
243
|
+
// Usuario canceló el diálogo - no hacer nada
|
|
244
|
+
if (result === false) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Guardado exitoso
|
|
249
|
+
if (result) {
|
|
250
|
+
renameFile(currentFileId, result.name);
|
|
251
|
+
markFileSaved(currentFileId);
|
|
252
|
+
// Save the handle directly from result for subsequent saves
|
|
253
|
+
currentFileHandleRef.current = result.handle;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Error real (result === null) - continuar al fallback
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Fallback to browser download
|
|
261
|
+
const element = document.createElement('a');
|
|
262
|
+
element.setAttribute(
|
|
263
|
+
'href',
|
|
264
|
+
'data:text/plain;charset=utf-8,' + encodeURIComponent(currentFile.content)
|
|
265
|
+
);
|
|
266
|
+
element.setAttribute('download', currentFile.name);
|
|
267
|
+
element.style.display = 'none';
|
|
268
|
+
document.body.appendChild(element);
|
|
269
|
+
element.click();
|
|
270
|
+
document.body.removeChild(element);
|
|
271
|
+
|
|
272
|
+
markFileSaved(currentFileId);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const handleRename = (newName: string) => {
|
|
276
|
+
renameFile(currentFileId, newName);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Keyboard shortcuts
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
282
|
+
// Solo si Ctrl (o Cmd en Mac) está presionado
|
|
283
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
284
|
+
|
|
285
|
+
switch (e.key.toLowerCase()) {
|
|
286
|
+
case 's':
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
if (e.shiftKey) {
|
|
289
|
+
handleSaveAs();
|
|
290
|
+
} else {
|
|
291
|
+
handleSave();
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
case 'n':
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
handleNew();
|
|
297
|
+
break;
|
|
298
|
+
case 'o':
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
handleOpen();
|
|
301
|
+
break;
|
|
302
|
+
case 'e':
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
handleExport();
|
|
305
|
+
break;
|
|
306
|
+
default:
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
312
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
313
|
+
}, [handleSave, handleSaveAs, handleNew, handleOpen, handleExport]);
|
|
314
|
+
|
|
315
|
+
if (!currentFile) {
|
|
316
|
+
return (
|
|
317
|
+
<div
|
|
318
|
+
style={{
|
|
319
|
+
height: '100vh',
|
|
320
|
+
display: 'flex',
|
|
321
|
+
flexDirection: 'column',
|
|
322
|
+
backgroundColor: '#f9fafb',
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<WireLiveHeader
|
|
326
|
+
currentFileName="No file"
|
|
327
|
+
isDirty={false}
|
|
328
|
+
onNew={handleNew}
|
|
329
|
+
onOpen={handleOpen}
|
|
330
|
+
onSave={handleSave}
|
|
331
|
+
onSaveAs={handleSaveAs}
|
|
332
|
+
onRename={handleRename}
|
|
333
|
+
onExport={handleExport}
|
|
334
|
+
onExampleSelect={handleExampleSelect}
|
|
335
|
+
/>
|
|
336
|
+
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
337
|
+
<p style={{ color: '#6b7280', fontSize: '16px' }}>
|
|
338
|
+
Create or open a file to start editing.
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div
|
|
347
|
+
style={{
|
|
348
|
+
height: '100vh',
|
|
349
|
+
display: 'flex',
|
|
350
|
+
flexDirection: 'column',
|
|
351
|
+
backgroundColor: '#ffffff',
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
{/* Header */}
|
|
355
|
+
<WireLiveHeader
|
|
356
|
+
currentFileName={currentFile.name}
|
|
357
|
+
isDirty={currentFile.isDirty}
|
|
358
|
+
onNew={handleNew}
|
|
359
|
+
onOpen={handleOpen}
|
|
360
|
+
onSave={handleSave}
|
|
361
|
+
onSaveAs={handleSaveAs}
|
|
362
|
+
onRename={handleRename}
|
|
363
|
+
onExport={handleExport}
|
|
364
|
+
onExampleSelect={handleExampleSelect}
|
|
365
|
+
/>
|
|
366
|
+
|
|
367
|
+
{/* Main content with split view */}
|
|
368
|
+
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
|
369
|
+
{/* Editor and Preview split */}
|
|
370
|
+
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'row' }}>
|
|
371
|
+
{/* Editor panel - left side (50%) */}
|
|
372
|
+
<div
|
|
373
|
+
style={{
|
|
374
|
+
flex: '0 0 50%',
|
|
375
|
+
height: '100%',
|
|
376
|
+
overflow: 'hidden',
|
|
377
|
+
backgroundColor: '#ffffff',
|
|
378
|
+
borderRight: '1px solid #e5e7eb',
|
|
379
|
+
display: 'flex',
|
|
380
|
+
flexDirection: 'column',
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<MonacoEditorComponent
|
|
384
|
+
content={currentFile.content}
|
|
385
|
+
onChange={handleFileChange}
|
|
386
|
+
fileName={currentFile.name}
|
|
387
|
+
language="wire"
|
|
388
|
+
/>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{/* Divider */}
|
|
392
|
+
<div
|
|
393
|
+
style={{
|
|
394
|
+
width: '1px',
|
|
395
|
+
backgroundColor: '#d1d5db',
|
|
396
|
+
cursor: 'col-resize',
|
|
397
|
+
transition: 'background-color 0.2s',
|
|
398
|
+
}}
|
|
399
|
+
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#9ca3af')}
|
|
400
|
+
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = '#d1d5db')}
|
|
401
|
+
/>
|
|
402
|
+
|
|
403
|
+
{/* Preview panel - right side (50%) */}
|
|
404
|
+
<div
|
|
405
|
+
style={{
|
|
406
|
+
flex: '0 0 50%',
|
|
407
|
+
height: '100%',
|
|
408
|
+
overflow: 'hidden',
|
|
409
|
+
backgroundColor: '#fafbfc',
|
|
410
|
+
display: 'flex',
|
|
411
|
+
flexDirection: 'column',
|
|
412
|
+
position: 'relative',
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
{/* Controls Container - Screens & Zoom */}
|
|
416
|
+
<div
|
|
417
|
+
style={{
|
|
418
|
+
position: 'absolute',
|
|
419
|
+
top: '15px',
|
|
420
|
+
right: '15px',
|
|
421
|
+
zIndex: 10,
|
|
422
|
+
display: 'flex',
|
|
423
|
+
gap: '6px',
|
|
424
|
+
backgroundColor: '#ffffff',
|
|
425
|
+
borderRadius: '6px',
|
|
426
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
|
427
|
+
padding: '6px',
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
{/* Screens Dropdown */}
|
|
431
|
+
{renderResult?.screens && renderResult.screens.length > 1 && (
|
|
432
|
+
<>
|
|
433
|
+
<div style={{ position: 'relative', display: 'inline-block' }}>
|
|
434
|
+
<button
|
|
435
|
+
style={{
|
|
436
|
+
padding: '6px 10px',
|
|
437
|
+
backgroundColor: '#f3f4f6',
|
|
438
|
+
border: '1px solid #d1d5db',
|
|
439
|
+
borderRadius: '4px',
|
|
440
|
+
cursor: 'pointer',
|
|
441
|
+
fontSize: '12px',
|
|
442
|
+
fontWeight: '500',
|
|
443
|
+
color: '#374151',
|
|
444
|
+
transition: 'all 0.2s',
|
|
445
|
+
maxWidth: '150px',
|
|
446
|
+
overflow: 'hidden',
|
|
447
|
+
textOverflow: 'ellipsis',
|
|
448
|
+
whiteSpace: 'nowrap',
|
|
449
|
+
display: 'flex',
|
|
450
|
+
alignItems: 'center',
|
|
451
|
+
gap: '6px',
|
|
452
|
+
}}
|
|
453
|
+
onClick={(e) => {
|
|
454
|
+
const menu = e.currentTarget.nextElementSibling as HTMLElement;
|
|
455
|
+
if (menu) {
|
|
456
|
+
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
|
457
|
+
}
|
|
458
|
+
}}
|
|
459
|
+
onMouseEnter={(e) => {
|
|
460
|
+
e.currentTarget.style.backgroundColor = '#e5e7eb';
|
|
461
|
+
}}
|
|
462
|
+
onMouseLeave={(e) => {
|
|
463
|
+
e.currentTarget.style.backgroundColor = '#f3f4f6';
|
|
464
|
+
}}
|
|
465
|
+
title="Select screen"
|
|
466
|
+
>
|
|
467
|
+
<span style={{ flex: 1, textAlign: 'left' }}>
|
|
468
|
+
{renderResult.selectedScreenName || 'Screen'}
|
|
469
|
+
</span>
|
|
470
|
+
<svg
|
|
471
|
+
width="14"
|
|
472
|
+
height="14"
|
|
473
|
+
viewBox="0 0 24 24"
|
|
474
|
+
fill="none"
|
|
475
|
+
stroke="currentColor"
|
|
476
|
+
strokeWidth="2"
|
|
477
|
+
strokeLinecap="round"
|
|
478
|
+
strokeLinejoin="round"
|
|
479
|
+
style={{ flexShrink: 0 }}
|
|
480
|
+
>
|
|
481
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
482
|
+
</svg>
|
|
483
|
+
</button>
|
|
484
|
+
|
|
485
|
+
<div
|
|
486
|
+
style={{
|
|
487
|
+
position: 'absolute',
|
|
488
|
+
top: '100%',
|
|
489
|
+
left: '0',
|
|
490
|
+
marginTop: '4px',
|
|
491
|
+
backgroundColor: '#ffffff',
|
|
492
|
+
border: '1px solid #d1d5db',
|
|
493
|
+
borderRadius: '4px',
|
|
494
|
+
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
|
495
|
+
zIndex: 100,
|
|
496
|
+
minWidth: '160px',
|
|
497
|
+
display: 'none',
|
|
498
|
+
}}
|
|
499
|
+
onMouseLeave={(e) => {
|
|
500
|
+
e.currentTarget.style.display = 'none';
|
|
501
|
+
}}
|
|
502
|
+
>
|
|
503
|
+
{renderResult.screens.map((screen, index) => (
|
|
504
|
+
<button
|
|
505
|
+
key={screen.id}
|
|
506
|
+
onClick={(e) => {
|
|
507
|
+
setSelectedScreen(screen.name);
|
|
508
|
+
const menu = (e.currentTarget.parentElement as HTMLElement);
|
|
509
|
+
if (menu) menu.style.display = 'none';
|
|
510
|
+
}}
|
|
511
|
+
style={{
|
|
512
|
+
width: '100%',
|
|
513
|
+
textAlign: 'left',
|
|
514
|
+
paddingLeft: '12px',
|
|
515
|
+
paddingRight: '12px',
|
|
516
|
+
paddingTop: '8px',
|
|
517
|
+
paddingBottom: '8px',
|
|
518
|
+
fontSize: '12px',
|
|
519
|
+
color:
|
|
520
|
+
renderResult.selectedScreenName === screen.name ? '#3b82f6' : '#374151',
|
|
521
|
+
backgroundColor:
|
|
522
|
+
renderResult.selectedScreenName === screen.name
|
|
523
|
+
? '#eff6ff'
|
|
524
|
+
: 'transparent',
|
|
525
|
+
border: 'none',
|
|
526
|
+
cursor: 'pointer',
|
|
527
|
+
transition: 'background-color 0.2s',
|
|
528
|
+
fontWeight:
|
|
529
|
+
renderResult.selectedScreenName === screen.name ? '600' : '500',
|
|
530
|
+
borderTopLeftRadius: index === 0 ? '4px' : '0px',
|
|
531
|
+
borderTopRightRadius: index === 0 ? '4px' : '0px',
|
|
532
|
+
borderBottomLeftRadius:
|
|
533
|
+
index === renderResult.screens.length - 1 ? '4px' : '0px',
|
|
534
|
+
borderBottomRightRadius:
|
|
535
|
+
index === renderResult.screens.length - 1 ? '4px' : '0px',
|
|
536
|
+
}}
|
|
537
|
+
onMouseEnter={(e) => {
|
|
538
|
+
if (renderResult.selectedScreenName !== screen.name) {
|
|
539
|
+
e.currentTarget.style.backgroundColor = '#f3f4f6';
|
|
540
|
+
}
|
|
541
|
+
}}
|
|
542
|
+
onMouseLeave={(e) => {
|
|
543
|
+
if (renderResult.selectedScreenName !== screen.name) {
|
|
544
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
545
|
+
}
|
|
546
|
+
}}
|
|
547
|
+
>
|
|
548
|
+
{screen.name}
|
|
549
|
+
</button>
|
|
550
|
+
))}
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
<div
|
|
554
|
+
style={{
|
|
555
|
+
width: '1px',
|
|
556
|
+
backgroundColor: '#d1d5db',
|
|
557
|
+
}}
|
|
558
|
+
/>
|
|
559
|
+
</>
|
|
560
|
+
)}
|
|
561
|
+
|
|
562
|
+
{/* Zoom Controls */}
|
|
563
|
+
<button
|
|
564
|
+
onClick={zoomOut}
|
|
565
|
+
style={{
|
|
566
|
+
padding: '6px 10px',
|
|
567
|
+
backgroundColor: '#f3f4f6',
|
|
568
|
+
border: '1px solid #d1d5db',
|
|
569
|
+
borderRadius: '4px',
|
|
570
|
+
cursor: 'pointer',
|
|
571
|
+
fontSize: '12px',
|
|
572
|
+
fontWeight: '500',
|
|
573
|
+
color: '#374151',
|
|
574
|
+
transition: 'all 0.2s',
|
|
575
|
+
}}
|
|
576
|
+
onMouseEnter={(e) => {
|
|
577
|
+
e.currentTarget.style.backgroundColor = '#e5e7eb';
|
|
578
|
+
}}
|
|
579
|
+
onMouseLeave={(e) => {
|
|
580
|
+
e.currentTarget.style.backgroundColor = '#f3f4f6';
|
|
581
|
+
}}
|
|
582
|
+
title="Zoom Out (Ctrl + Scroll)"
|
|
583
|
+
>
|
|
584
|
+
−
|
|
585
|
+
</button>
|
|
586
|
+
<div
|
|
587
|
+
style={{
|
|
588
|
+
padding: '6px 10px',
|
|
589
|
+
backgroundColor: '#f3f4f6',
|
|
590
|
+
borderRadius: '4px',
|
|
591
|
+
fontSize: '12px',
|
|
592
|
+
fontWeight: '500',
|
|
593
|
+
color: '#374151',
|
|
594
|
+
minWidth: '50px',
|
|
595
|
+
textAlign: 'center',
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
{Math.round(zoom * 100)}%
|
|
599
|
+
</div>
|
|
600
|
+
<button
|
|
601
|
+
onClick={zoomIn}
|
|
602
|
+
style={{
|
|
603
|
+
padding: '6px 10px',
|
|
604
|
+
backgroundColor: '#f3f4f6',
|
|
605
|
+
border: '1px solid #d1d5db',
|
|
606
|
+
borderRadius: '4px',
|
|
607
|
+
cursor: 'pointer',
|
|
608
|
+
fontSize: '12px',
|
|
609
|
+
fontWeight: '500',
|
|
610
|
+
color: '#374151',
|
|
611
|
+
transition: 'all 0.2s',
|
|
612
|
+
}}
|
|
613
|
+
onMouseEnter={(e) => {
|
|
614
|
+
e.currentTarget.style.backgroundColor = '#e5e7eb';
|
|
615
|
+
}}
|
|
616
|
+
onMouseLeave={(e) => {
|
|
617
|
+
e.currentTarget.style.backgroundColor = '#f3f4f6';
|
|
618
|
+
}}
|
|
619
|
+
title="Zoom In (Ctrl + Scroll)"
|
|
620
|
+
>
|
|
621
|
+
+
|
|
622
|
+
</button>
|
|
623
|
+
<div
|
|
624
|
+
style={{
|
|
625
|
+
width: '1px',
|
|
626
|
+
backgroundColor: '#d1d5db',
|
|
627
|
+
}}
|
|
628
|
+
/>
|
|
629
|
+
<button
|
|
630
|
+
onClick={resetZoom}
|
|
631
|
+
style={{
|
|
632
|
+
padding: '6px 10px',
|
|
633
|
+
backgroundColor: '#f3f4f6',
|
|
634
|
+
border: '1px solid #d1d5db',
|
|
635
|
+
borderRadius: '4px',
|
|
636
|
+
cursor: 'pointer',
|
|
637
|
+
fontSize: '11px',
|
|
638
|
+
fontWeight: '500',
|
|
639
|
+
color: '#374151',
|
|
640
|
+
transition: 'all 0.2s',
|
|
641
|
+
}}
|
|
642
|
+
onMouseEnter={(e) => {
|
|
643
|
+
e.currentTarget.style.backgroundColor = '#e5e7eb';
|
|
644
|
+
}}
|
|
645
|
+
onMouseLeave={(e) => {
|
|
646
|
+
e.currentTarget.style.backgroundColor = '#f3f4f6';
|
|
647
|
+
}}
|
|
648
|
+
title="Reset Zoom"
|
|
649
|
+
>
|
|
650
|
+
Reset
|
|
651
|
+
</button>
|
|
652
|
+
</div>
|
|
653
|
+
|
|
654
|
+
{/* Canvas container with scroll */}
|
|
655
|
+
<div
|
|
656
|
+
ref={previewContainerRef}
|
|
657
|
+
style={{
|
|
658
|
+
flex: 1,
|
|
659
|
+
overflow: 'auto',
|
|
660
|
+
display: 'grid',
|
|
661
|
+
placeItems: 'center',
|
|
662
|
+
padding: '20px',
|
|
663
|
+
userSelect: 'none',
|
|
664
|
+
}}
|
|
665
|
+
onWheel={handleWheel}
|
|
666
|
+
>
|
|
667
|
+
{renderState === 'parsing' || renderState === 'rendering' ? (
|
|
668
|
+
<div style={{ color: '#6b7280', fontSize: '14px' }}>
|
|
669
|
+
{renderState === 'parsing' ? 'Parsing...' : 'Rendering...'}
|
|
670
|
+
</div>
|
|
671
|
+
) : diagnostics.length > 0 ? (
|
|
672
|
+
<div style={{ color: '#dc2626', fontSize: '14px', textAlign: 'center' }}>
|
|
673
|
+
<div style={{ marginBottom: '10px', fontWeight: '500' }}>Parse Error</div>
|
|
674
|
+
{diagnostics[0]?.message}
|
|
675
|
+
</div>
|
|
676
|
+
) : renderResult?.svg ? (
|
|
677
|
+
<div ref={svgContainerRef} dangerouslySetInnerHTML={{ __html: renderResult.svg }} />
|
|
678
|
+
) : (
|
|
679
|
+
<div style={{ color: '#9ca3af', fontSize: '14px' }}>Add code to preview SVG</div>
|
|
680
|
+
)}
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
{/* Diagnostics panel - bottom (only show if errors) */}
|
|
686
|
+
{diagnostics.length > 0 && (
|
|
687
|
+
<div
|
|
688
|
+
style={{
|
|
689
|
+
borderTop: '1px solid #e5e7eb',
|
|
690
|
+
backgroundColor: '#fef2f2',
|
|
691
|
+
padding: '12px 16px',
|
|
692
|
+
maxHeight: '120px',
|
|
693
|
+
overflow: 'auto',
|
|
694
|
+
}}
|
|
695
|
+
>
|
|
696
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
|
697
|
+
<span style={{ color: '#dc2626', fontWeight: '500' }}>
|
|
698
|
+
Problems {diagnostics.length}
|
|
699
|
+
</span>
|
|
700
|
+
<button
|
|
701
|
+
onClick={() => setDiagnosticsVisible(!diagnosticsVisible)}
|
|
702
|
+
style={{
|
|
703
|
+
marginLeft: 'auto',
|
|
704
|
+
background: 'none',
|
|
705
|
+
border: 'none',
|
|
706
|
+
cursor: 'pointer',
|
|
707
|
+
color: '#6b7280',
|
|
708
|
+
fontSize: '12px',
|
|
709
|
+
padding: '4px 8px',
|
|
710
|
+
}}
|
|
711
|
+
>
|
|
712
|
+
{diagnosticsVisible ? 'Hide' : 'Show'}
|
|
713
|
+
</button>
|
|
714
|
+
</div>
|
|
715
|
+
{diagnosticsVisible && (
|
|
716
|
+
<div style={{ fontSize: '12px' }}>
|
|
717
|
+
{diagnostics.map((diag, idx) => (
|
|
718
|
+
<div key={idx} style={{ color: '#7f1d1d', marginBottom: '4px' }}>
|
|
719
|
+
Line {diag.line}: {diag.message}
|
|
720
|
+
</div>
|
|
721
|
+
))}
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
)}
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
};
|