@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.
Files changed (129) hide show
  1. package/.turbo/turbo-build.log +103 -7
  2. package/CHANGELOG.md +20 -0
  3. package/LICENSE +0 -15
  4. package/README.md +15 -17
  5. package/dist/assets/abap-DLDM7-KI.js +1 -0
  6. package/dist/assets/apex-DNDY2TF8.js +1 -0
  7. package/dist/assets/azcli-Y6nb8tq_.js +1 -0
  8. package/dist/assets/bat-BwHxbl9M.js +1 -0
  9. package/dist/assets/bicep-CFznDFnq.js +2 -0
  10. package/dist/assets/cameligo-Bf6VGUru.js +1 -0
  11. package/dist/assets/clojure-Dnu-v4kV.js +1 -0
  12. package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  13. package/dist/assets/coffee-Bd8akH9Z.js +1 -0
  14. package/dist/assets/cpp-BbWJElDN.js +1 -0
  15. package/dist/assets/csharp-Co3qMtFm.js +1 -0
  16. package/dist/assets/csp-D-4FJmMZ.js +1 -0
  17. package/dist/assets/css-DdJfP1eB.js +3 -0
  18. package/dist/assets/css.worker-DBVD8oXr.js +93 -0
  19. package/dist/assets/cssMode-BrWliq3h.js +1 -0
  20. package/dist/assets/cypher-cTPe9QuQ.js +1 -0
  21. package/dist/assets/dart-BOtBlQCF.js +1 -0
  22. package/dist/assets/dockerfile-BG73LgW2.js +1 -0
  23. package/dist/assets/ecl-BEgZUVRK.js +1 -0
  24. package/dist/assets/elixir-BkW5O-1t.js +1 -0
  25. package/dist/assets/flow9-BeJ5waoc.js +1 -0
  26. package/dist/assets/freemarker2-Co3UwGNz.js +3 -0
  27. package/dist/assets/fsharp-PahG7c26.js +1 -0
  28. package/dist/assets/go-acbASCJo.js +1 -0
  29. package/dist/assets/graphql-BxJiqAUM.js +1 -0
  30. package/dist/assets/handlebars-D8lKCbT6.js +1 -0
  31. package/dist/assets/hcl-DtV1sZF8.js +1 -0
  32. package/dist/assets/html-PxDJOY2h.js +1 -0
  33. package/dist/assets/html.worker-CwpTb9lJ.js +470 -0
  34. package/dist/assets/htmlMode-BP-Aj81M.js +1 -0
  35. package/dist/assets/index-BU4hYlZS.js +1595 -0
  36. package/dist/assets/index-CmAJnnOw.css +1 -0
  37. package/dist/assets/ini-Kd9XrMLS.js +1 -0
  38. package/dist/assets/java-CXBNlu9o.js +1 -0
  39. package/dist/assets/javascript-BkTbkArn.js +1 -0
  40. package/dist/assets/json.worker-BoL8UZqY.js +58 -0
  41. package/dist/assets/jsonMode-C9MupgLd.js +7 -0
  42. package/dist/assets/julia-cl7-CwDS.js +1 -0
  43. package/dist/assets/kotlin-s7OhZKlX.js +1 -0
  44. package/dist/assets/less-9HpZscsL.js +2 -0
  45. package/dist/assets/lexon-OrD6JF1K.js +1 -0
  46. package/dist/assets/liquid-DNReYZ4N.js +1 -0
  47. package/dist/assets/lspLanguageFeatures-CePGAFz3.js +4 -0
  48. package/dist/assets/lua-Cyyb5UIc.js +1 -0
  49. package/dist/assets/m3-B8OfTtLu.js +1 -0
  50. package/dist/assets/markdown-BFxVWTOG.js +1 -0
  51. package/dist/assets/mdx-B0gIbLLv.js +1 -0
  52. package/dist/assets/mips-CiqrrVzr.js +1 -0
  53. package/dist/assets/msdax-DmeGPVcC.js +1 -0
  54. package/dist/assets/mysql-C_tMU-Nz.js +1 -0
  55. package/dist/assets/objective-c-BDtDVThU.js +1 -0
  56. package/dist/assets/pascal-vHIfCaH5.js +1 -0
  57. package/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
  58. package/dist/assets/perl-Ub6l9XKa.js +1 -0
  59. package/dist/assets/pgsql-BlNEE0v7.js +1 -0
  60. package/dist/assets/php-BBUBE1dy.js +1 -0
  61. package/dist/assets/pla-DSh2-awV.js +1 -0
  62. package/dist/assets/postiats-CocnycG-.js +1 -0
  63. package/dist/assets/powerquery-tScXyioY.js +1 -0
  64. package/dist/assets/powershell-COWaemsV.js +1 -0
  65. package/dist/assets/protobuf-Brw8urJB.js +2 -0
  66. package/dist/assets/pug-8SOpv6rk.js +1 -0
  67. package/dist/assets/python-EvgFBOf1.js +1 -0
  68. package/dist/assets/qsharp-Bw9ernYp.js +1 -0
  69. package/dist/assets/r-j7ic8hl3.js +1 -0
  70. package/dist/assets/razor-Cl3cdObV.js +1 -0
  71. package/dist/assets/redis-Bu5POkcn.js +1 -0
  72. package/dist/assets/redshift-Bs9aos_-.js +1 -0
  73. package/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
  74. package/dist/assets/ruby-zBfavPgS.js +1 -0
  75. package/dist/assets/rust-BzKRNQWT.js +1 -0
  76. package/dist/assets/sb-BBc9UKZt.js +1 -0
  77. package/dist/assets/scala-D9hQfWCl.js +1 -0
  78. package/dist/assets/scheme-BPhDTwHR.js +1 -0
  79. package/dist/assets/scss-CBJaRo0y.js +3 -0
  80. package/dist/assets/shell-DiJ1NA_G.js +1 -0
  81. package/dist/assets/solidity-Db0IVjzk.js +1 -0
  82. package/dist/assets/sophia-CnS9iZB_.js +1 -0
  83. package/dist/assets/sparql-CJmd_6j2.js +1 -0
  84. package/dist/assets/sql-ClhHkBeG.js +1 -0
  85. package/dist/assets/st-CHwy0fLd.js +1 -0
  86. package/dist/assets/swift-CnmFD0ga.js +1 -0
  87. package/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
  88. package/dist/assets/tcl-Dm6ycUr_.js +1 -0
  89. package/dist/assets/ts.worker-BH9nVgjN.js +67718 -0
  90. package/dist/assets/tsMode-BjwsOLQ1.js +11 -0
  91. package/dist/assets/twig-Csy3S7wG.js +1 -0
  92. package/dist/assets/typescript-BciYDL9m.js +1 -0
  93. package/dist/assets/typespec-Btyra-wh.js +1 -0
  94. package/dist/assets/vb-Db0cS2oM.js +1 -0
  95. package/dist/assets/wgsl-BTesnYfV.js +298 -0
  96. package/dist/assets/xml-ub5NEfyc.js +1 -0
  97. package/dist/assets/yaml-D1FNAoSy.js +1 -0
  98. package/dist/examples/admin-dashboard.wire +95 -0
  99. package/dist/examples/analytics-dashboard.wire +65 -0
  100. package/dist/examples/form-example.wire +50 -0
  101. package/dist/examples/simple-dashboard.wire +40 -0
  102. package/dist/examples/simple-multi-screen.wire +30 -0
  103. package/dist/index.html +2 -2
  104. package/package.json +5 -3
  105. package/postcss.config.mjs +3 -1
  106. package/public/examples/admin-dashboard.wire +95 -0
  107. package/public/examples/analytics-dashboard.wire +65 -0
  108. package/public/examples/form-example.wire +50 -0
  109. package/public/examples/simple-dashboard.wire +40 -0
  110. package/public/examples/simple-multi-screen.wire +30 -0
  111. package/src/App.tsx +3 -13
  112. package/src/components/MonacoEditorComponent.tsx +112 -0
  113. package/src/components/WireLiveEditor.tsx +729 -0
  114. package/src/components/WireLiveHeader.tsx +502 -0
  115. package/src/components/index.ts +5 -0
  116. package/src/hooks/useCanvasZoom.ts +137 -0
  117. package/src/hooks/useFileSystemAccess.ts +123 -0
  118. package/src/hooks/useWireParser.ts +222 -0
  119. package/src/index.css +1 -3
  120. package/src/main.tsx +7 -5
  121. package/src/monaco/wireLanguage.ts +370 -0
  122. package/src/store/editorStore.ts +196 -0
  123. package/src/store/index.ts +2 -0
  124. package/tailwind.config.js +2 -1
  125. package/vite.config.ts +17 -0
  126. package/dist/assets/index-CHiOjnNN.js +0 -9
  127. package/dist/assets/index-CUIy2zPc.css +0 -1
  128. package/src/App.js +0 -4
  129. 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
+ };