@sailingrotevista/rotevista-dash 1.0.24 → 1.0.26

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 (4) hide show
  1. package/app.js +38 -52
  2. package/index.html +26 -74
  3. package/package.json +1 -1
  4. package/style.css +79 -177
package/app.js CHANGED
@@ -42,7 +42,7 @@ let socket, renderInterval, simInterval;
42
42
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
43
43
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
44
44
 
45
- // Gestione Focus e Interazioni
45
+ // Gestione Interazioni (Long Press, Focus, Ghost Clicks)
46
46
  let pressTimer, isFocusActive = false, blockNextClick = false;
47
47
 
48
48
  // Modalità Scale (Standard/Hercules) salvate nel browser
@@ -54,8 +54,7 @@ const graphModes = {
54
54
  };
55
55
 
56
56
  const store = {
57
- raw: {},
58
- timestamps: {},
57
+ raw: {}, timestamps: {},
59
58
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
60
59
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
61
60
  histories: { stw: [], sog: [], depth: [], tws: [] },
@@ -76,7 +75,7 @@ const ui = {
76
75
  };
77
76
 
78
77
  // ==========================================================================
79
- // 3. CARICAMENTO CONFIGURAZIONE DAL SERVER
78
+ // 3. COMUNICAZIONE CON IL SERVER (FETCH CONFIG)
80
79
  // ==========================================================================
81
80
  async function fetchServerConfig() {
82
81
  if (!window.location.protocol.includes("http")) return;
@@ -90,6 +89,7 @@ async function fetchServerConfig() {
90
89
  const data = await response.json();
91
90
  const actual = data.configuration || data;
92
91
  if (actual && typeof actual === 'object') {
92
+ // Normalizzazione dati (conversione stringhe -> numeri)
93
93
  const parseNumbers = (obj) => {
94
94
  for (let k in obj) {
95
95
  if (typeof obj[k] === 'object') parseNumbers(obj[k]);
@@ -97,6 +97,7 @@ async function fetchServerConfig() {
97
97
  }
98
98
  };
99
99
  parseNumbers(actual);
100
+ // Merge nel sistema locale
100
101
  if (actual.alarms) CONFIG.alarms = { ...CONFIG.alarms, ...actual.alarms };
101
102
  if (actual.graphs) CONFIG.graphs = { ...CONFIG.graphs, ...actual.graphs };
102
103
  if (actual.averaging) CONFIG.averages = { ...CONFIG.averages, ...actual.averaging };
@@ -120,7 +121,7 @@ function msToKts(ms) { return ms * 1.94384; }
120
121
  function ktsToMs(kts) { return kts / 1.94384; }
121
122
  function getShortestRotation(curr, target) { let diff = (target - curr) % 360; if (diff > 180) diff -= 360; else if (diff < -180) diff += 360; return curr + diff; }
122
123
 
123
- // Calcolo media circolare vettoriale con rilevamento stabilità
124
+ // Calcolo media circolare vettoriale
124
125
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
125
126
  const now = Date.now();
126
127
  const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
@@ -141,7 +142,7 @@ function processIncomingData(path, val) {
141
142
  if (path === "environment.wind.angleApparent") { store.smoothBuf.awa.push({ val: val, time: now }); store.longBuf.awa.push({ val: val, time: now }); }
142
143
  if (path === "environment.wind.angleTrueWater") { store.smoothBuf.twa.push({ val: val, time: now }); store.longBuf.twa.push({ val: val, time: now }); }
143
144
 
144
- // Calcolo TWD (True Wind Direction)
145
+ // Calcolo TWD (Vento Reale Direzione Geografica)
145
146
  if (path === "navigation.headingTrue" || path === "environment.wind.angleTrueWater" || path === "environment.wind.directionTrue") {
146
147
  let twdRad = 0;
147
148
  if (path === "environment.wind.directionTrue") twdRad = val;
@@ -160,21 +161,25 @@ function processIncomingData(path, val) {
160
161
  function startDisplayLoop() {
161
162
  renderInterval = setInterval(() => {
162
163
  const now = Date.now();
164
+
165
+ // 5.1 Watchdog
163
166
  const pathsToWatch = { "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog, "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog, "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws };
164
167
  for (let p in pathsToWatch) { if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) { pathsToWatch[p][pathsToWatch[p] === ui.awsSvg ? 'textContent' : 'innerText'] = "---"; delete store.raw[p]; } }
165
168
 
169
+ // 5.2 Renders Istantanei
166
170
  if (store.raw["navigation.speedThroughWater"] !== undefined) { const v = msToKts(store.raw["navigation.speedThroughWater"]); ui.stw.innerText = v.toFixed(1); manageHistory('stw', v); }
167
171
  let curSog = 0; if (store.raw["navigation.speedOverGround"] !== undefined) { curSog = msToKts(store.raw["navigation.speedOverGround"]); ui.sog.innerText = curSog.toFixed(1); manageHistory('sog', curSog); }
168
172
  if (store.raw["environment.depth.belowTransducer"] !== undefined) { const d = store.raw["environment.depth.belowTransducer"]; ui.depth.innerText = d.toFixed(1); checkDepthAlarm(d); manageHistory('depth', d); }
169
173
  if (store.raw["environment.wind.speedTrue"] !== undefined) { const w = msToKts(store.raw["environment.wind.speedTrue"]); ui.tws.innerText = w.toFixed(1); manageHistory('tws', w); }
170
174
  if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
171
175
 
176
+ // 5.3 Render Quadrante (2s smoothing)
172
177
  const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, CONFIG.averages.smoothWindow, true);
173
178
  if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, smAwa.val); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
174
179
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
175
180
  if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
176
181
 
177
- // Calcolo Drift Reale (COG-HDG)
182
+ // Drift Reale (COG-HDG)
178
183
  if (store.raw["navigation.courseOverGroundTrue"] && store.raw["navigation.headingTrue"]) {
179
184
  let drift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
180
185
  if (curSog < CONFIG.averages.minSpeed) drift = 0;
@@ -182,6 +187,7 @@ function startDisplayLoop() {
182
187
  let ds = drift > 180 ? drift - 360 : drift; updateLeewayDisplay(Math.max(-20, Math.min(20, ds)));
183
188
  } else updateLeewayDisplay(0);
184
189
 
190
+ // 5.4 Render Medie Lunghe (Ogni 3s)
185
191
  if (now - lastAvgUIUpdate > 3000) {
186
192
  let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averages.longWindow, false),
187
193
  cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averages.longWindow, false),
@@ -198,9 +204,9 @@ function startDisplayLoop() {
198
204
  };
199
205
  upUI(ui.hdg, hObj); upUI(ui.cog, cObj); upUI(ui.awaAvg, awObj); upUI(ui.twaAvg, twObj); upUI(ui.twdAvg, twdObj);
200
206
 
207
+ // TACK
201
208
  if (hObj && twObj && hObj.val !== null) {
202
- let tA = twObj.val * 2;
203
- ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
209
+ let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
204
210
  if (cObj) ui.tackCog.innerHTML = `${Math.round((cObj.val - tA + 360) % 360).toString().padStart(3, '0')}&deg;`;
205
211
  let tStable = (hObj.stable && twObj.stable) || (curSog < CONFIG.averages.minSpeed);
206
212
  if (tStable) { ui.tackHdg.classList.remove('unstable-data'); ui.tackCog.classList.remove('unstable-data'); }
@@ -215,7 +221,7 @@ function startDisplayLoop() {
215
221
  }
216
222
 
217
223
  // ==========================================================================
218
- // 6. CONNESSIONE SIGNALK
224
+ // 6. CONNESSIONE SIGNALK (subscribe=self)
219
225
  // ==========================================================================
220
226
  function connect() {
221
227
  if (simulationMode) return;
@@ -248,11 +254,8 @@ function calculateScale(type, data, mode) {
248
254
  const s = CONFIG.scales[type] || { stdMax: 12, hercSpan: 4, step: 2 };
249
255
  let aMin = Math.min(...data), aMax = Math.max(...data);
250
256
  if (mode === 'hercules') {
251
- let avg = (aMin + aMax) / 2;
252
- let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin));
253
- if (span % 2 !== 0) span += 1;
254
- let min = Math.max(0, Math.floor(avg - (span / 2)));
255
- return { min, max: min + span };
257
+ let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin)); if (span % 2 !== 0) span += 1;
258
+ let min = Math.max(0, Math.floor(avg - (span / 2))); return { min, max: min + span };
256
259
  } else return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
257
260
  }
258
261
 
@@ -272,77 +275,60 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
272
275
  if (isTws && i > 0) {
273
276
  const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
274
277
  let c = "#f1c40f"; if (v >= CONFIG.graphs.reef2) c = "#e74c3c"; else if (v >= CONFIG.graphs.reef1) c = "#e67e22";
275
- cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" stroke-width="${isHercules?3:2}" class="${isHercules?'line-hercules':''}" />`;
278
+ cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
276
279
  }
277
280
  });
278
281
  const aP = pD + ` L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z`, clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#f1c40f' };
279
- svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(241,196,15,0.12)" stroke="none" />${cS}` : `${grids}<path d="${aP}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${clrs[id]}" stroke-width="1.5" />`;
282
+ svg.innerHTML = isTws ? `${grids}<path d="${aP}" fill="rgba(241,196,15,0.12)" stroke="none" />${cS}` : `${grids}<path d="${aP}" fill="${clrs[id]}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${clrs[id]}" />`;
280
283
  }
281
284
 
282
285
  // ==========================================================================
283
- // 8. EVENTI, INTERAZIONI E FOCUS MODE
286
+ // 8. EVENTI E INTERAZIONI (Touch, Focus e Fullscreen)
284
287
  // ==========================================================================
285
288
 
289
+ // Blocco globale del menu contestuale per Android/iOS
290
+ window.addEventListener('contextmenu', e => e.preventDefault());
291
+
286
292
  function toggleFocusMode(type, element) {
287
293
  const container = document.querySelector('.main-container');
288
294
  const parentPanel = element.closest('.side-panel');
289
295
  const isLeft = parentPanel.classList.contains('left-panel');
290
-
291
296
  isFocusActive = !isFocusActive;
292
-
293
- if (isFocusActive) {
294
- container.classList.add('focus-active');
295
- container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right');
296
- parentPanel.classList.add('has-focus');
297
- element.classList.add('is-focused');
298
- blockNextClick = true; // Impedisce al click di rilascio del tocco lungo di chiudere subito
299
- } else {
300
- container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
301
- document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus'));
302
- document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
303
- }
297
+ if (isFocusActive) { container.classList.add('focus-active'); container.classList.add(isLeft ? 'focus-side-left' : 'focus-side-right'); parentPanel.classList.add('has-focus'); element.classList.add('is-focused'); blockNextClick = true; }
298
+ else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('has-focus')); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
304
299
  }
305
300
 
306
301
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
307
302
  const el = document.getElementById(type + '-graph').closest('.data-box');
308
303
 
309
- // Doppio Click -> Toggle Hercules Zoom
310
- el.addEventListener('dblclick', (e) => {
311
- if (isFocusActive) return;
312
- e.preventDefault();
313
- graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard';
314
- localStorage.setItem('mode_' + type, graphModes[type]);
315
- el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200);
316
- });
304
+ // Doppio Click -> Hercules Zoom
305
+ el.addEventListener('dblclick', (e) => { if (isFocusActive) return; e.preventDefault(); graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); el.style.backgroundColor = "rgba(255,255,255,0.15)"; setTimeout(() => el.style.backgroundColor = "", 200); });
317
306
 
318
- // Gestione Pressione Prolungata (1s) -> Tactical Focus
307
+ // Long Press -> Tactical Focus
319
308
  const startPress = () => { if (!isFocusActive) pressTimer = setTimeout(() => toggleFocusMode(type, el), 1000); };
320
309
  const cancelPress = () => { clearTimeout(pressTimer); };
321
310
  el.addEventListener('mousedown', startPress); el.addEventListener('touchstart', startPress, {passive: true});
322
- ['mouseup', 'mouseleave', 'touchend'].forEach(evt => el.addEventListener(evt, cancelPress));
311
+ ['mouseup', 'mouseleave', 'touchend', 'touchcancel'].forEach(evt => el.addEventListener(evt, cancelPress));
323
312
 
324
- // Click per uscire o per catturare il rilascio del tocco lungo
325
- el.addEventListener('click', (e) => {
326
- if (blockNextClick) { blockNextClick = false; return; }
327
- if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el);
328
- });
313
+ // Click -> Exit Focus o Ghost click filtering
314
+ el.addEventListener('click', (e) => { if (blockNextClick) { blockNextClick = false; return; } if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); });
329
315
  });
330
316
 
331
- // Fullscreen (Hotspot)
317
+ // Fullscreen via Hotspot
332
318
  if (ui.hotspot) { ui.hotspot.addEventListener('click', () => { const doc = document.documentElement, isF = document.fullscreenElement || document.webkitFullscreenElement; if (!isF) { if (doc.requestFullscreen) doc.requestFullscreen(); else if (doc.webkitRequestFullscreen) doc.webkitRequestFullscreen(); } else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); } }); }
333
319
 
334
- // Allarmi Audio e Depth
335
- function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
336
- function playBingBing() { if (!audioCtx) return; const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
337
-
338
320
  // ==========================================================================
339
321
  // 9. INIZIALIZZAZIONE
340
322
  // ==========================================================================
341
- (function generateTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
323
+ (function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
342
324
 
343
325
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
344
326
  window.addEventListener('load', init);
345
327
 
328
+ // Allarmi e Audio
329
+ function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
330
+ function playBingBing() { if (!audioCtx) return; const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
331
+
346
332
  // Simulatore (Triple click su Depth)
347
333
  ui.depth.closest('.data-box').addEventListener('click', (function() {
348
334
  let dC = 0, lC = 0; return function() { const n = Date.now(); if (n - lC < 500) dC++; else dC = 1; lC = n; if (dC === 3) { simulationMode = !simulationMode; if (simulationMode) { if (socket) socket.close(); ui.status.innerText = "SIM ATTIVO"; simInterval = setInterval(() => { processIncomingData("navigation.headingTrue", degToRad(Math.random()*360)); processIncomingData("navigation.speedOverGround", ktsToMs(8)); }, 200); } else location.reload(); dC = 0; } };
package/index.html CHANGED
@@ -16,44 +16,41 @@
16
16
 
17
17
  <div class="main-container">
18
18
 
19
- <!-- ======================================================= -->
20
- <!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
21
- <!-- ======================================================= -->
19
+ <!-- COLONNA SINISTRA: Dati Rotta e Velocità -->
22
20
  <div class="side-panel left-panel">
23
-
24
- <!-- STW: Velocità attraverso l'acqua -->
21
+ <!-- STW con Sparkline -->
25
22
  <div class="data-box">
26
23
  <div class="label-row"><span class="label">STW</span><span class="unit">kts</span></div>
27
24
  <span class="value" id="stw">0.0</span>
28
25
  <div class="graph-wrapper">
29
26
  <svg class="sparkline" id="stw-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
30
- <div class="scale-labels right" id="stw-scale"></div>
27
+ <div class="scale-labels" id="stw-scale"></div>
31
28
  </div>
32
29
  </div>
33
30
 
34
- <!-- SOG: Velocità sul fondo (GPS) -->
31
+ <!-- SOG con Sparkline -->
35
32
  <div class="data-box">
36
33
  <div class="label-row"><span class="label">SOG</span><span class="unit">kts</span></div>
37
34
  <span class="value" id="sog">0.0</span>
38
35
  <div class="graph-wrapper">
39
36
  <svg class="sparkline" id="sog-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
40
- <div class="scale-labels right" id="sog-scale"></div>
37
+ <div class="scale-labels" id="sog-scale"></div>
41
38
  </div>
42
39
  </div>
43
40
 
44
- <!-- HEADING: Prua Bussola (Media 60s) -->
41
+ <!-- HEADING MEAN -->
45
42
  <div class="data-box">
46
43
  <div class="label-row"><span class="label">HEADING MEAN</span></div>
47
44
  <span class="value value-large" id="hdg">000&deg;</span>
48
45
  </div>
49
46
 
50
- <!-- COG: Rotta sul fondo (Media 60s) -->
47
+ <!-- COG MEAN -->
51
48
  <div class="data-box">
52
49
  <div class="label-row"><span class="label">COG MEAN</span></div>
53
50
  <span class="value value-large" id="cog">000&deg;</span>
54
51
  </div>
55
52
 
56
- <!-- TACK: Calcolo mure opposte (HDG e COG previsti) -->
53
+ <!-- TACK: Previsione mure opposte -->
57
54
  <div class="data-box">
58
55
  <div class="label-row"><span class="label">TACK</span></div>
59
56
  <div class="dual-value-container">
@@ -69,152 +66,107 @@
69
66
  </div>
70
67
  </div>
71
68
 
72
- <!-- ======================================================= -->
73
- <!-- CENTRO: Strumento Vento SVG (Ingrandito e Ottimizzato) -->
74
- <!-- ======================================================= -->
69
+ <!-- CENTRO: Strumento Vento SVG -->
75
70
  <div class="center-panel">
76
- <!-- ViewBox ottimizzato (35 38 330 375) per ingrandire il diametro del quadrante -->
77
71
  <svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
78
72
  <defs>
79
- <!-- Gradienti e Maschere -->
80
73
  <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" /><stop offset="100%" style="stop-color:#888888;stop-opacity:1" /></linearGradient>
81
74
  <linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" /><stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" /><stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" /><stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" /><stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" /></linearGradient>
82
75
  <clipPath id="leeway-clip"><rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" /></clipPath>
83
-
84
- <!-- Filtro Glow per l'area di interazione centrale -->
85
- <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
86
- <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
87
- </filter>
76
+ <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" /></filter>
88
77
  </defs>
89
78
 
90
- <!-- Sfondo circolare del quadrante -->
91
- <circle cx="200" cy="200" r="160" fill="#050505" />
92
- <circle cx="200" cy="200" r="125" fill="#121212" />
93
-
94
- <!-- Settori Vento (Rosso/Verde/Arancio) -->
79
+ <circle cx="200" cy="200" r="160" fill="#050505" /><circle cx="200" cy="200" r="125" fill="#121212" />
95
80
  <path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" opacity="1"/>
96
81
  <path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" opacity="1"/>
97
82
  <path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" opacity="1"/>
98
83
 
99
- <!-- Tacche (Generate da Javascript) e Etichette Gradi -->
100
84
  <g id="ticks"></g>
101
85
  <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
102
- <text font-size="16" transform="translate(200, 65)">0</text>
103
- <text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
104
- <text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
105
- <text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
86
+ <text font-size="16" transform="translate(200, 65)">0</text><text font-size="16" transform="translate(335, 200) rotate(90)">90</text><text font-size="16" transform="translate(65, 200) rotate(-90)">90</text><text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
106
87
  <text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text><text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
107
88
  <text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text><text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
108
89
  <text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text><text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
109
90
  <text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text><text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
110
91
  </g>
111
92
 
112
- <!-- Puntatore Track (Blu) -->
113
93
  <g id="track-pointer" transform="rotate(0, 200, 200)"><path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" /></g>
114
-
115
- <!-- Pulsante Fullscreen Hotspot (Disco cliccabile al centro) -->
116
94
  <circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
117
-
118
- <!-- Icona Barca (Centrata visivamente con traslazione Y+5) -->
119
95
  <path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z" fill="url(#axiom-grad)" transform="translate(0, 5)" style="pointer-events: none;" />
120
96
 
121
- <!-- Display Velocità Vento Apparente (Al centro sotto la barca) -->
122
97
  <g id="aws-display-group" transform="translate(200, 265)">
123
98
  <text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
124
99
  <text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
125
100
  </g>
126
-
127
- <!-- Lancette AWA (A) e TWA (T) -->
128
101
  <g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.85"><path d="M200,80 L211,95 L200,145 L189,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" /><text x="200" y="102" fill="#000" font-size="11" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text></g>
129
102
  <g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85"><path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" /><text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text></g>
130
103
 
131
- <!-- Barra LEEWAY / DRIFT (Posizionata più in alto a Y=372) -->
132
104
  <g transform="translate(75, 395)">
133
105
  <text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0&deg;</text>
134
- <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
135
- <rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
106
+ <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" /><rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
136
107
  <g stroke="#555" stroke-width="1">
137
- <line x1="0" y1="-2" x2="0" y2="14" /><line x1="31.25" y1="2" x2="31.25" y2="10" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
138
- <line x1="93.75" y1="3" x2="93.75" y2="9" /><line x1="125" y1="-2" x2="125" y2="14" /><line x1="156.25" y1="3" x2="156.25" y2="9" />
139
- <line x1="187.5" y1="2" x2="187.5" y2="10" /><line x1="218.75" y1="2" x2="218.75" y2="10" /><line x1="250" y1="-2" x2="250" y2="14" />
140
- </g>
141
- <g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
142
- <text x="0" y="24">-20&deg;</text><text x="62.5" y="24">-10</text><text x="125" y="24">0&deg;</text><text x="187.5" y="24">10</text><text x="250" y="24">20&deg;</text>
108
+ <line x1="0" y1="-2" x2="0" y2="14" /><line x1="31.25" y1="2" x2="31.25" y2="10" /><line x1="62.5" y1="2" x2="62.5" y2="10" /><line x1="93.75" y1="3" x2="93.75" y2="9" /><line x1="125" y1="-2" x2="125" y2="14" /><line x1="156.25" y1="3" x2="156.25" y2="9" /><line x1="187.5" y1="2" x2="187.5" y2="10" /><line x1="218.75" y1="2" x2="218.75" y2="10" /><line x1="250" y1="-2" x2="250" y2="14" />
143
109
  </g>
110
+ <g fill="#555" font-size="8" text-anchor="middle" font-weight="bold"><text x="0" y="24">-20&deg;</text><text x="62.5" y="24">-10</text><text x="125" y="24">0&deg;</text><text x="187.5" y="24">10</text><text x="250" y="24">20&deg;</text></g>
144
111
  </g>
145
112
  </svg>
146
113
  </div>
147
114
 
148
- <!-- ======================================================= -->
149
- <!-- COLONNA DESTRA: Profondità e Vento Reale -->
150
- <!-- ======================================================= -->
115
+ <!-- COLONNA DESTRA: Dati Vento e Profondità -->
151
116
  <div class="side-panel right-panel">
152
-
153
- <!-- DEPTH: Profondità sotto il trasduttore -->
117
+ <!-- DEPTH -->
154
118
  <div class="data-box">
155
119
  <div class="label-row"><span class="unit">m</span><span class="label">DEPTH</span></div>
156
120
  <span class="value" id="depth">--.-</span>
157
121
  <div class="graph-wrapper">
158
- <div class="scale-labels left" id="depth-scale"></div>
122
+ <div class="scale-labels" id="depth-scale"></div>
159
123
  <svg class="sparkline" id="depth-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
160
124
  </div>
161
125
  </div>
162
126
 
163
- <!-- TWS: Velocità del vento reale -->
127
+ <!-- TWS -->
164
128
  <div class="data-box">
165
129
  <div class="label-row"><span class="unit">kts</span><span class="label">TWS</span></div>
166
130
  <span class="value" id="tws">0.0</span>
167
131
  <div class="graph-wrapper">
168
- <div class="scale-labels left" id="tws-scale"></div>
132
+ <div class="scale-labels" id="tws-scale"></div>
169
133
  <svg class="sparkline" id="tws-graph" viewBox="0 0 200 40" preserveAspectRatio="none"></svg>
170
134
  </div>
171
135
  </div>
172
136
 
173
- <!-- TWA: Angolo del vento reale (Media 60s) -->
137
+ <!-- TWA MEAN -->
174
138
  <div class="data-box">
175
139
  <div class="label-row"><span class="label">TWA MEAN</span></div>
176
140
  <span class="value value-large" id="twa-avg">---&deg;</span>
177
141
  </div>
178
142
 
179
- <!-- AWA: Angolo del vento apparente (Media 60s) -->
143
+ <!-- AWA MEAN -->
180
144
  <div class="data-box">
181
145
  <div class="label-row"><span class="label">AWA MEAN</span></div>
182
146
  <span class="value value-large" id="awa-avg">---&deg;</span>
183
147
  </div>
184
148
 
185
- <!-- TWD: Direzione reale del vento (Media 60s + Bussola) -->
149
+ <!-- TWD MEAN con Bussola -->
186
150
  <div class="data-box">
187
151
  <div class="label-row"><span class="label">TWD MEAN</span></div>
188
152
  <div class="value-with-compass">
189
153
  <svg class="mini-compass" viewBox="0 0 40 40">
190
- <!-- Sfondo e bordo bussola -->
191
154
  <circle cx="20" cy="20" r="19" fill="#151515" stroke="#444" stroke-width="1.5"/>
192
-
193
- <!-- Etichetta Nord (N) -->
194
- <text x="20" y="8" fill="#e74c3c" font-size="6" text-anchor="middle" font-weight="900" font-family="Arial">N</text>
195
-
196
- <!-- Tacche cardinali (E, S, W) -->
155
+ <text x="20" y="8" fill="#e74c3c" font-size="6" text-anchor="middle" font-weight="900">N</text>
197
156
  <g stroke="#555" stroke-width="0.8">
198
- <line x1="20" y1="11" x2="20" y2="13"/> <!-- Nord marker -->
199
- <line x1="30" y1="20" x2="27" y2="20"/> <!-- Est -->
200
- <line x1="20" y1="30" x2="20" y2="27"/> <!-- Sud -->
201
- <line x1="10" y1="20" x2="13" y2="20"/> <!-- Ovest -->
157
+ <line x1="20" y1="11" x2="20" y2="13"/><line x1="30" y1="20" x2="27" y2="20"/><line x1="20" y1="30" x2="20" y2="27"/><line x1="10" y1="20" x2="13" y2="20"/>
202
158
  </g>
203
-
204
- <!-- Freccia TWD (Gialla, forma a punta di freccia aeronautica) -->
205
159
  <g id="twd-arrow" transform="rotate(0, 20, 20)">
206
160
  <path d="M20,10 L16,26 L20,23 L24,26 Z" fill="#ffff00" stroke="#000" stroke-width="0.5"/>
207
161
  </g>
208
-
209
- <!-- Perno centrale -->
210
162
  <circle cx="20" cy="20" r="1.5" fill="#555" />
211
163
  </svg>
212
164
  <span class="value value-large" id="twd-avg">---&deg;</span>
213
165
  </div>
214
166
  </div>
215
167
  </div>
168
+
216
169
  </div>
217
-
218
170
  <script src="app.js"></script>
219
171
  </body>
220
172
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/style.css CHANGED
@@ -1,221 +1,102 @@
1
1
  /* ==========================================================================
2
- 1. BASE E STRUTTURA GENERALE (GRID LAYOUT)
2
+ 1. BASE E STRUTTURA GENERALE
3
3
  ========================================================================== */
4
4
  body {
5
5
  background-color: #000;
6
6
  color: #fff;
7
7
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
8
- margin: 0;
9
- padding: 0;
10
- height: 100vh;
11
- width: 100vw;
8
+ margin: 0; padding: 0;
9
+ height: 100vh; width: 100vw;
12
10
  overflow: hidden;
11
+ /* BLOCCO TOTALE GESTI DI SISTEMA (Android/iOS) */
12
+ -webkit-touch-callout: none;
13
+ -webkit-user-select: none;
14
+ user-select: none;
15
+ touch-action: none;
13
16
  }
14
17
 
15
18
  .main-container {
16
19
  display: grid;
17
- width: 100%;
18
- height: 100%;
19
- padding: 5px;
20
- box-sizing: border-box;
20
+ width: 100%; height: 100%;
21
+ padding: 5px; box-sizing: border-box;
21
22
  gap: 8px;
22
- /* Proporzioni Desktop Standard */
23
- grid-template-columns: 1fr 3.5fr 1fr;
23
+ grid-template-columns: 1.5fr 3.5fr 1.5fr;
24
24
  grid-template-rows: 100%;
25
- transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
26
- }
27
-
28
- /* Ottimizzazione per schermi molto larghi (es. 16:10, 21:9) */
29
- @media (min-aspect-ratio: 1.6) {
30
- .main-container {
31
- grid-template-columns: 1.4fr 3fr 1.4fr;
32
- gap: 15px;
33
- }
25
+ transition: all 0.4s ease;
34
26
  }
35
27
 
36
28
  /* ==========================================================================
37
29
  2. PANNELLI E DATA-BOX
38
30
  ========================================================================== */
39
- .side-panel {
40
- display: flex;
41
- flex-direction: column;
42
- background: rgba(255, 255, 255, 0.02);
43
- border-radius: 10px;
44
- z-index: 10;
45
- height: 100%;
46
- }
47
-
31
+ .side-panel { display: flex; flex-direction: column; background: rgba(255, 255, 255, 0.03); border-radius: 12px; height: 100%; overflow: hidden; }
48
32
  .left-panel { grid-column: 1; }
33
+ .center-panel { grid-column: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; overflow: hidden; }
49
34
  .right-panel { grid-column: 3; align-items: flex-end; text-align: right; }
50
35
 
51
- .center-panel {
52
- grid-column: 2;
53
- display: flex;
54
- flex-direction: column;
55
- align-items: center;
56
- justify-content: center;
57
- height: 100%;
58
- overflow: hidden;
59
- }
60
-
61
36
  .data-box {
62
37
  position: relative;
63
38
  border-bottom: 1px solid #222;
64
- padding: 8px 10px;
39
+ padding: 8px 12px;
65
40
  display: flex;
66
41
  flex-direction: column;
67
- width: 100%;
68
- box-sizing: border-box;
69
- container-type: size; /* Cruciale per il calcolo cqh */
42
+ width: 100%; box-sizing: border-box;
43
+ container-type: size;
70
44
  flex: 1;
45
+ overflow: hidden;
71
46
  }
72
47
 
73
- /* Definizione altezze percentuali basate sul Viewport */
48
+ /* Altezze Desktop */
74
49
  .data-box:nth-child(1), .data-box:nth-child(2) { height: 25vh; }
75
50
  .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 16.666vh; }
76
51
 
77
52
  /* ==========================================================================
78
- 3. TACTICAL FOCUS MODE (50/50 SPLIT)
53
+ 3. TACTICAL FOCUS MODE (ORIZZONTALE)
79
54
  ========================================================================== */
80
55
  .focus-active { grid-template-columns: 1fr 1fr !important; }
81
56
  .focus-active .side-panel:not(.has-focus) { display: none !important; }
82
57
 
83
- .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1; }
84
- .focus-active.focus-side-left .center-panel { grid-column: 2; }
85
- .focus-active.focus-side-right .center-panel { grid-column: 1; }
86
- .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2; }
58
+ /* Simmetria Orizzontale */
59
+ .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
60
+ .focus-active.focus-side-left .center-panel { grid-column: 2 !important; }
61
+ .focus-active.focus-side-right .center-panel { grid-column: 1 !important; }
62
+ .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
87
63
 
88
64
  .focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
89
- .focus-active .has-focus .data-box.is-focused { height: 100vh !important; border: none; background: rgba(255, 255, 255, 0.04); }
65
+ .focus-active .has-focus .data-box.is-focused { height: 100vh !important; border: none; background: rgba(255, 255, 255, 0.05); }
90
66
 
67
+ /* Tipografia e Grafica in Focus */
91
68
  .focus-active .is-focused .value { font-size: clamp(3rem, 18cqh, 8rem) !important; margin-top: 10px; }
92
- .focus-active .is-focused .scale-labels { font-size: 22px !important; min-width: 35px !important; }
93
-
94
- /* Bussola Esplosa in Focus Mode */
95
- .focus-active .is-focused .mini-compass {
96
- width: 45vh !important;
97
- height: 45vh !important;
98
- }
99
-
69
+ .focus-active .is-focused .scale-labels { font-size: 22px !important; min-width: 40px !important; }
100
70
  .focus-active .is-focused .sparkline path { stroke-width: 0.8px !important; }
101
- .focus-active .is-focused .line-hercules { stroke-width: 1.2px !important; filter: drop-shadow(0 0 3px #ff0000); }
102
71
 
103
72
  /* ==========================================================================
104
- 4. TIPOGRAFIA DINAMICA (ELASTICA CQH)
73
+ 4. TIPOGRAFIA DINAMICA (CQH)
105
74
  ========================================================================== */
106
75
  .label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
107
- .label { color: #666; font-size: 0.6rem; font-weight: bold; text-transform: uppercase; }
108
- .unit { color: #888; font-size: 0.55rem; font-weight: bold; }
109
-
110
- .value {
111
- color: #fff;
112
- font-size: clamp(1.2rem, 22cqh, 3rem);
113
- font-weight: 600;
114
- line-height: 0.9;
115
- letter-spacing: -1px;
116
- transition: color 0.3s ease;
117
- padding-bottom: 5px;
118
- }
119
-
120
- .value-large {
121
- margin-top: auto;
122
- margin-bottom: auto;
123
- font-size: clamp(1.5rem, 35cqh, 4rem);
124
- line-height: 0.85;
125
- }
126
-
127
- /* TACK Layout - Centratura verticale e compattezza */
128
- .dual-value-container {
129
- display: flex;
130
- justify-content: space-between;
131
- align-items: center;
132
- width: 100%;
133
- /* La "molla" auto sopra e sotto lo tiene al centro esatto del box */
134
- margin-top: auto;
135
- margin-bottom: auto;
136
- padding-bottom: 5px;
137
- }
138
-
139
- .dual-value-col {
140
- display: flex;
141
- flex-direction: column;
142
- width: 48%;
143
- /* Rimosso height 100% per non farlo allungare fino al titolo */
144
- }
145
-
146
- .dual-label {
147
- color: #666;
148
- font-size: 0.55rem;
149
- font-weight: bold;
150
- text-transform: uppercase;
151
- margin-bottom: 2px; /* Distanza fissa e piccola dal numero sottostante */
152
- }
153
-
154
- .value.dual-val {
155
- font-size: clamp(1rem, 22cqh, 1.8rem);
156
- padding-bottom: 0;
157
- line-height: 0.9;
158
- }
76
+ .label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
77
+ .unit { color: #888; font-size: 0.6rem; font-weight: bold; }
78
+ .value { color: #fff; font-size: clamp(1.2rem, 22cqh, 3rem); font-weight: 600; line-height: 0.9; letter-spacing: -1px; padding-bottom: 5px; }
159
79
 
160
- /* BUSSOLA TWD: Centrata e proporzionata */
161
- .value-with-compass {
162
- display: flex;
163
- justify-content: space-between;
164
- align-items: center;
165
- width: 100%;
166
- margin-top: auto;
167
- margin-bottom: auto;
168
- gap: 8px;
169
- }
80
+ .value-large, .dual-value-container, .value-with-compass { margin-top: auto; margin-bottom: auto; }
81
+ .value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
170
82
 
171
- .mini-compass {
172
- /* Impostata a 60% dell'altezza come da tua prova */
173
- width: clamp(45px, 60cqh, 120px);
174
- height: clamp(45px, 60cqh, 120px);
175
- background: #000;
176
- border-radius: 50%;
177
- flex-shrink: 0;
178
- border: 1.5px solid #333;
179
- box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
180
- transition: all 0.4s ease;
181
- }
83
+ .dual-value-container { display: flex; justify-content: space-between; width: 100%; }
84
+ .dual-value-col { display: flex; flex-direction: column; width: 48%; }
85
+ .dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
86
+ .value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
182
87
 
183
- .value-with-compass .value-large {
184
- margin: 0;
185
- flex-grow: 1;
186
- text-align: right;
187
- font-size: clamp(1.2rem, 35cqh, 4rem);
188
- }
88
+ .value-with-compass { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 10px; }
89
+ .mini-compass { width: clamp(40px, 60cqh, 120px); height: clamp(40px, 60cqh, 120px); background: #000; border-radius: 50%; flex-shrink: 0; border: 1.5px solid #333; box-shadow: inset 0 0 10px rgba(0,0,0,0.8); }
90
+ .value-with-compass .value-large { margin: 0; flex-grow: 1; text-align: right; font-size: clamp(1.2rem, 35cqh, 4rem); }
189
91
 
190
92
  /* ==========================================================================
191
93
  5. GRAFICI E SCALE
192
94
  ========================================================================== */
193
- .graph-wrapper {
194
- position: relative;
195
- width: 100%;
196
- flex-grow: 1;
197
- min-height: 20px;
198
- margin-top: 4px;
199
- display: flex;
200
- align-items: stretch;
201
- gap: 4px;
202
- }
95
+ .graph-wrapper { position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px; display: flex; align-items: stretch; gap: 6px; }
203
96
  .sparkline { flex-grow: 1; height: 100%; background: rgba(255, 255, 255, 0.03); border-radius: 4px; }
204
- .sparkline path { stroke-width: 1px !important; }
205
-
206
- .scale-labels {
207
- display: flex;
208
- flex-direction: column;
209
- justify-content: space-between;
210
- font-size: 10px;
211
- color: #666;
212
- font-weight: bold;
213
- min-width: 16px;
214
- height: 100%;
215
- line-height: 1;
216
- transition: all 0.3s ease;
217
- }
97
+ .sparkline path, .sparkline line { stroke-width: 1px !important; transition: all 0.3s ease; }
218
98
 
99
+ .scale-labels { display: flex; flex-direction: column; justify-content: space-between; font-size: 10px; color: #777; font-weight: bold; min-width: 20px; height: 100%; line-height: 1; }
219
100
  .left-panel .scale-labels { order: 2; text-align: left; }
220
101
  .left-panel .sparkline { order: 1; }
221
102
  .right-panel .scale-labels { order: 1; text-align: right; }
@@ -226,35 +107,56 @@ body {
226
107
  #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
227
108
  #tws-graph { stroke: #f1c40f; fill: rgba(241, 196, 15, 0.12); }
228
109
 
229
- /* ==========================================================================
230
- 6. HERCULES E ANIMAZIONI
231
- ========================================================================== */
232
110
  .line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
233
- .box-hercules { background: rgba(255, 0, 0, 0.08) !important; transition: all 0.3s ease; }
234
- .box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900; letter-spacing: 1px; opacity: 0.8; }
111
+ .box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
112
+ .box-hercules::after { content: "HERCULES"; position: absolute; bottom: 4px; right: 35px; font-size: 7px; color: #ff4444; font-weight: 900; opacity: 0.8; }
235
113
 
114
+ /* ==========================================================================
115
+ 6. STATI E ANIMAZIONI
116
+ ========================================================================== */
236
117
  #status { position: absolute; top: 5px; right: 15px; font-size: 0.5rem; text-transform: uppercase; z-index: 1000; }
237
118
  .online { color: #2ecc71; opacity: 0.5; }
238
119
  .offline { color: #e74c3c; font-weight: bold; }
239
-
240
120
  #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow { transition: all 0.6s cubic-bezier(0.1, 0.7, 0.1, 1); }
241
121
  #leeway-mask-rect { transition: none; }
242
122
  .alarm-warning { color: #f1c40f !important; }
243
123
  .alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
244
124
  @keyframes blink-unstable { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
245
125
  .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
246
-
247
126
  #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
248
127
 
249
128
  /* ==========================================================================
250
- 7. RESPONSIVE (PORTRAIT)
129
+ 7. RESPONSIVE (PORTRAIT) - FIX DEFINITIVO 2x2 E FOCUS
251
130
  ========================================================================== */
252
131
  @media (max-aspect-ratio: 0.9 / 1) {
253
- .main-container { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
254
- .center-panel { grid-column: 1 / span 2; grid-row: 1; height: 50vh; }
255
- .left-panel { grid-column: 1; grid-row: 2; height: 50vh; }
256
- .right-panel { grid-column: 2; grid-row: 2; height: 50vh; }
257
- .data-box:nth-child(1), .data-box:nth-child(2) { height: 12.5vh; }
258
- .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 8.333vh; }
132
+ .main-container {
133
+ grid-template-columns: 1fr 1fr !important;
134
+ grid-template-rows: 45vh 55vh !important;
135
+ }
136
+
137
+ /* Ordine Standard Portrait: Vento (1), SX (2), DX (3) */
138
+ .center-panel { grid-row: 1 !important; grid-column: 1 / span 2 !important; }
139
+ .left-panel { grid-row: 2 !important; grid-column: 1 !important; }
140
+ .right-panel { grid-row: 2 !important; grid-column: 2 !important; }
141
+
142
+ /* FOCUS MODE IN VERTICALE: Abbandoniamo la Grid, usiamo Flex per sicurezza */
143
+ .main-container.focus-active {
144
+ display: flex !important;
145
+ flex-direction: column !important;
146
+ }
147
+ .focus-active .center-panel {
148
+ flex: 0 0 45vh !important;
149
+ width: 100% !important;
150
+ }
151
+ .focus-active .side-panel.has-focus {
152
+ flex: 0 0 55vh !important;
153
+ width: 100% !important;
154
+ display: flex !important;
155
+ }
156
+
157
+ /* Calibrazione altezze box per Portrait */
158
+ .data-box:nth-child(1), .data-box:nth-child(2) { height: 13.5vh; }
159
+ .data-box:nth-child(3), .data-box:nth-child(4), .data-box:nth-child(5) { height: 9.3vh; }
160
+
259
161
  .value-large { font-size: clamp(1.2rem, 35cqh, 2.5rem); }
260
162
  }