Il contesto

Il pannello di controllo che ho costruito per gli agenti commerciali include anche la gestione dei terminali [BRAND_POS]. Il problema pratico: chi fornisce assistenza spesso non sa com'è fatta l'app del telefono che accompagna il dispositivo. Non l'ha mai installata, non l'ha mai aperta.

La soluzione ovvia sarebbe un PDF con gli screenshot. Ma un PDF è statico, noioso e nessuno lo legge. L'idea era diversa: creare qualcosa che sembri l'app stessa — si vede la schermata Home, si clicca sul tasto "Carte" in basso, e appare la schermata Carte. Esattamente come sul telefono reale, ma dentro il browser, dentro il pannello.

ℹ️

Stack: Firebase Storage per le immagini, JavaScript vanilla dentro un modal glassmorphism già presente nel pannello. Nessuna libreria esterna.

L'idea tecnica: hotspot in percentuale su una macchina a stati

La tecnica giusta non è ritagliare le immagini in più pezzi. Ogni schermata è un'immagine intera (lo screenshot del telefono). Sopra ci si posizionano dei rettangoli cliccabili invisibili — gli hotspot — definiti in percentuale rispetto all'immagine, non in pixel fissi. Questo garantisce che restino allineati correttamente su qualsiasi dimensione del modal.

L'intera navigazione è una macchina a stati minima: ogni screen ha un ID, un'immagine e una lista di hotspot. Ogni hotspot specifica solo dove porta (target) e le sue coordinate percentuali. La funzione renderScreen(id) disegna l'immagine e sovrappone gli hotspot — nient'altro.

javascript — struttura dati BRAND_SCREENS
const BRAND_SCREENS = {
  home: {
    img: 'Home.png',
    hotspots: [
      { top:88, left:0,  w:25, h:12, target:'carte',  label:'Carte'  },
      { top:88, left:25, w:25, h:12, target:'altro',  label:'Altro'  },
      { top:88, left:50, w:25, h:12, target:'disp',   label:'Dispositivi' },
    ]
  },
  carte: {
    img: 'Carte.png',
    hotspots: [
      { top:2, left:2, w:12, h:7, target:'home', label:'← Home' },
      // ... altri hotspot
    ]
  },
  // altri screen...
};

Le coordinate top, left, w, h sono tutte in percentuale (0–100). Un hotspot { top:88, left:0, w:25, h:12 } occupa il quarto inferiore sinistro dell'immagine — indipendentemente da quanto è alta la finestra del modal.

Fase 1: l'editor di hotspot

Il problema pratico è misurare le coordinate. Farlo a occhio su un'immagine sarebbe lento e frustrante, soprattutto con 20 schermate e decine di zone cliccabili. Ho costruito prima un editor standalone — un singolo file HTML che non ha bisogno di server, si apre direttamente nel browser.

  • 1
    Carica uno screenshot

    Si aggiunge uno screen con un ID (es. home) e si carica l'immagine. L'editor la mostra a schermo intero nel canvas.

  • 2
    Disegna gli hotspot col mouse

    Si trascina sul canvas per disegnare un rettangolo. Le coordinate vengono calcolate automaticamente in percentuale rispetto all'immagine.

  • 3
    Collega la destinazione

    Per ogni zona si sceglie dal menu a tendina quale screen aprire. Il menu si popola con tutti gli screen già creati. Finché non c'è una destinazione, la zona resta gialla con un "?".

  • 4
    Esporta il codice

    Il pulsante "Genera codice" produce direttamente l'oggetto BRAND_SCREENS pronto da incollare nell'app. C'è anche Esporta/Importa in JSON per non perdere il lavoro tra una sessione e l'altra.

💡

Hotspot spostabili e ridimensionabili: ogni zona può essere trascinata per spostarla o ridimensionata dall'angolino in basso a destra. Si elimina con Canc. Molto più comodo che riscrivere le percentuali a mano ogni volta.

Fase 2: il modal interattivo nel pannello

Hotspot visibili, non trasparenti

Una scelta di design importante: gli hotspot nel modal finale non sono invisibili. Non avendo tutti gli screen disponibili, l'operatore deve sapere esattamente dove può cliccare e dove l'immagine è "muta". La soluzione: un alone blu pulsante sopra ogni zona cliccabile.

css — glow pulsante sugli hotspot
/* Animazione keyframe */
@keyframes mpgGlow {
  0%, 100% { box-shadow: 0 0 6px 2px rgba(14,165,233,.35); }
  50%       { box-shadow: 0 0 14px 5px rgba(14,165,233,.6); }
}

/* L'hotspot cliccabile */
.mpg-hotspot {
  position: absolute;
  border: 1px solid rgba(14,165,233,.7);
  background: rgba(14,165,233,.12);
  border-radius: 6px;
  cursor: pointer;
  animation: mpgGlow 2s ease-in-out infinite;
}

/* Le zone senza destinazione non hanno classe → nessun glow */

Firebase Storage: getDownloadURL invece di img src

Il primo tentativo era semplice: costruire l'URL di Firebase Storage a mano e usarlo come src dell'<img>. Non ha funzionato — le immagini restano bianche.

Il motivo: Firebase Storage può avere regole di sicurezza che richiedono autenticazione. Un tag <img src="..."> non può mandare il token di autenticazione dell'utente corrente — è una richiesta HTTP semplice, senza credenziali. La soluzione è usare direttamente l'SDK:

javascript — prima (non funziona con auth)
// URL costruito a mano → bloccato dalle regole Firebase
img.src = `https://firebasestorage.googleapis.com/v0/b/
  BUCKET/o/AppBRAND%2FHome.png?alt=media`;
javascript — dopo (auth-aware con SDK)
async function _guideLoadImg(screenId) {
  const imgFile = BRAND_SCREENS[screenId].img;
  // getDownloadURL gestisce automaticamente il token di autenticazione
  const url = await firebase.storage()
    .ref(`AppBRAND/${imgFile}`)
    .getDownloadURL();
  return url;
}

Auto-scala per riempire il modal senza scrollbar

L'immagine di uno screenshot da telefono è verticale — alta e stretta. Il modal potrebbe essere troppo basso, causando una scrollbar, oppure troppo largo, lasciando spazio vuoto ai lati. La soluzione è misurare lo spazio disponibile e applicare un transform: scale() all'intero stage (immagine + hotspot insieme), così le proporzioni e l'allineamento degli hotspot restano perfetti.

javascript — _guideFitStage()
function _guideFitStage() {
  const body  = document.getElementById('mpgBody');
  const stage = document.getElementById('mpgStage');
  const img   = stage.querySelector('img');
  if (!img || !img.naturalHeight) return;

  const availH = body.clientHeight - 8;    // 8px di margine
  const availW = body.clientWidth  - 8;
  const scaleH = availH / img.naturalHeight;
  const scaleW = availW / img.naturalWidth;
  const scale  = Math.min(scaleH, scaleW, 1); // mai ingrandire oltre 1:1

  stage.style.transform       = `scale(${scale})`;
  stage.style.transformOrigin = 'top center';
}

Un ResizeObserver sul body del modal chiama _guideFitStage() ogni volta che la finestra cambia dimensione, così la scala si aggiorna in tempo reale.

I problemi che ho incontrato

1. flex:1 non si espande senza height esplicita sul parent

Il corpo del modal (l'area dove sta l'immagine) aveva flex: 1 per occupare tutto lo spazio disponibile. Ma restava a altezza zero.

Il motivo è una regola CSS poco intuitiva: flex: 1 su un figlio funziona solo se il parent ha un'altezza definita in modo esplicito — non basta max-height. Con solo max-height: 88vh sul contenitore, il browser non sa quale sia l'altezza di riferimento per distribuire lo spazio tra i figli flex.

css — prima (body collassa a 0)
#mpgBox {
  display: flex;
  flex-direction: column;
  max-height: 88vh;   /* ← non basta come riferimento per flex:1 */
}
#mpgBody { flex: 1; }
css — dopo (funziona)
#mpgBox {
  display: flex;
  flex-direction: column;
  height: min(94vh, 960px);  /* height esplicita → flex:1 funziona */
}
#mpgBody {
  flex: 1;
  min-height: 0;   /* necessario per evitare overflow in column layout */
  overflow: hidden;
}
⚠️

Regola da ricordare: in un flex container con flex-direction: column, aggiungere sempre height esplicita sul parent e min-height: 0 sui figli con flex: 1. Senza min-height: 0, il figlio può traboccare oltre il contenitore anche se la height sembra corretta.

2. Immagini bianche: l'errore silenzioso di Firebase Storage

Con l'URL di Firebase costruito a mano, il tag <img> non mostrava errori nel DOM — restava semplicemente bianco. Non c'era nessun messaggio in console. Solo controllando la tab Network si vedeva la risposta 401 Unauthorized.

La causa: Firebase Storage di default richiede autenticazione per leggere i file, e una richiesta HTTP normale da <img src> non porta il token JWT dell'utente loggato. getDownloadURL() dell'SDK risolve il problema perché costruisce internamente un URL con token temporaneo già incluso.

3. Navigazione lenta: ~400ms per schermata

La prima versione funzionante faceva una chiamata getDownloadURL() ad ogni click di navigazione. Ogni chiamata aggiunge ~300–500ms di latenza percepita — abbastanza da rendere la guida scomoda da usare.

La soluzione in due livelli:

  • Cache in memoria — la prima volta che si naviga su uno screen, la URL viene salvata in _guideUrlCache. La seconda volta (o dopo aver riaperto il modal) non si fa più alcuna chiamata a Firebase.
  • Preload in background — appena il modal si apre, parte _guidePreloadAll(): lancia in parallelo le getDownloadURL() per tutti e 20 gli screen. Il modal si apre sulla Home immediatamente; quando l'utente naviga al secondo screen, la URL è già pronta in cache.
javascript — cache + preload
var _guideUrlCache = {};

async function _guideGetUrl(imgFile) {
  if (_guideUrlCache[imgFile]) return _guideUrlCache[imgFile]; // cache hit
  const url = await firebase.storage().ref(`AppBRAND/${imgFile}`).getDownloadURL();
  _guideUrlCache[imgFile] = url;
  return url;
}

function _guidePreloadAll() {
  // Lancia tutte le chiamate in parallelo senza aspettare i risultati
  Object.values(BRAND_SCREENS).forEach(s => _guideGetUrl(s.img));
}

Il risultato finale

La guida è accessibile dalla pagina Utilità del pannello con un pulsante dedicato (icona 📱, gradiente teal/cyan). Al click si apre un modal glassmorphism con:

  • 20 schermate dell'app [BRAND_POS], navigate esattamente come sul telefono
  • 52 hotspot visibili con glow blu pulsante — l'operatore sa esattamente dove cliccare
  • Pulsante ← Indietro nella header quando non si è sulla Home, con history stack completo
  • Navigazione istantanea dopo il primo caricamento grazie alla cache URL + preload in background
  • Auto-scala dell'immagine per riempire sempre il modal senza scrollbar, su qualsiasi schermo
💡

Editor riutilizzabile: il file HTML dell'editor di hotspot è completamente separato dall'app. Può essere usato per qualsiasi guida basata su screenshot — basta sostituire le immagini e riesportare il codice. Non serve installare nulla.


Takeaway

  1. Hotspot in percentuale, non in pixel. Le coordinate percentuali restano allineate su qualsiasi dimensione del contenitore. I pixel fissi si rompono al primo resize.
  2. Firebase Storage richiede getDownloadURL. Non costruire l'URL a mano per passarlo a <img src> — se lo Storage è protetto da auth, l'immagine non caricherà senza errori visibili.
  3. flex: 1 in column layout vuole height esplicita sul parent. max-height non basta. Aggiungere anche min-height: 0 sui figli per evitare overflow silenziosi.
  4. Cache + preload eliminano la latenza percepita. Una singola chiamata per screen al primo accesso è accettabile; farla ad ogni navigazione non lo è. Bastano un dizionario in memoria e un preload parallelo all'apertura.
← Torna al Blog