@wire-dsl/web 0.0.7 → 0.1.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.
Files changed (127) hide show
  1. package/.turbo/turbo-build.log +103 -7
  2. package/CHANGELOG.md +30 -3
  3. package/dist/assets/abap-DLDM7-KI.js +1 -0
  4. package/dist/assets/apex-DNDY2TF8.js +1 -0
  5. package/dist/assets/azcli-Y6nb8tq_.js +1 -0
  6. package/dist/assets/bat-BwHxbl9M.js +1 -0
  7. package/dist/assets/bicep-CFznDFnq.js +2 -0
  8. package/dist/assets/cameligo-Bf6VGUru.js +1 -0
  9. package/dist/assets/clojure-Dnu-v4kV.js +1 -0
  10. package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  11. package/dist/assets/coffee-Bd8akH9Z.js +1 -0
  12. package/dist/assets/cpp-BbWJElDN.js +1 -0
  13. package/dist/assets/csharp-Co3qMtFm.js +1 -0
  14. package/dist/assets/csp-D-4FJmMZ.js +1 -0
  15. package/dist/assets/css-DdJfP1eB.js +3 -0
  16. package/dist/assets/css.worker-DBVD8oXr.js +93 -0
  17. package/dist/assets/cssMode-BgkzAyoH.js +1 -0
  18. package/dist/assets/cypher-cTPe9QuQ.js +1 -0
  19. package/dist/assets/dart-BOtBlQCF.js +1 -0
  20. package/dist/assets/dockerfile-BG73LgW2.js +1 -0
  21. package/dist/assets/ecl-BEgZUVRK.js +1 -0
  22. package/dist/assets/elixir-BkW5O-1t.js +1 -0
  23. package/dist/assets/flow9-BeJ5waoc.js +1 -0
  24. package/dist/assets/freemarker2-D05KrEgD.js +3 -0
  25. package/dist/assets/fsharp-PahG7c26.js +1 -0
  26. package/dist/assets/go-acbASCJo.js +1 -0
  27. package/dist/assets/graphql-BxJiqAUM.js +1 -0
  28. package/dist/assets/handlebars-CmNF6dIr.js +1 -0
  29. package/dist/assets/hcl-DtV1sZF8.js +1 -0
  30. package/dist/assets/html-DdFfMtqo.js +1 -0
  31. package/dist/assets/html.worker-CwpTb9lJ.js +470 -0
  32. package/dist/assets/htmlMode-BzENSv3x.js +1 -0
  33. package/dist/assets/index-C6yQ9VSx.js +1595 -0
  34. package/dist/assets/index-CmAJnnOw.css +1 -0
  35. package/dist/assets/ini-Kd9XrMLS.js +1 -0
  36. package/dist/assets/java-CXBNlu9o.js +1 -0
  37. package/dist/assets/javascript-BDq34vkg.js +1 -0
  38. package/dist/assets/json.worker-BoL8UZqY.js +58 -0
  39. package/dist/assets/jsonMode-xsVJWt4Q.js +7 -0
  40. package/dist/assets/julia-cl7-CwDS.js +1 -0
  41. package/dist/assets/kotlin-s7OhZKlX.js +1 -0
  42. package/dist/assets/less-9HpZscsL.js +2 -0
  43. package/dist/assets/lexon-OrD6JF1K.js +1 -0
  44. package/dist/assets/liquid-BKLduW-j.js +1 -0
  45. package/dist/assets/lspLanguageFeatures-DENz5XIL.js +4 -0
  46. package/dist/assets/lua-Cyyb5UIc.js +1 -0
  47. package/dist/assets/m3-B8OfTtLu.js +1 -0
  48. package/dist/assets/markdown-BFxVWTOG.js +1 -0
  49. package/dist/assets/mdx-Cpg3g8iv.js +1 -0
  50. package/dist/assets/mips-CiqrrVzr.js +1 -0
  51. package/dist/assets/msdax-DmeGPVcC.js +1 -0
  52. package/dist/assets/mysql-C_tMU-Nz.js +1 -0
  53. package/dist/assets/objective-c-BDtDVThU.js +1 -0
  54. package/dist/assets/pascal-vHIfCaH5.js +1 -0
  55. package/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
  56. package/dist/assets/perl-Ub6l9XKa.js +1 -0
  57. package/dist/assets/pgsql-BlNEE0v7.js +1 -0
  58. package/dist/assets/php-BBUBE1dy.js +1 -0
  59. package/dist/assets/pla-DSh2-awV.js +1 -0
  60. package/dist/assets/postiats-CocnycG-.js +1 -0
  61. package/dist/assets/powerquery-tScXyioY.js +1 -0
  62. package/dist/assets/powershell-COWaemsV.js +1 -0
  63. package/dist/assets/protobuf-Brw8urJB.js +2 -0
  64. package/dist/assets/pug-8SOpv6rk.js +1 -0
  65. package/dist/assets/python-Ca2JvAvf.js +1 -0
  66. package/dist/assets/qsharp-Bw9ernYp.js +1 -0
  67. package/dist/assets/r-j7ic8hl3.js +1 -0
  68. package/dist/assets/razor-B_xld0Yq.js +1 -0
  69. package/dist/assets/redis-Bu5POkcn.js +1 -0
  70. package/dist/assets/redshift-Bs9aos_-.js +1 -0
  71. package/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
  72. package/dist/assets/ruby-zBfavPgS.js +1 -0
  73. package/dist/assets/rust-BzKRNQWT.js +1 -0
  74. package/dist/assets/sb-BBc9UKZt.js +1 -0
  75. package/dist/assets/scala-D9hQfWCl.js +1 -0
  76. package/dist/assets/scheme-BPhDTwHR.js +1 -0
  77. package/dist/assets/scss-CBJaRo0y.js +3 -0
  78. package/dist/assets/shell-DiJ1NA_G.js +1 -0
  79. package/dist/assets/solidity-Db0IVjzk.js +1 -0
  80. package/dist/assets/sophia-CnS9iZB_.js +1 -0
  81. package/dist/assets/sparql-CJmd_6j2.js +1 -0
  82. package/dist/assets/sql-ClhHkBeG.js +1 -0
  83. package/dist/assets/st-CHwy0fLd.js +1 -0
  84. package/dist/assets/swift-CnmFD0ga.js +1 -0
  85. package/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
  86. package/dist/assets/tcl-Dm6ycUr_.js +1 -0
  87. package/dist/assets/ts.worker-BH9nVgjN.js +67718 -0
  88. package/dist/assets/tsMode-BinQkqy9.js +11 -0
  89. package/dist/assets/twig-Csy3S7wG.js +1 -0
  90. package/dist/assets/typescript-DQO38ZbJ.js +1 -0
  91. package/dist/assets/typespec-Btyra-wh.js +1 -0
  92. package/dist/assets/vb-Db0cS2oM.js +1 -0
  93. package/dist/assets/wgsl-BTesnYfV.js +298 -0
  94. package/dist/assets/xml-DF1bgZg2.js +1 -0
  95. package/dist/assets/yaml-BiNWh9S_.js +1 -0
  96. package/dist/examples/admin-dashboard.wire +95 -0
  97. package/dist/examples/analytics-dashboard.wire +65 -0
  98. package/dist/examples/form-example.wire +50 -0
  99. package/dist/examples/simple-dashboard.wire +40 -0
  100. package/dist/examples/simple-multi-screen.wire +30 -0
  101. package/dist/index.html +2 -2
  102. package/package.json +5 -3
  103. package/postcss.config.mjs +3 -1
  104. package/public/examples/admin-dashboard.wire +95 -0
  105. package/public/examples/analytics-dashboard.wire +65 -0
  106. package/public/examples/form-example.wire +50 -0
  107. package/public/examples/simple-dashboard.wire +40 -0
  108. package/public/examples/simple-multi-screen.wire +30 -0
  109. package/src/App.tsx +3 -13
  110. package/src/components/MonacoEditorComponent.tsx +112 -0
  111. package/src/components/WireLiveEditor.tsx +729 -0
  112. package/src/components/WireLiveHeader.tsx +469 -0
  113. package/src/components/index.ts +5 -0
  114. package/src/hooks/useCanvasZoom.ts +137 -0
  115. package/src/hooks/useFileSystemAccess.ts +123 -0
  116. package/src/hooks/useWireParser.ts +222 -0
  117. package/src/index.css +1 -3
  118. package/src/main.tsx +7 -5
  119. package/src/monaco/wireLanguage.ts +370 -0
  120. package/src/store/editorStore.ts +196 -0
  121. package/src/store/index.ts +2 -0
  122. package/tailwind.config.js +2 -1
  123. package/vite.config.ts +17 -0
  124. package/dist/assets/index-CHiOjnNN.js +0 -9
  125. package/dist/assets/index-CUIy2zPc.css +0 -1
  126. package/src/App.js +0 -4
  127. package/src/main.js +0 -6
@@ -0,0 +1,469 @@
1
+ import React from 'react';
2
+ import { ChevronDown, Download, FolderOpen, Plus, Save, FileDown } from 'lucide-react';
3
+
4
+ export interface WireLiveHeaderProps {
5
+ currentFileName: string;
6
+ isDirty: boolean;
7
+ onNew: () => void;
8
+ onOpen: () => void;
9
+ onSave: () => void;
10
+ onSaveAs?: () => void;
11
+ onRename: (newName: string) => void;
12
+ onExport: () => void;
13
+ onExampleSelect?: (exampleName: string) => void;
14
+ examples?: string[];
15
+ }
16
+
17
+ export const WireLiveHeader: React.FC<WireLiveHeaderProps> = ({
18
+ currentFileName,
19
+ isDirty,
20
+ onNew,
21
+ onOpen,
22
+ onSave,
23
+ onSaveAs,
24
+ onRename,
25
+ onExport,
26
+ onExampleSelect,
27
+ examples = [
28
+ 'simple-dashboard',
29
+ 'simple-multi-screen',
30
+ 'admin-dashboard',
31
+ 'form-example',
32
+ 'analytics-dashboard',
33
+ ],
34
+ }) => {
35
+ const [examplesOpen, setExamplesOpen] = React.useState(false);
36
+ const [isRenamingFile, setIsRenamingFile] = React.useState(false);
37
+ const [editingFileName, setEditingFileName] = React.useState(currentFileName);
38
+ const inputRef = React.useRef<HTMLInputElement>(null);
39
+
40
+ // Remove .wire extension from filename for display
41
+ const fileNameWithoutExt = React.useMemo(() => {
42
+ return currentFileName.endsWith('.wire') ? currentFileName.slice(0, -5) : currentFileName;
43
+ }, [currentFileName]);
44
+
45
+ React.useEffect(() => {
46
+ setEditingFileName(fileNameWithoutExt);
47
+ }, [fileNameWithoutExt]);
48
+
49
+ React.useEffect(() => {
50
+ if (isRenamingFile && inputRef.current) {
51
+ inputRef.current.focus();
52
+ inputRef.current.select();
53
+ }
54
+ }, [isRenamingFile]);
55
+
56
+ const handleRenameSubmit = () => {
57
+ if (editingFileName.trim()) {
58
+ const newNameWithoutExt = editingFileName.trim();
59
+ const newNameWithExt = newNameWithoutExt.endsWith('.wire')
60
+ ? newNameWithoutExt
61
+ : `${newNameWithoutExt}.wire`;
62
+
63
+ if (newNameWithExt !== currentFileName) {
64
+ onRename(newNameWithExt);
65
+ }
66
+ } else {
67
+ setEditingFileName(fileNameWithoutExt);
68
+ }
69
+ setIsRenamingFile(false);
70
+ };
71
+
72
+ const handleRenameKeyDown = (e: React.KeyboardEvent) => {
73
+ if (e.key === 'Enter') {
74
+ handleRenameSubmit();
75
+ } else if (e.key === 'Escape') {
76
+ setIsRenamingFile(false);
77
+ setEditingFileName(fileNameWithoutExt);
78
+ }
79
+ };
80
+
81
+ return (
82
+ <header
83
+ style={{
84
+ height: '64px',
85
+ backgroundColor: '#ffffff',
86
+ borderBottom: '1px solid #e5e7eb',
87
+ display: 'flex',
88
+ alignItems: 'center',
89
+ justifyContent: 'space-between',
90
+ paddingLeft: '24px',
91
+ paddingRight: '24px',
92
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
93
+ }}
94
+ >
95
+ <div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
96
+ {/* Logo y nombre */}
97
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
98
+ <div
99
+ style={{
100
+ width: '32px',
101
+ height: '32px',
102
+ backgroundColor: '#3b82f6',
103
+ borderRadius: '8px',
104
+ display: 'flex',
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ fontSize: '18px',
108
+ fontWeight: 'bold',
109
+ }}
110
+ >
111
+ <svg
112
+ xmlns="http://www.w3.org/2000/svg"
113
+ width="234"
114
+ height="242"
115
+ viewBox="0 0 234 242"
116
+ role="img"
117
+ aria-label="Wire-DSL logo"
118
+ >
119
+ <defs>
120
+ <radialGradient id="bgGrad" cx="30%" cy="25%" r="75%">
121
+ <stop offset="0%" stopColor="#FFFFAA" stopOpacity="1" />
122
+ <stop offset="55%" stopColor="#AAAAAA" stopOpacity="1" />
123
+ <stop offset="100%" stopColor="#AADDDD" stopOpacity="1" />
124
+ </radialGradient>
125
+
126
+ <radialGradient id="nodeGrad" cx="33%" cy="33%" r="75%">
127
+ <stop offset="0%" stopColor="#EEEEEE" stopOpacity="1" />
128
+ <stop offset="100%" stopColor="#AAAAAA" stopOpacity="1" />
129
+ </radialGradient>
130
+
131
+ <linearGradient id="strokeGrad" x1="0" y1="0" x2="1" y2="1">
132
+ <stop offset="0%" stopColor="#F4F4F4" stopOpacity="1" />
133
+ <stop offset="100%" stopColor="#DADADA" stopOpacity="1" />
134
+ </linearGradient>
135
+ </defs>
136
+
137
+ <g fill="none" strokeLinecap="round" strokeLinejoin="round" transform="translate(0, 30)">
138
+ <g>
139
+ <circle cx="44" cy="45" r="15" fill="url(#nodeGrad)" />
140
+ <circle cx="86" cy="136" r="15" fill="url(#nodeGrad)" />
141
+ <circle cx="117" cy="56" r="15" fill="url(#nodeGrad)" />
142
+ <circle cx="148" cy="136" r="15" fill="url(#nodeGrad)" />
143
+ <circle cx="190" cy="45" r="15" fill="url(#nodeGrad)" />
144
+ </g>
145
+
146
+ <path
147
+ d="M 44 45 L 86 136 L 117 56 L 148 136 L 190 45"
148
+ stroke="url(#strokeGrad)"
149
+ strokeWidth="18"
150
+ />
151
+ </g>
152
+ </svg>
153
+ </div>
154
+ <h1 style={{ fontSize: '18px', fontWeight: '600', color: '#111827', margin: 0 }}>
155
+ Wire Live
156
+ </h1>
157
+ <span
158
+ style={{
159
+ fontSize: '11px',
160
+ backgroundColor: '#f3f4f6',
161
+ color: '#4b5563',
162
+ paddingLeft: '8px',
163
+ paddingRight: '8px',
164
+ paddingTop: '4px',
165
+ paddingBottom: '4px',
166
+ borderRadius: '6px',
167
+ fontWeight: '500',
168
+ }}
169
+ >
170
+ open-source
171
+ </span>
172
+ </div>
173
+
174
+ {/* Separador */}
175
+ <div style={{ width: '1px', height: '32px', backgroundColor: '#e5e7eb' }} />
176
+
177
+ {/* Archivo actual */}
178
+ <div
179
+ style={{
180
+ fontSize: '14px',
181
+ color: '#4b5563',
182
+ display: 'flex',
183
+ alignItems: 'center',
184
+ gap: '8px',
185
+ }}
186
+ >
187
+ {isRenamingFile ? (
188
+ <input
189
+ ref={inputRef}
190
+ type="text"
191
+ value={editingFileName}
192
+ onChange={(e) => setEditingFileName(e.target.value)}
193
+ onBlur={handleRenameSubmit}
194
+ onKeyDown={handleRenameKeyDown}
195
+ style={{
196
+ fontSize: '14px',
197
+ fontWeight: '500',
198
+ color: '#111827',
199
+ padding: '4px 8px',
200
+ border: '2px solid #3b82f6',
201
+ borderRadius: '4px',
202
+ fontFamily: 'inherit',
203
+ outline: 'none',
204
+ }}
205
+ />
206
+ ) : (
207
+ <span
208
+ onClick={() => setIsRenamingFile(true)}
209
+ style={{
210
+ fontWeight: '500',
211
+ color: '#111827',
212
+ cursor: 'pointer',
213
+ padding: '4px 6px',
214
+ borderRadius: '4px',
215
+ transition: 'background-color 0.2s',
216
+ }}
217
+ onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#f3f4f6')}
218
+ onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
219
+ title="Click to rename"
220
+ >
221
+ {currentFileName}
222
+ </span>
223
+ )}
224
+ {isDirty && <span style={{ color: '#f59e0b', fontSize: '10px' }}>● unsaved</span>}
225
+ </div>
226
+ </div>
227
+
228
+ {/* Botones de acción */}
229
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
230
+ {/* Nuevo */}
231
+ <button
232
+ onClick={onNew}
233
+ style={{
234
+ display: 'flex',
235
+ alignItems: 'center',
236
+ gap: '8px',
237
+ paddingLeft: '12px',
238
+ paddingRight: '12px',
239
+ paddingTop: '8px',
240
+ paddingBottom: '8px',
241
+ fontSize: '14px',
242
+ fontWeight: '500',
243
+ color: '#374151',
244
+ backgroundColor: 'transparent',
245
+ border: 'none',
246
+ borderRadius: '6px',
247
+ cursor: 'pointer',
248
+ transition: 'background-color 0.2s',
249
+ }}
250
+ onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#f3f4f6')}
251
+ onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
252
+ title="Nuevo archivo (Ctrl+N)"
253
+ >
254
+ <Plus size={16} />
255
+ New
256
+ </button>
257
+
258
+ {/* Abrir */}
259
+ <button
260
+ onClick={onOpen}
261
+ style={{
262
+ display: 'flex',
263
+ alignItems: 'center',
264
+ gap: '8px',
265
+ paddingLeft: '12px',
266
+ paddingRight: '12px',
267
+ paddingTop: '8px',
268
+ paddingBottom: '8px',
269
+ fontSize: '14px',
270
+ fontWeight: '500',
271
+ color: '#374151',
272
+ backgroundColor: 'transparent',
273
+ border: 'none',
274
+ borderRadius: '6px',
275
+ cursor: 'pointer',
276
+ transition: 'background-color 0.2s',
277
+ }}
278
+ onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#f3f4f6')}
279
+ onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
280
+ title="Abrir archivo (Ctrl+O)"
281
+ >
282
+ <FolderOpen size={16} />
283
+ Open
284
+ </button>
285
+
286
+ {/* Guardar */}
287
+ <button
288
+ onClick={onSave}
289
+ style={{
290
+ display: 'flex',
291
+ alignItems: 'center',
292
+ gap: '8px',
293
+ paddingLeft: '12px',
294
+ paddingRight: '12px',
295
+ paddingTop: '8px',
296
+ paddingBottom: '8px',
297
+ fontSize: '14px',
298
+ fontWeight: '500',
299
+ color: isDirty ? '#3b82f6' : '#374151',
300
+ backgroundColor: isDirty ? '#eff6ff' : 'transparent',
301
+ border: isDirty ? '1px solid #93c5fd' : 'none',
302
+ borderRadius: '6px',
303
+ cursor: 'pointer',
304
+ transition: 'all 0.2s',
305
+ }}
306
+ onMouseEnter={(e) => {
307
+ if (isDirty) {
308
+ e.currentTarget.style.backgroundColor = '#dbeafe';
309
+ } else {
310
+ e.currentTarget.style.backgroundColor = '#f3f4f6';
311
+ }
312
+ }}
313
+ onMouseLeave={(e) => {
314
+ e.currentTarget.style.backgroundColor = isDirty ? '#eff6ff' : 'transparent';
315
+ }}
316
+ title="Guardar archivo (Ctrl+S)"
317
+ >
318
+ <Save size={16} />
319
+ Save
320
+ </button>
321
+
322
+ {/* Guardar Como */}
323
+ {onSaveAs && (
324
+ <button
325
+ onClick={onSaveAs}
326
+ style={{
327
+ display: 'flex',
328
+ alignItems: 'center',
329
+ gap: '8px',
330
+ paddingLeft: '12px',
331
+ paddingRight: '12px',
332
+ paddingTop: '8px',
333
+ paddingBottom: '8px',
334
+ fontSize: '14px',
335
+ fontWeight: '500',
336
+ color: '#374151',
337
+ backgroundColor: 'transparent',
338
+ border: 'none',
339
+ borderRadius: '6px',
340
+ cursor: 'pointer',
341
+ transition: 'background-color 0.2s',
342
+ }}
343
+ onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#f3f4f6')}
344
+ onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
345
+ title="Guardar como (Ctrl+Shift+S)"
346
+ >
347
+ <FileDown size={16} />
348
+ Save As
349
+ </button>
350
+ )}
351
+
352
+ {/* Ejemplos */}
353
+ <div style={{ position: 'relative' }}>
354
+ <button
355
+ onClick={() => setExamplesOpen(!examplesOpen)}
356
+ style={{
357
+ display: 'flex',
358
+ alignItems: 'center',
359
+ gap: '8px',
360
+ paddingLeft: '12px',
361
+ paddingRight: '12px',
362
+ paddingTop: '8px',
363
+ paddingBottom: '8px',
364
+ fontSize: '14px',
365
+ fontWeight: '500',
366
+ color: '#374151',
367
+ backgroundColor: 'transparent',
368
+ border: 'none',
369
+ borderRadius: '6px',
370
+ cursor: 'pointer',
371
+ transition: 'background-color 0.2s',
372
+ }}
373
+ onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#f3f4f6')}
374
+ onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
375
+ >
376
+ Examples
377
+ <ChevronDown
378
+ size={16}
379
+ style={{
380
+ transform: examplesOpen ? 'rotate(180deg)' : 'rotate(0deg)',
381
+ transition: 'transform 0.2s',
382
+ }}
383
+ />
384
+ </button>
385
+
386
+ {examplesOpen && (
387
+ <div
388
+ style={{
389
+ position: 'absolute',
390
+ top: '100%',
391
+ right: '0',
392
+ marginTop: '8px',
393
+ backgroundColor: '#ffffff',
394
+ border: '1px solid #e5e7eb',
395
+ borderRadius: '8px',
396
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
397
+ zIndex: 1000,
398
+ minWidth: '160px',
399
+ }}
400
+ >
401
+ {examples.map((example, index) => (
402
+ <button
403
+ key={example}
404
+ onClick={() => {
405
+ onExampleSelect?.(example);
406
+ setExamplesOpen(false);
407
+ }}
408
+ style={{
409
+ width: '100%',
410
+ textAlign: 'left',
411
+ paddingLeft: '16px',
412
+ paddingRight: '16px',
413
+ paddingTop: '10px',
414
+ paddingBottom: '10px',
415
+ fontSize: '14px',
416
+ color: '#374151',
417
+ backgroundColor: 'transparent',
418
+ border: 'none',
419
+ cursor: 'pointer',
420
+ transition: 'background-color 0.2s',
421
+ borderTopLeftRadius: index === 0 ? '8px' : '0px',
422
+ borderTopRightRadius: index === 0 ? '8px' : '0px',
423
+ borderBottomLeftRadius: index === examples.length - 1 ? '8px' : '0px',
424
+ borderBottomRightRadius: index === examples.length - 1 ? '8px' : '0px',
425
+ }}
426
+ onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#f9fafb')}
427
+ onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
428
+ >
429
+ {example}
430
+ </button>
431
+ ))}
432
+ </div>
433
+ )}
434
+ </div>
435
+
436
+ {/* Separador */}
437
+ <div style={{ width: '1px', height: '32px', backgroundColor: '#e5e7eb' }} />
438
+
439
+ {/* Exportar */}
440
+ <button
441
+ onClick={onExport}
442
+ style={{
443
+ display: 'flex',
444
+ alignItems: 'center',
445
+ gap: '8px',
446
+ paddingLeft: '16px',
447
+ paddingRight: '16px',
448
+ paddingTop: '8px',
449
+ paddingBottom: '8px',
450
+ fontSize: '14px',
451
+ fontWeight: '600',
452
+ color: '#ffffff',
453
+ backgroundColor: '#3b82f6',
454
+ border: 'none',
455
+ borderRadius: '6px',
456
+ cursor: 'pointer',
457
+ transition: 'background-color 0.2s',
458
+ }}
459
+ onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#2563eb')}
460
+ onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = '#3b82f6')}
461
+ title="Exportar como SVG"
462
+ >
463
+ <Download size={16} />
464
+ Export SVG
465
+ </button>
466
+ </div>
467
+ </header>
468
+ );
469
+ };
@@ -0,0 +1,5 @@
1
+ export { WireLiveEditor } from './WireLiveEditor';
2
+ export { WireLiveHeader } from './WireLiveHeader';
3
+ export { MonacoEditorComponent } from './MonacoEditorComponent';
4
+ export type { MonacoEditorProps } from './MonacoEditorComponent';
5
+ export type { WireLiveHeaderProps } from './WireLiveHeader';
@@ -0,0 +1,137 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+
3
+ export interface CanvasZoomState {
4
+ zoom: number;
5
+ fitZoom: number;
6
+ }
7
+
8
+ /**
9
+ * Canvas zoom hook - manages zoom level exactly like VS Code extension
10
+ * - Stores original SVG dimensions (extracted from DOM, not prop)
11
+ * - Calculates fitZoom based on available space when preview ref is ready
12
+ * - Defaults to fitZoom on mount
13
+ * - Uses scale multiplier for zoom calculations
14
+ */
15
+ export const useCanvasZoom = () => {
16
+ const [state, setState] = useState<CanvasZoomState>({
17
+ zoom: 1,
18
+ fitZoom: 1,
19
+ });
20
+
21
+ const previewRef = useRef<HTMLDivElement | null>(null);
22
+ const svgOriginalWidthRef = useRef<number>(0);
23
+ const svgOriginalHeightRef = useRef<number>(0);
24
+
25
+ /**
26
+ * Calculate fit zoom based on available preview space
27
+ * Same formula as VS Code extension
28
+ */
29
+ const calculateFitZoom = useCallback(() => {
30
+ if (!previewRef.current || svgOriginalWidthRef.current <= 0 || svgOriginalHeightRef.current <= 0) {
31
+ return 1;
32
+ }
33
+
34
+ const padding = 40;
35
+ const availWidth = previewRef.current.clientWidth - padding;
36
+ const availHeight = previewRef.current.clientHeight - padding;
37
+
38
+ const fit = Math.min(
39
+ availWidth / svgOriginalWidthRef.current,
40
+ availHeight / svgOriginalHeightRef.current
41
+ );
42
+
43
+ // Return fit value clamped to [0.1, 1]
44
+ return Math.min(fit, 1);
45
+ }, []);
46
+
47
+ /**
48
+ * Extract original SVG dimensions from DOM and initialize zoom
49
+ * Called when SVG is first rendered. Only initializes zoom on first call.
50
+ * Subsequent calls only update original dimensions.
51
+ */
52
+ const extractAndInitialize = useCallback(() => {
53
+ if (!previewRef.current) return;
54
+
55
+ const svgElement = previewRef.current.querySelector('svg');
56
+ if (!svgElement) return;
57
+
58
+ // Extract width and height from SVG attributes (set by renderToSVG)
59
+ const width = parseFloat(svgElement.getAttribute('width') || '0');
60
+ const height = parseFloat(svgElement.getAttribute('height') || '0');
61
+
62
+ if (width > 0 && height > 0) {
63
+ const isFirstTime = svgOriginalWidthRef.current === 0;
64
+
65
+ svgOriginalWidthRef.current = width;
66
+ svgOriginalHeightRef.current = height;
67
+
68
+ // Only initialize zoom on first extraction
69
+ if (isFirstTime) {
70
+ const fitZoomValue = calculateFitZoom();
71
+ setState({
72
+ zoom: fitZoomValue,
73
+ fitZoom: fitZoomValue,
74
+ });
75
+ } else {
76
+ // Subsequent calls: update fitZoom in case viewport changed, but keep zoom as-is
77
+ const fitZoomValue = calculateFitZoom();
78
+ setState((prev) => ({
79
+ ...prev,
80
+ fitZoom: fitZoomValue,
81
+ }));
82
+ }
83
+ }
84
+ }, [calculateFitZoom]);
85
+
86
+
87
+
88
+ const zoomIn = useCallback(() => {
89
+ setState((prev) => ({
90
+ ...prev,
91
+ zoom: Math.min(prev.zoom + 0.1, 3),
92
+ }));
93
+ }, []);
94
+
95
+ const zoomOut = useCallback(() => {
96
+ setState((prev) => ({
97
+ ...prev,
98
+ zoom: Math.max(prev.zoom - 0.1, 0.1),
99
+ }));
100
+ }, []);
101
+
102
+ const resetZoom = useCallback(() => {
103
+ setState((prev) => ({
104
+ ...prev,
105
+ zoom: prev.fitZoom,
106
+ }));
107
+ }, []);
108
+
109
+ const handleWheel = useCallback(
110
+ (e: React.WheelEvent<HTMLDivElement>) => {
111
+ if (!e.ctrlKey && !e.metaKey) return;
112
+
113
+ e.preventDefault();
114
+ const direction = e.deltaY > 0 ? -1 : 1;
115
+ setState((prev) => ({
116
+ ...prev,
117
+ zoom: Math.max(0.1, Math.min(prev.zoom + 0.1 * direction, 3)),
118
+ }));
119
+ },
120
+ []
121
+ );
122
+
123
+ return {
124
+ zoom: state.zoom,
125
+ fitZoom: state.fitZoom,
126
+ originalWidth: svgOriginalWidthRef.current,
127
+ originalHeight: svgOriginalHeightRef.current,
128
+ zoomIn,
129
+ zoomOut,
130
+ resetZoom,
131
+ handleWheel,
132
+ setPreviewRef: (ref: HTMLDivElement | null) => {
133
+ previewRef.current = ref;
134
+ },
135
+ extractAndInitialize,
136
+ };
137
+ };