@flow.os/style 0.0.1-dev.1771785969 → 0.0.1-dev.1771840262
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/index.ts +29 -15
- package/package.json +6 -1
- package/react.ts +68 -0
- package/resolve.ts +44 -1
- package/server.ts +299 -0
- package/shorthand.ts +34 -0
- package/style-builder/button.ts +41 -0
- package/style-builder/constants.ts +16 -0
- package/style-builder/dom.ts +18 -0
- package/style-builder/index.ts +48 -0
- package/style-builder/panel.ts +69 -0
- package/style-builder/position.ts +25 -0
- package/visual-builder.ts +822 -0
- package/vite-plugin.ts +86 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Style Builder – vanilla DOM.
|
|
3
|
+
* Con previewIframe: applicazione istantanea nel preview (zero latency) + patch al file in background.
|
|
4
|
+
* Senza: solo patch al file. Apri /builder.html per l’esperienza con iframe (nessun refresh).
|
|
5
|
+
*/
|
|
6
|
+
import { SHORTHAND_MAP, CSS_TO_SHORTHAND } from './shorthand.js';
|
|
7
|
+
import type { StyleShorthandKey } from './shorthand.js';
|
|
8
|
+
|
|
9
|
+
const PANEL_WIDTH = 300;
|
|
10
|
+
const FLOW_STYLE_BUILDER_BUTTON_ID = 'flowStyleBuilder';
|
|
11
|
+
const PANEL_ID = 'flowStyleBuilderPanel';
|
|
12
|
+
const HIGHLIGHT_ID = 'flowStyleBuilderHighlight';
|
|
13
|
+
const OVERLAY_ID = 'flowStyleBuilderOverlay';
|
|
14
|
+
const PATCH_SERVER_URL = 'http://127.0.0.1:3757';
|
|
15
|
+
const DEBOUNCE_MS = 300;
|
|
16
|
+
const STORAGE_KEY_FILE = 'flowStyleBuilderFile';
|
|
17
|
+
const STORAGE_KEY_LINE = 'flowStyleBuilderLine';
|
|
18
|
+
|
|
19
|
+
const THEME = {
|
|
20
|
+
bg: '#0f1419',
|
|
21
|
+
surface: '#1a1f26',
|
|
22
|
+
border: '#2d3748',
|
|
23
|
+
accent: '#14b8a6',
|
|
24
|
+
accentDim: '#0d9488',
|
|
25
|
+
text: '#e2e8f0',
|
|
26
|
+
muted: '#94a3b8',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export type VisualStyleBuilderOptions = {
|
|
30
|
+
/** Iframe che mostra l’app (stesso origin). Se presente: feedback istantaneo nel preview + nessun refresh. */
|
|
31
|
+
previewIframe?: HTMLIFrameElement;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type Overrides = Partial<Record<StyleShorthandKey | string, number | string>>;
|
|
35
|
+
|
|
36
|
+
const GROUPS: { label: string; keys: (keyof typeof SHORTHAND_MAP)[] }[] = [
|
|
37
|
+
{ label: 'Dimensioni', keys: ['w', 'h', 'minW', 'minH', 'maxW', 'maxH'] },
|
|
38
|
+
{ label: 'Tipografia & colore', keys: ['text', 'color', 'bg'] },
|
|
39
|
+
{ label: 'Layout', keys: ['display', 'flex', 'gap'] },
|
|
40
|
+
{ label: 'Bordo & effetto', keys: ['rounded', 'opacity', 'scale'] },
|
|
41
|
+
{ label: 'Posizione', keys: ['top', 'left', 'right', 'bottom'] },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const NUM_KEYS = new Set([
|
|
45
|
+
'p', 'pt', 'pr', 'pb', 'pl', 'm', 'mt', 'mr', 'mb', 'ml',
|
|
46
|
+
'w', 'h', 'minW', 'minH', 'maxW', 'maxH', 'gap', 'rounded', 'text', 'opacity',
|
|
47
|
+
'top', 'left', 'right', 'bottom',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function getCssProp(shorthand: string): string | undefined {
|
|
51
|
+
return shorthand in SHORTHAND_MAP ? (SHORTHAND_MAP as Record<string, string>)[shorthand] : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function applyToElement(el: HTMLElement, shorthand: string, value: number | string): void {
|
|
55
|
+
const cssProp = getCssProp(shorthand);
|
|
56
|
+
if (!cssProp) return;
|
|
57
|
+
if (cssProp === 'transform' && typeof value === 'number') {
|
|
58
|
+
(el.style as unknown as Record<string, string>)['transform'] = `scale(${value})`;
|
|
59
|
+
} else if (typeof value === 'number') {
|
|
60
|
+
(el.style as unknown as Record<string, string>)[cssProp] = `${value}px`;
|
|
61
|
+
} else {
|
|
62
|
+
(el.style as unknown as Record<string, string>)[cssProp] = String(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parsePx(value: string): number | string {
|
|
67
|
+
if (value === '') return '';
|
|
68
|
+
const n = parseFloat(value);
|
|
69
|
+
return !Number.isNaN(n) ? n : value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function overridesToJson(overrides: Overrides): string {
|
|
73
|
+
const obj: Record<string, number | string> = {};
|
|
74
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
75
|
+
if (v !== '' && v !== undefined) obj[k] = v;
|
|
76
|
+
}
|
|
77
|
+
return JSON.stringify(obj, null, 2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setStyles(el: HTMLElement, s: Partial<CSSStyleDeclaration>): void {
|
|
81
|
+
for (const [k, v] of Object.entries(s)) {
|
|
82
|
+
if (v != null && typeof v === 'string') (el.style as unknown as Record<string, string>)[k] = v;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createEl<K extends keyof HTMLElementTagNameMap>(
|
|
87
|
+
tag: K,
|
|
88
|
+
style?: Partial<CSSStyleDeclaration>,
|
|
89
|
+
attrs?: Record<string, string>
|
|
90
|
+
): HTMLElementTagNameMap[K] {
|
|
91
|
+
const el = document.createElement(tag);
|
|
92
|
+
if (style) setStyles(el as unknown as HTMLElement, style);
|
|
93
|
+
if (attrs) for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
|
|
94
|
+
return el;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function initVisualStyleBuilder(options: VisualStyleBuilderOptions = {}): void {
|
|
98
|
+
if (document.getElementById(FLOW_STYLE_BUILDER_BUTTON_ID) || document.getElementById(PANEL_ID)) return;
|
|
99
|
+
|
|
100
|
+
const { previewIframe } = options;
|
|
101
|
+
let overrides: Overrides = {};
|
|
102
|
+
let currentFile = '';
|
|
103
|
+
let currentLine: number | null = null;
|
|
104
|
+
let debounceTimer = 0;
|
|
105
|
+
|
|
106
|
+
function getPreviewDoc(): Document | null {
|
|
107
|
+
return previewIframe?.contentDocument ?? document;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function applyOverridesToPreview(): void {
|
|
111
|
+
const doc = getPreviewDoc();
|
|
112
|
+
if (!doc || currentLine == null || !currentFile) return;
|
|
113
|
+
const sel = `[data-flow-style-line="${currentLine}"][data-flow-style-file="${currentFile}"]`;
|
|
114
|
+
const el = doc.querySelector(sel);
|
|
115
|
+
if (!el || !(el instanceof HTMLElement)) return;
|
|
116
|
+
for (const [key, v] of Object.entries(overrides)) {
|
|
117
|
+
if (v !== '' && v !== undefined) applyToElement(el, key, v);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let panelRight = true;
|
|
122
|
+
let activeTab: 'select' | 'elements' | 'style' = 'elements';
|
|
123
|
+
let hoverEl: HTMLElement | null = null;
|
|
124
|
+
|
|
125
|
+
const panel = createEl('div', {
|
|
126
|
+
position: 'fixed',
|
|
127
|
+
top: '0',
|
|
128
|
+
right: '0',
|
|
129
|
+
width: `${PANEL_WIDTH}px`,
|
|
130
|
+
height: '100vh',
|
|
131
|
+
background: THEME.bg,
|
|
132
|
+
color: THEME.text,
|
|
133
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
134
|
+
fontSize: '12px',
|
|
135
|
+
boxShadow: '-4px 0 24px rgba(0,0,0,0.5)',
|
|
136
|
+
display: 'flex',
|
|
137
|
+
flexDirection: 'column',
|
|
138
|
+
zIndex: '2147483646',
|
|
139
|
+
overflow: 'hidden',
|
|
140
|
+
});
|
|
141
|
+
panel.id = PANEL_ID;
|
|
142
|
+
|
|
143
|
+
const highlight = createEl('div', {
|
|
144
|
+
position: 'fixed',
|
|
145
|
+
pointerEvents: 'none',
|
|
146
|
+
border: `2px solid ${THEME.accent}`,
|
|
147
|
+
borderRadius: '4px',
|
|
148
|
+
boxSizing: 'border-box',
|
|
149
|
+
zIndex: '2147483645',
|
|
150
|
+
});
|
|
151
|
+
highlight.id = HIGHLIGHT_ID;
|
|
152
|
+
highlight.style.display = 'none';
|
|
153
|
+
document.body.appendChild(highlight);
|
|
154
|
+
|
|
155
|
+
const overlay = createEl('div', {
|
|
156
|
+
position: 'fixed',
|
|
157
|
+
top: '0',
|
|
158
|
+
left: '0',
|
|
159
|
+
right: '0',
|
|
160
|
+
bottom: '0',
|
|
161
|
+
zIndex: '2147483644',
|
|
162
|
+
cursor: 'crosshair',
|
|
163
|
+
});
|
|
164
|
+
overlay.id = OVERLAY_ID;
|
|
165
|
+
overlay.style.display = 'none';
|
|
166
|
+
document.body.appendChild(overlay);
|
|
167
|
+
|
|
168
|
+
function setPanelSide(right: boolean): void {
|
|
169
|
+
panelRight = right;
|
|
170
|
+
const s = panel.style as unknown as Record<string, string>;
|
|
171
|
+
s['right'] = right ? '0' : '';
|
|
172
|
+
s['left'] = right ? '' : '0';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const header = createEl('header', {
|
|
176
|
+
padding: '10px 12px',
|
|
177
|
+
borderBottom: `1px solid ${THEME.border}`,
|
|
178
|
+
display: 'flex',
|
|
179
|
+
flexDirection: 'column',
|
|
180
|
+
gap: '10px',
|
|
181
|
+
flexShrink: '0',
|
|
182
|
+
background: THEME.surface,
|
|
183
|
+
});
|
|
184
|
+
const headerRow = createEl('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between' });
|
|
185
|
+
const positionBtn = createEl('button', {
|
|
186
|
+
padding: '6px',
|
|
187
|
+
border: `1px solid ${THEME.border}`,
|
|
188
|
+
borderRadius: '6px',
|
|
189
|
+
background: 'transparent',
|
|
190
|
+
color: THEME.muted,
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
}) as HTMLButtonElement;
|
|
193
|
+
positionBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>';
|
|
194
|
+
positionBtn.title = 'Sposta pannello a sinistra';
|
|
195
|
+
positionBtn.addEventListener('click', () => {
|
|
196
|
+
setPanelSide(!panelRight);
|
|
197
|
+
positionBtn.innerHTML = panelRight
|
|
198
|
+
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>'
|
|
199
|
+
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';
|
|
200
|
+
positionBtn.title = panelRight ? 'Sposta pannello a sinistra' : 'Sposta pannello a destra';
|
|
201
|
+
});
|
|
202
|
+
headerRow.appendChild(positionBtn);
|
|
203
|
+
header.appendChild(headerRow);
|
|
204
|
+
|
|
205
|
+
const tabBar = createEl('div', { display: 'flex', gap: '4px' });
|
|
206
|
+
const tabs = [
|
|
207
|
+
{ id: 'select' as const, label: 'Seleziona', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/></svg>' },
|
|
208
|
+
{ id: 'elements' as const, label: 'Elementi', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>' },
|
|
209
|
+
{ id: 'style' as const, label: 'Stile', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>' },
|
|
210
|
+
];
|
|
211
|
+
const tabPanels = { select: createEl('div'), elements: createEl('div'), style: createEl('div') };
|
|
212
|
+
tabPanels.select.style.display = 'none';
|
|
213
|
+
tabPanels.style.style.display = 'none';
|
|
214
|
+
tabs.forEach((t) => {
|
|
215
|
+
const btn = createEl('button', {
|
|
216
|
+
flex: '1',
|
|
217
|
+
padding: '8px 6px',
|
|
218
|
+
border: 'none',
|
|
219
|
+
borderRadius: '6px',
|
|
220
|
+
background: activeTab === t.id ? THEME.accent : 'transparent',
|
|
221
|
+
color: activeTab === t.id ? THEME.bg : THEME.muted,
|
|
222
|
+
fontSize: '11px',
|
|
223
|
+
fontWeight: '600',
|
|
224
|
+
cursor: 'pointer',
|
|
225
|
+
display: 'flex',
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
justifyContent: 'center',
|
|
228
|
+
gap: '4px',
|
|
229
|
+
}) as HTMLButtonElement;
|
|
230
|
+
btn.innerHTML = t.icon + ' ' + t.label;
|
|
231
|
+
btn.addEventListener('click', () => {
|
|
232
|
+
activeTab = t.id;
|
|
233
|
+
tabs.forEach((_, i) => {
|
|
234
|
+
const b = tabBar.children[i];
|
|
235
|
+
if (b instanceof HTMLElement) {
|
|
236
|
+
b.style.background = tabs[i]?.id === activeTab ? THEME.accent : 'transparent';
|
|
237
|
+
b.style.color = tabs[i]?.id === activeTab ? THEME.bg : THEME.muted;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
(Object.keys(tabPanels) as (keyof typeof tabPanels)[]).forEach((k) => {
|
|
241
|
+
tabPanels[k].style.display = k === activeTab ? 'flex' : 'none';
|
|
242
|
+
});
|
|
243
|
+
tabPanels[activeTab].style.flexDirection = 'column';
|
|
244
|
+
tabPanels[activeTab].style.flex = '1';
|
|
245
|
+
tabPanels[activeTab].style.overflow = 'auto';
|
|
246
|
+
if (activeTab === 'select') {
|
|
247
|
+
overlay.style.display = '';
|
|
248
|
+
highlight.style.display = '';
|
|
249
|
+
} else {
|
|
250
|
+
overlay.style.display = 'none';
|
|
251
|
+
highlight.style.display = 'none';
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
tabBar.appendChild(btn);
|
|
255
|
+
});
|
|
256
|
+
header.appendChild(tabBar);
|
|
257
|
+
panel.appendChild(header);
|
|
258
|
+
|
|
259
|
+
tabPanels.elements.style.flexDirection = 'column';
|
|
260
|
+
tabPanels.elements.style.flex = '1';
|
|
261
|
+
tabPanels.elements.style.overflow = 'auto';
|
|
262
|
+
const fileRow = createEl('div', { padding: '10px 12px', borderBottom: `1px solid ${THEME.border}`, display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' });
|
|
263
|
+
const fileInput = createEl('input', {
|
|
264
|
+
flex: '1',
|
|
265
|
+
minWidth: '80px',
|
|
266
|
+
padding: '8px 10px',
|
|
267
|
+
border: `1px solid ${THEME.border}`,
|
|
268
|
+
borderRadius: '6px',
|
|
269
|
+
background: THEME.surface,
|
|
270
|
+
color: THEME.text,
|
|
271
|
+
fontSize: '12px',
|
|
272
|
+
}) as HTMLInputElement;
|
|
273
|
+
fileInput.placeholder = 'client/routes/index.tsx';
|
|
274
|
+
fileInput.value = 'client/routes/index.tsx';
|
|
275
|
+
const loadBtn = createEl('button', {
|
|
276
|
+
padding: '8px 14px',
|
|
277
|
+
border: 'none',
|
|
278
|
+
borderRadius: '6px',
|
|
279
|
+
fontSize: '12px',
|
|
280
|
+
fontWeight: '500',
|
|
281
|
+
cursor: 'pointer',
|
|
282
|
+
background: THEME.accent,
|
|
283
|
+
color: THEME.bg,
|
|
284
|
+
}) as HTMLButtonElement;
|
|
285
|
+
loadBtn.textContent = 'Carica';
|
|
286
|
+
fileRow.appendChild(fileInput);
|
|
287
|
+
fileRow.appendChild(loadBtn);
|
|
288
|
+
tabPanels.elements.appendChild(fileRow);
|
|
289
|
+
const blocksSection = createEl('div', { padding: '10px 12px', maxHeight: '160px', overflow: 'auto' });
|
|
290
|
+
const blocksLabel = createEl('div', { fontSize: '10px', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em', color: THEME.muted, marginBottom: '8px' });
|
|
291
|
+
blocksLabel.textContent = 'Blocchi s= / styleFlow=';
|
|
292
|
+
blocksSection.appendChild(blocksLabel);
|
|
293
|
+
const blocksList = createEl('div', { display: 'flex', flexDirection: 'column', gap: '6px' });
|
|
294
|
+
blocksSection.appendChild(blocksList);
|
|
295
|
+
tabPanels.elements.appendChild(blocksSection);
|
|
296
|
+
|
|
297
|
+
const selectHint = createEl('div', { padding: '16px', color: THEME.muted, fontSize: '13px', lineHeight: '1.5' });
|
|
298
|
+
selectHint.textContent = 'Passa col mouse sugli elementi per evidenziarli. Clicca su un elemento con s= o styleFlow= per modificarlo.';
|
|
299
|
+
tabPanels.select.appendChild(selectHint);
|
|
300
|
+
|
|
301
|
+
panel.appendChild(tabPanels.select);
|
|
302
|
+
panel.appendChild(tabPanels.elements);
|
|
303
|
+
panel.appendChild(tabPanels.style);
|
|
304
|
+
|
|
305
|
+
const scroll = createEl('div', { flex: '1', overflow: 'auto', padding: '12px' });
|
|
306
|
+
scroll.style.display = 'none';
|
|
307
|
+
tabPanels.style.appendChild(scroll);
|
|
308
|
+
|
|
309
|
+
const footer = createEl('div', {
|
|
310
|
+
padding: '12px',
|
|
311
|
+
borderTop: `1px solid ${THEME.border}`,
|
|
312
|
+
display: 'flex',
|
|
313
|
+
flexDirection: 'column',
|
|
314
|
+
gap: '8px',
|
|
315
|
+
background: THEME.surface,
|
|
316
|
+
});
|
|
317
|
+
const btnRow = createEl('div', { display: 'flex', gap: '8px', flexWrap: 'wrap' });
|
|
318
|
+
const copyObjBtn = createEl('button', {
|
|
319
|
+
padding: '8px 12px',
|
|
320
|
+
border: `1px solid ${THEME.border}`,
|
|
321
|
+
borderRadius: '6px',
|
|
322
|
+
fontSize: '12px',
|
|
323
|
+
fontWeight: '500',
|
|
324
|
+
cursor: 'pointer',
|
|
325
|
+
background: THEME.surface,
|
|
326
|
+
color: THEME.muted,
|
|
327
|
+
}) as HTMLButtonElement;
|
|
328
|
+
copyObjBtn.textContent = 'Copia oggetto';
|
|
329
|
+
const copyPropBtn = createEl('button', {
|
|
330
|
+
padding: '8px 12px',
|
|
331
|
+
border: `1px solid ${THEME.border}`,
|
|
332
|
+
borderRadius: '6px',
|
|
333
|
+
fontSize: '12px',
|
|
334
|
+
fontWeight: '500',
|
|
335
|
+
cursor: 'pointer',
|
|
336
|
+
background: THEME.surface,
|
|
337
|
+
color: THEME.muted,
|
|
338
|
+
}) as HTMLButtonElement;
|
|
339
|
+
copyPropBtn.textContent = 'Copia s={}';
|
|
340
|
+
btnRow.appendChild(copyObjBtn);
|
|
341
|
+
btnRow.appendChild(copyPropBtn);
|
|
342
|
+
footer.appendChild(btnRow);
|
|
343
|
+
footer.style.display = 'none';
|
|
344
|
+
tabPanels.style.appendChild(footer);
|
|
345
|
+
|
|
346
|
+
const emptyState = createEl('div', { padding: '20px', color: THEME.muted, fontSize: '13px', lineHeight: '1.5' });
|
|
347
|
+
emptyState.textContent = 'Usa il tab Seleziona per cliccare su un elemento, oppure tab Elementi per caricare i blocchi da file. Le modifiche si applicano al file subito.';
|
|
348
|
+
tabPanels.style.appendChild(emptyState);
|
|
349
|
+
|
|
350
|
+
const inputByKey: Record<string, HTMLInputElement> = {};
|
|
351
|
+
const colorByKey: Record<string, HTMLInputElement> = {};
|
|
352
|
+
|
|
353
|
+
function getStyleForPatch(): Record<string, number | string> {
|
|
354
|
+
const out: Record<string, number | string> = {};
|
|
355
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
356
|
+
if (v !== '' && v !== undefined) out[k] = v;
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function schedulePatch(): void {
|
|
362
|
+
if (debounceTimer) window.clearTimeout(debounceTimer);
|
|
363
|
+
debounceTimer = window.setTimeout(() => {
|
|
364
|
+
debounceTimer = 0;
|
|
365
|
+
if (!currentFile || currentLine == null) return;
|
|
366
|
+
const style = getStyleForPatch();
|
|
367
|
+
if (Object.keys(style).length === 0) return;
|
|
368
|
+
fetch(`${PATCH_SERVER_URL}/__flow-style-patch`, {
|
|
369
|
+
method: 'POST',
|
|
370
|
+
headers: { 'Content-Type': 'application/json' },
|
|
371
|
+
body: JSON.stringify({ file: currentFile, line: currentLine, style }),
|
|
372
|
+
}).catch(() => {});
|
|
373
|
+
}, DEBOUNCE_MS);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function numInputWithStepper(key: string, v: string, isNum: boolean): HTMLElement {
|
|
377
|
+
const wrap = createEl('div', { display: 'flex', alignItems: 'center', gap: '4px', flex: '1', minWidth: '0' });
|
|
378
|
+
const step = 1;
|
|
379
|
+
const minusBtn = createEl('button', {
|
|
380
|
+
width: '28px',
|
|
381
|
+
height: '28px',
|
|
382
|
+
padding: '0',
|
|
383
|
+
border: `1px solid ${THEME.border}`,
|
|
384
|
+
borderRadius: '6px',
|
|
385
|
+
background: THEME.surface,
|
|
386
|
+
color: THEME.muted,
|
|
387
|
+
fontSize: '14px',
|
|
388
|
+
cursor: 'pointer',
|
|
389
|
+
flexShrink: '0',
|
|
390
|
+
}) as HTMLButtonElement;
|
|
391
|
+
minusBtn.textContent = '−';
|
|
392
|
+
const plusBtn = createEl('button', {
|
|
393
|
+
width: '28px',
|
|
394
|
+
height: '28px',
|
|
395
|
+
padding: '0',
|
|
396
|
+
border: `1px solid ${THEME.border}`,
|
|
397
|
+
borderRadius: '6px',
|
|
398
|
+
background: THEME.surface,
|
|
399
|
+
color: THEME.muted,
|
|
400
|
+
fontSize: '14px',
|
|
401
|
+
cursor: 'pointer',
|
|
402
|
+
flexShrink: '0',
|
|
403
|
+
}) as HTMLButtonElement;
|
|
404
|
+
plusBtn.textContent = '+';
|
|
405
|
+
const input = createEl('input', {
|
|
406
|
+
width: '3rem',
|
|
407
|
+
minWidth: '36px',
|
|
408
|
+
padding: '6px 8px',
|
|
409
|
+
border: `1px solid ${THEME.border}`,
|
|
410
|
+
borderRadius: '6px',
|
|
411
|
+
background: THEME.surface,
|
|
412
|
+
color: THEME.text,
|
|
413
|
+
fontSize: '12px',
|
|
414
|
+
textAlign: 'center',
|
|
415
|
+
}) as HTMLInputElement;
|
|
416
|
+
input.type = isNum ? 'number' : 'text';
|
|
417
|
+
input.value = v;
|
|
418
|
+
input.placeholder = isNum ? '0' : '';
|
|
419
|
+
const update = () => {
|
|
420
|
+
const next = isNum ? parsePx(input.value) : input.value;
|
|
421
|
+
overrides[key] = next;
|
|
422
|
+
applyOverridesToPreview();
|
|
423
|
+
schedulePatch();
|
|
424
|
+
};
|
|
425
|
+
minusBtn.addEventListener('click', () => {
|
|
426
|
+
const n = parseFloat(input.value);
|
|
427
|
+
if (!Number.isNaN(n)) {
|
|
428
|
+
input.value = String(n - step);
|
|
429
|
+
update();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
plusBtn.addEventListener('click', () => {
|
|
433
|
+
const n = parseFloat(input.value);
|
|
434
|
+
if (!Number.isNaN(n)) {
|
|
435
|
+
input.value = String(n + step);
|
|
436
|
+
update();
|
|
437
|
+
} else if (input.value === '') {
|
|
438
|
+
input.value = String(step);
|
|
439
|
+
update();
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
input.addEventListener('input', update);
|
|
443
|
+
inputByKey[key] = input;
|
|
444
|
+
wrap.appendChild(minusBtn);
|
|
445
|
+
wrap.appendChild(input);
|
|
446
|
+
wrap.appendChild(plusBtn);
|
|
447
|
+
return wrap;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function renderBoxControl(
|
|
451
|
+
label: string,
|
|
452
|
+
mainKey: 'p' | 'm',
|
|
453
|
+
sideKeys: [string, string, string, string]
|
|
454
|
+
): HTMLElement {
|
|
455
|
+
const [ptK, prK, pbK, plK] = sideKeys;
|
|
456
|
+
const wrap = createEl('div', { marginBottom: '18px' });
|
|
457
|
+
const header = createEl('div', {
|
|
458
|
+
display: 'flex',
|
|
459
|
+
alignItems: 'center',
|
|
460
|
+
gap: '6px',
|
|
461
|
+
marginBottom: '8px',
|
|
462
|
+
fontSize: '10px',
|
|
463
|
+
fontWeight: '600',
|
|
464
|
+
textTransform: 'uppercase',
|
|
465
|
+
letterSpacing: '0.06em',
|
|
466
|
+
color: THEME.muted,
|
|
467
|
+
});
|
|
468
|
+
header.textContent = label;
|
|
469
|
+
const lockBtn = createEl('button', {
|
|
470
|
+
width: '22px',
|
|
471
|
+
height: '22px',
|
|
472
|
+
padding: '0',
|
|
473
|
+
border: `1px solid ${THEME.border}`,
|
|
474
|
+
borderRadius: '4px',
|
|
475
|
+
background: THEME.surface,
|
|
476
|
+
color: THEME.muted,
|
|
477
|
+
cursor: 'pointer',
|
|
478
|
+
}) as HTMLButtonElement;
|
|
479
|
+
let locked = true;
|
|
480
|
+
lockBtn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>';
|
|
481
|
+
lockBtn.title = 'Sblocca lati';
|
|
482
|
+
const makeInput = (key: string, placeholder: string) => {
|
|
483
|
+
const inp = createEl('input', {
|
|
484
|
+
width: '100%',
|
|
485
|
+
minWidth: '24px',
|
|
486
|
+
padding: '4px 4px',
|
|
487
|
+
border: `1px solid ${THEME.border}`,
|
|
488
|
+
borderRadius: '4px',
|
|
489
|
+
background: THEME.surface,
|
|
490
|
+
color: THEME.text,
|
|
491
|
+
fontSize: '11px',
|
|
492
|
+
textAlign: 'center',
|
|
493
|
+
}) as HTMLInputElement;
|
|
494
|
+
inp.type = 'number';
|
|
495
|
+
inp.placeholder = placeholder;
|
|
496
|
+
inp.value = String(overrides[key] ?? '');
|
|
497
|
+
inp.addEventListener('input', () => {
|
|
498
|
+
const v = parsePx(inp.value);
|
|
499
|
+
overrides[key] = v;
|
|
500
|
+
applyOverridesToPreview();
|
|
501
|
+
schedulePatch();
|
|
502
|
+
});
|
|
503
|
+
(inputByKey as Record<string, HTMLInputElement>)[key] = inp;
|
|
504
|
+
return inp;
|
|
505
|
+
};
|
|
506
|
+
const topInp = makeInput(ptK, 'T');
|
|
507
|
+
const rightInp = makeInput(prK, 'R');
|
|
508
|
+
const bottomInp = makeInput(pbK, 'B');
|
|
509
|
+
const leftInp = makeInput(plK, 'L');
|
|
510
|
+
const allInp = makeInput(mainKey, '');
|
|
511
|
+
allInp.placeholder = '0';
|
|
512
|
+
allInp.value = String(overrides[mainKey] ?? overrides[ptK] ?? overrides[prK] ?? overrides[pbK] ?? overrides[plK] ?? '');
|
|
513
|
+
const grid = createEl('div', {
|
|
514
|
+
display: 'grid',
|
|
515
|
+
gridTemplateColumns: '28px 28px 28px',
|
|
516
|
+
gridTemplateRows: '28px 28px 28px',
|
|
517
|
+
gap: '2px',
|
|
518
|
+
alignItems: 'center',
|
|
519
|
+
justifyContent: 'center',
|
|
520
|
+
});
|
|
521
|
+
const topCell = createEl('div', { gridColumn: '2', gridRow: '1' });
|
|
522
|
+
topCell.appendChild(topInp);
|
|
523
|
+
const rightCell = createEl('div', { gridColumn: '3', gridRow: '2' });
|
|
524
|
+
rightCell.appendChild(rightInp);
|
|
525
|
+
const bottomCell = createEl('div', { gridColumn: '2', gridRow: '3' });
|
|
526
|
+
bottomCell.appendChild(bottomInp);
|
|
527
|
+
const leftCell = createEl('div', { gridColumn: '1', gridRow: '2' });
|
|
528
|
+
leftCell.appendChild(leftInp);
|
|
529
|
+
const center = createEl('div', { gridColumn: '2', gridRow: '2', width: '28px', height: '28px', border: `2px solid ${THEME.border}`, borderRadius: '4px', background: THEME.surface, display: 'flex', alignItems: 'center', justifyContent: 'center' });
|
|
530
|
+
center.appendChild(allInp);
|
|
531
|
+
const toggleLock = () => {
|
|
532
|
+
locked = !locked;
|
|
533
|
+
lockBtn.title = locked ? 'Sblocca lati' : 'Blocca tutti';
|
|
534
|
+
topCell.style.display = locked ? 'none' : 'block';
|
|
535
|
+
rightCell.style.display = locked ? 'none' : 'block';
|
|
536
|
+
bottomCell.style.display = locked ? 'none' : 'block';
|
|
537
|
+
leftCell.style.display = locked ? 'none' : 'block';
|
|
538
|
+
(center.style as unknown as Record<string, string>)['display'] = locked ? 'flex' : 'none';
|
|
539
|
+
if (locked) {
|
|
540
|
+
const v = overrides[mainKey] ?? overrides[ptK] ?? '';
|
|
541
|
+
overrides[mainKey] = v;
|
|
542
|
+
[ptK, prK, pbK, plK].forEach((k) => (overrides[k] = v));
|
|
543
|
+
topInp.value = rightInp.value = bottomInp.value = leftInp.value = allInp.value = String(v);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
lockBtn.addEventListener('click', toggleLock);
|
|
547
|
+
if (locked) {
|
|
548
|
+
topCell.style.display = 'none';
|
|
549
|
+
rightCell.style.display = 'none';
|
|
550
|
+
bottomCell.style.display = 'none';
|
|
551
|
+
leftCell.style.display = 'none';
|
|
552
|
+
} else {
|
|
553
|
+
(center.style as unknown as Record<string, string>)['display'] = 'none';
|
|
554
|
+
}
|
|
555
|
+
allInp.addEventListener('input', () => {
|
|
556
|
+
if (locked) {
|
|
557
|
+
const v = parsePx(allInp.value);
|
|
558
|
+
overrides[mainKey] = v;
|
|
559
|
+
[ptK, prK, pbK, plK].forEach((k) => (overrides[k] = v));
|
|
560
|
+
topInp.value = rightInp.value = bottomInp.value = leftInp.value = String(v);
|
|
561
|
+
applyOverridesToPreview();
|
|
562
|
+
schedulePatch();
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
header.appendChild(lockBtn);
|
|
566
|
+
wrap.appendChild(header);
|
|
567
|
+
grid.appendChild(topCell);
|
|
568
|
+
grid.appendChild(leftCell);
|
|
569
|
+
grid.appendChild(center);
|
|
570
|
+
grid.appendChild(rightCell);
|
|
571
|
+
grid.appendChild(bottomCell);
|
|
572
|
+
wrap.appendChild(grid);
|
|
573
|
+
return wrap;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function renderGroups(): void {
|
|
577
|
+
scroll.innerHTML = '';
|
|
578
|
+
scroll.appendChild(renderBoxControl('Padding', 'p', ['pt', 'pr', 'pb', 'pl']));
|
|
579
|
+
scroll.appendChild(renderBoxControl('Margin', 'm', ['mt', 'mr', 'mb', 'ml']));
|
|
580
|
+
for (const g of GROUPS) {
|
|
581
|
+
const groupDiv = createEl('div', { marginBottom: '18px' });
|
|
582
|
+
const labelDiv = createEl('div', {
|
|
583
|
+
fontSize: '10px',
|
|
584
|
+
fontWeight: '600',
|
|
585
|
+
textTransform: 'uppercase',
|
|
586
|
+
letterSpacing: '0.06em',
|
|
587
|
+
color: THEME.muted,
|
|
588
|
+
marginBottom: '8px',
|
|
589
|
+
});
|
|
590
|
+
labelDiv.textContent = g.label;
|
|
591
|
+
groupDiv.appendChild(labelDiv);
|
|
592
|
+
for (const key of g.keys) {
|
|
593
|
+
const row = createEl('div', { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' });
|
|
594
|
+
const label = createEl('span', { width: '36px', flexShrink: '0', color: THEME.muted, fontSize: '12px' });
|
|
595
|
+
label.textContent = key;
|
|
596
|
+
row.appendChild(label);
|
|
597
|
+
const isColor = key === 'color' || key === 'bg';
|
|
598
|
+
const isNum = NUM_KEYS.has(key);
|
|
599
|
+
const val = overrides[key];
|
|
600
|
+
const v = val === undefined || val === '' ? '' : String(val);
|
|
601
|
+
if (isColor) {
|
|
602
|
+
const colorWrap = createEl('div', { flex: '1', display: 'flex', gap: '8px', minWidth: '0' });
|
|
603
|
+
const colorInput = createEl('input', {
|
|
604
|
+
padding: '2px',
|
|
605
|
+
height: '28px',
|
|
606
|
+
width: '40px',
|
|
607
|
+
flexShrink: '0',
|
|
608
|
+
border: `1px solid ${THEME.border}`,
|
|
609
|
+
borderRadius: '6px',
|
|
610
|
+
background: THEME.surface,
|
|
611
|
+
}) as HTMLInputElement;
|
|
612
|
+
colorInput.type = 'color';
|
|
613
|
+
colorInput.value = /^#([0-9A-Fa-f]{3}){1,2}$/.test(v) ? v : '#000000';
|
|
614
|
+
const textInput = createEl('input', {
|
|
615
|
+
flex: '1',
|
|
616
|
+
padding: '6px 10px',
|
|
617
|
+
border: `1px solid ${THEME.border}`,
|
|
618
|
+
borderRadius: '6px',
|
|
619
|
+
background: THEME.surface,
|
|
620
|
+
color: THEME.text,
|
|
621
|
+
fontSize: '12px',
|
|
622
|
+
minWidth: '0',
|
|
623
|
+
}) as HTMLInputElement;
|
|
624
|
+
textInput.type = 'text';
|
|
625
|
+
textInput.value = v;
|
|
626
|
+
textInput.placeholder = '#hex';
|
|
627
|
+
const syncColor = () => {
|
|
628
|
+
overrides[key] = textInput.value;
|
|
629
|
+
if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(textInput.value)) colorInput.value = textInput.value;
|
|
630
|
+
applyOverridesToPreview();
|
|
631
|
+
schedulePatch();
|
|
632
|
+
};
|
|
633
|
+
colorInput.addEventListener('input', () => {
|
|
634
|
+
overrides[key] = colorInput.value;
|
|
635
|
+
textInput.value = colorInput.value;
|
|
636
|
+
applyOverridesToPreview();
|
|
637
|
+
schedulePatch();
|
|
638
|
+
});
|
|
639
|
+
textInput.addEventListener('input', syncColor);
|
|
640
|
+
colorByKey[key] = colorInput;
|
|
641
|
+
inputByKey[key] = textInput;
|
|
642
|
+
colorWrap.appendChild(colorInput);
|
|
643
|
+
colorWrap.appendChild(textInput);
|
|
644
|
+
row.appendChild(colorWrap);
|
|
645
|
+
} else {
|
|
646
|
+
row.appendChild(numInputWithStepper(key, v, isNum));
|
|
647
|
+
}
|
|
648
|
+
groupDiv.appendChild(row);
|
|
649
|
+
}
|
|
650
|
+
scroll.appendChild(groupDiv);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function switchToStyleTab(): void {
|
|
655
|
+
activeTab = 'style';
|
|
656
|
+
tabs.forEach((_, i) => {
|
|
657
|
+
const b = tabBar.children[i];
|
|
658
|
+
if (b instanceof HTMLElement) {
|
|
659
|
+
b.style.background = tabs[i]?.id === 'style' ? THEME.accent : 'transparent';
|
|
660
|
+
b.style.color = tabs[i]?.id === 'style' ? THEME.bg : THEME.muted;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
(Object.keys(tabPanels) as (keyof typeof tabPanels)[]).forEach((k) => {
|
|
664
|
+
tabPanels[k].style.display = k === 'style' ? 'flex' : 'none';
|
|
665
|
+
});
|
|
666
|
+
tabPanels.style.style.flexDirection = 'column';
|
|
667
|
+
tabPanels.style.style.flex = '1';
|
|
668
|
+
tabPanels.style.style.overflow = 'auto';
|
|
669
|
+
overlay.style.display = 'none';
|
|
670
|
+
highlight.style.display = 'none';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function selectBlock(file: string, line: number): void {
|
|
674
|
+
currentFile = file;
|
|
675
|
+
currentLine = line;
|
|
676
|
+
try {
|
|
677
|
+
sessionStorage.setItem(STORAGE_KEY_FILE, file);
|
|
678
|
+
sessionStorage.setItem(STORAGE_KEY_LINE, String(line));
|
|
679
|
+
} catch {}
|
|
680
|
+
switchToStyleTab();
|
|
681
|
+
fetch(`${PATCH_SERVER_URL}/__flow-style-read?file=${encodeURIComponent(file)}&line=${line}`)
|
|
682
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('Read failed'))))
|
|
683
|
+
.then((data: { style?: Record<string, number | string> }) => {
|
|
684
|
+
overrides = { ...(data.style ?? {}) };
|
|
685
|
+
emptyState.style.display = 'none';
|
|
686
|
+
scroll.style.display = '';
|
|
687
|
+
footer.style.display = 'flex';
|
|
688
|
+
renderGroups();
|
|
689
|
+
applyOverridesToPreview();
|
|
690
|
+
})
|
|
691
|
+
.catch(() => {
|
|
692
|
+
overrides = {};
|
|
693
|
+
renderGroups();
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function loadBlocks(file: string, thenSelectLine?: number): void {
|
|
698
|
+
file = file.trim();
|
|
699
|
+
if (!file) return;
|
|
700
|
+
fileInput.value = file;
|
|
701
|
+
fetch(`${PATCH_SERVER_URL}/__flow-style-blocks?file=${encodeURIComponent(file)}`)
|
|
702
|
+
.then((res) => {
|
|
703
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
704
|
+
return res.json();
|
|
705
|
+
})
|
|
706
|
+
.then((data: { blocks?: { line: number }[] }) => {
|
|
707
|
+
const blocks = data.blocks ?? [];
|
|
708
|
+
blocksList.innerHTML = '';
|
|
709
|
+
currentFile = file;
|
|
710
|
+
if (blocks.length === 0) {
|
|
711
|
+
const msg = createEl('div', { fontSize: '12px', color: THEME.muted });
|
|
712
|
+
msg.textContent = 'Nessun blocco s= / styleFlow= trovato.';
|
|
713
|
+
blocksList.appendChild(msg);
|
|
714
|
+
} else {
|
|
715
|
+
for (const b of blocks) {
|
|
716
|
+
const btn = createEl('button', {
|
|
717
|
+
padding: '8px 12px',
|
|
718
|
+
textAlign: 'left',
|
|
719
|
+
border: `1px solid ${THEME.border}`,
|
|
720
|
+
borderRadius: '6px',
|
|
721
|
+
background: THEME.surface,
|
|
722
|
+
color: THEME.text,
|
|
723
|
+
fontSize: '12px',
|
|
724
|
+
cursor: 'pointer',
|
|
725
|
+
}) as HTMLButtonElement;
|
|
726
|
+
btn.textContent = `Riga ${b.line}`;
|
|
727
|
+
btn.addEventListener('click', () => selectBlock(file, b.line));
|
|
728
|
+
blocksList.appendChild(btn);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (thenSelectLine != null && blocks.some((x) => x.line === thenSelectLine)) {
|
|
732
|
+
selectBlock(file, thenSelectLine);
|
|
733
|
+
}
|
|
734
|
+
})
|
|
735
|
+
.catch(() => {
|
|
736
|
+
blocksList.innerHTML = '';
|
|
737
|
+
const err = createEl('div', { fontSize: '12px', color: '#f87171' });
|
|
738
|
+
err.textContent = 'Errore caricamento blocchi';
|
|
739
|
+
blocksList.appendChild(err);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function getElementUnderCursor(e: MouseEvent): HTMLElement | null {
|
|
744
|
+
if (previewIframe?.contentDocument) {
|
|
745
|
+
const rect = previewIframe.getBoundingClientRect();
|
|
746
|
+
const x = e.clientX - rect.left;
|
|
747
|
+
const y = e.clientY - rect.top;
|
|
748
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height) return null;
|
|
749
|
+
const doc = previewIframe.contentDocument;
|
|
750
|
+
const el = doc.elementFromPoint(x, y);
|
|
751
|
+
return el instanceof HTMLElement ? el : null;
|
|
752
|
+
}
|
|
753
|
+
const el = document.elementsFromPoint(e.clientX, e.clientY).find(
|
|
754
|
+
(n) => n !== overlay && n !== highlight && !panel.contains(n)
|
|
755
|
+
);
|
|
756
|
+
return el instanceof HTMLElement ? el : null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function getHighlightRect(el: HTMLElement): { top: number; left: number; width: number; height: number } {
|
|
760
|
+
const rect = el.getBoundingClientRect();
|
|
761
|
+
if (previewIframe?.contentDocument && el.ownerDocument === previewIframe.contentDocument) {
|
|
762
|
+
const frameRect = previewIframe.getBoundingClientRect();
|
|
763
|
+
return {
|
|
764
|
+
top: frameRect.top + rect.top,
|
|
765
|
+
left: frameRect.left + rect.left,
|
|
766
|
+
width: rect.width,
|
|
767
|
+
height: rect.height,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
overlay.addEventListener('mousemove', (e: MouseEvent) => {
|
|
774
|
+
hoverEl = getElementUnderCursor(e);
|
|
775
|
+
if (hoverEl && highlight.parentNode === document.body) {
|
|
776
|
+
const r = getHighlightRect(hoverEl);
|
|
777
|
+
highlight.style.top = `${r.top}px`;
|
|
778
|
+
highlight.style.left = `${r.left}px`;
|
|
779
|
+
highlight.style.width = `${r.width}px`;
|
|
780
|
+
highlight.style.height = `${r.height}px`;
|
|
781
|
+
highlight.style.display = '';
|
|
782
|
+
} else {
|
|
783
|
+
highlight.style.display = 'none';
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
overlay.addEventListener('click', (e: MouseEvent) => {
|
|
787
|
+
const el = getElementUnderCursor(e);
|
|
788
|
+
if (!el) return;
|
|
789
|
+
const file = el.getAttribute('data-flow-style-file');
|
|
790
|
+
const lineStr = el.getAttribute('data-flow-style-line');
|
|
791
|
+
if (file && lineStr) {
|
|
792
|
+
const line = parseInt(lineStr, 10);
|
|
793
|
+
if (!Number.isNaN(line)) {
|
|
794
|
+
e.preventDefault();
|
|
795
|
+
selectBlock(file, line);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
loadBtn.addEventListener('click', () => loadBlocks(fileInput.value.trim()));
|
|
801
|
+
|
|
802
|
+
copyObjBtn.addEventListener('click', () => {
|
|
803
|
+
void navigator.clipboard.writeText(overridesToJson(overrides));
|
|
804
|
+
});
|
|
805
|
+
copyPropBtn.addEventListener('click', () => {
|
|
806
|
+
void navigator.clipboard.writeText(`s={${overridesToJson(overrides)}}`);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
document.body.appendChild(panel);
|
|
810
|
+
|
|
811
|
+
// Ripristino dopo refresh: stesso file e blocco selezionato
|
|
812
|
+
try {
|
|
813
|
+
const savedFile = sessionStorage.getItem(STORAGE_KEY_FILE);
|
|
814
|
+
const savedLine = sessionStorage.getItem(STORAGE_KEY_LINE);
|
|
815
|
+
if (savedFile) {
|
|
816
|
+
const lineNum = savedLine ? parseInt(savedLine, 10) : NaN;
|
|
817
|
+
loadBlocks(savedFile, Number.isNaN(lineNum) ? undefined : lineNum);
|
|
818
|
+
}
|
|
819
|
+
} catch {}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export const VisualStyleBuilder = initVisualStyleBuilder;
|