Compare commits

...

4 Commits

Author SHA1 Message Date
09c9706938 docs: aggiungi requisiti di sviluppo al README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:35:11 +01:00
ecbae836b3 feat: guida utente in-app e fix grafici Convertitore
- DocsPanel: pannello documentazione slide-from-right con nav a pill,
  IntersectionObserver per sezione attiva, card con step numerati e callout tip
- InfoPanel: bottone "Guida" emette open-docs invece di aprire link esterno
- App: integra DocsPanel con v-model showDocs
- Converter: fix capitalize food names (capFirst invece di text-transform),
  simmetria visiva input/output (stesso underline e font-size 1.6rem),
  rimosso CSS morto (doppio align-items, doppio background, visibility hack)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:32:36 +01:00
f24e591ca0 docs: riscrivi guida utente allineata allo stato attuale dell'app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:10:00 +01:00
eef38a74cc Merge branch 'new-logo' 2026-03-27 14:07:32 +01:00
6 changed files with 597 additions and 53 deletions

View File

@@ -33,6 +33,17 @@ App mobile-first per la gestione della dieta quotidiana — pianificazione pasti
| Mobile | Capacitor Android | | Mobile | Capacitor Android |
| Build APK | Docker | | Build APK | Docker |
## Requisiti per lo sviluppo
| Strumento | Versione minima | Note |
|---|---|---|
| Node.js | 18.x | |
| npm | 9.x | incluso con Node.js |
| Git | 2.x | |
| Browser | Chrome / Edge / Firefox recente | DevTools modalità mobile consigliati |
Per la build APK Android sono necessari requisiti aggiuntivi — vedi [docker/README.md](docker/README.md).
## Avvio in sviluppo ## Avvio in sviluppo
```bash ```bash

View File

@@ -4,21 +4,21 @@
BitePlan è organizzata in tre sezioni accessibili dalla barra in basso: BitePlan è organizzata in tre sezioni accessibili dalla barra in basso:
| Icona | Sezione | Funzione | | Icona | Tab | Funzione |
|---|---|---| |---|---|---|
| Calendario | Piano Pasti | Pianifica i pasti della settimana | | Calendario | Pasti | Pianifica i pasti della settimana |
| Bilancia | Convertitore | Converti peso crudo/cotto | | Frecce | Converti | Converti peso crudo/cotto |
| Lista | Lista della spesa | Gestisci la spesa | | Carrello | Spesa | Gestisci la lista della spesa |
--- ---
## Piano Pasti ## Pasti
La sezione mostra i sette giorni della settimana. Il giorno corrente è aperto di default. La sezione mostra i sette giorni della settimana. Il giorno corrente è espanso di default.
### Aggiungere un alimento ### Aggiungere un alimento
1. Tocca il giorno per espandere la card 1. Tocca il giorno per espandere la card (se non è già aperta)
2. Vai al pasto desiderato (Colazione, Pranzo o Cena) 2. Vai al pasto desiderato (Colazione, Pranzo o Cena)
3. Scrivi il nome dell'alimento nel campo di testo 3. Scrivi il nome dell'alimento nel campo di testo
4. Premi **+** o il tasto Invio per aggiungerlo 4. Premi **+** o il tasto Invio per aggiungerlo
@@ -27,43 +27,60 @@ La sezione mostra i sette giorni della settimana. Il giorno corrente è aperto d
Tocca il pulsante **×** a destra dell'elemento. Tocca il pulsante **×** a destra dell'elemento.
### Generare la lista della spesa
Premi il pulsante **Genera lista della spesa** in fondo alla pagina. Tutti gli alimenti pianificati vengono aggiunti alla lista della spesa, senza duplicati rispetto a quelli già presenti. L'app passa automaticamente alla tab Spesa.
### Persistenza ### Persistenza
I dati vengono salvati automaticamente sul dispositivo (LocalStorage). Non è necessario premere nessun pulsante di salvataggio. I dati vengono salvati automaticamente sul dispositivo. Non è necessario premere nessun pulsante di salvataggio.
--- ---
## Convertitore crudo/cotto ## Converti
Calcola il peso cotto a partire dal crudo, o viceversa, in base ai coefficienti di resa di ogni alimento. Calcola il peso cotto a partire dal crudo, o viceversa, in base ai coefficienti di resa di ogni alimento e metodo di cottura.
### Utilizzo ### Utilizzo
1. Cerca l'alimento nel campo di ricerca (es. `pollo`, `riso`) 1. Cerca l'alimento nel campo di ricerca (es. `pollo`, `riso`, `zucchine`)
2. Seleziona il metodo di cottura dall'elenco risultati 2. Seleziona dall'elenco la combinazione alimento + metodo di cottura
3. Scegli la direzione: **Crudo → Cotto** o **Cotto → Crudo** (pulsante swap) 3. Inserisci il peso in grammi
4. Inserisci il peso in grammi 4. Il risultato appare in tempo reale
5. Il risultato appare in tempo reale 5. Per invertire la direzione (cotto → crudo), premi il pulsante **⇄** al centro
### Alimenti disponibili ### Alimenti e metodi disponibili
Proteine: pollo, manzo, maiale, salmone, tonno, uova Il database copre oltre 50 voci suddivise per categoria:
Carboidrati: riso, pasta, lenticchie
Verdure: zucchine, carote, patate, spinaci, broccoli
Per l'elenco completo con metodi di cottura e coefficienti, vedi [conversioni.md](conversioni.md). | Categoria | Esempi |
|---|---|
| Cereali e pasta | Riso (4 varietà), pasta, farro, orzo, quinoa, cous cous |
| Legumi secchi | Ceci, fagioli, lenticchie |
| Verdure | Carote, zucchine, patate, spinaci, broccoli, cavolfiore, asparagi, carciofi, finocchi e altri |
| Carni | Pollo petto, tacchino fesa, hamburger, vitello |
| Pesce | Tonno, merluzzo, spigola, sogliola |
| Uova | Uovo al tegamino, frittata |
I metodi di cottura disponibili variano per alimento: **bollitura**, **padella**, **forno**, **friggitrice ad aria**.
Per l'elenco completo con coefficienti e fonti, vedi [conversioni.md](conversioni.md).
--- ---
## Lista della spesa ## Spesa
### Aggiungere un elemento ### Aggiungere un elemento
Scrivi il nome nel campo in alto e premi **+** o Invio. Scrivi il nome nel campo in alto e premi **+** o Invio.
### Importare dal piano pasti
Vai alla tab **Pasti** e premi **Genera lista della spesa** — gli alimenti pianificati vengono aggiunti automaticamente.
### Spuntare un elemento ### Spuntare un elemento
Tocca la casella a sinistra dell'elemento. Gli elementi completati vengono spostati in fondo con testo barrato. Tocca la casella a sinistra dell'elemento. Gli elementi completati vengono spostati in una sezione separata con testo barrato.
### Rimuovere un elemento ### Rimuovere un elemento

View File

@@ -14,7 +14,8 @@
</svg> </svg>
</button> </button>
<InfoPanel v-model="showInfo" /> <InfoPanel v-model="showInfo" @open-docs="showDocs = true" />
<DocsPanel v-model="showDocs" />
</div> </div>
</template> </template>
@@ -22,12 +23,14 @@
import { ref } from 'vue' import { ref } from 'vue'
import BottomNav from './components/BottomNav.vue' import BottomNav from './components/BottomNav.vue'
import InfoPanel from './components/InfoPanel.vue' import InfoPanel from './components/InfoPanel.vue'
import DocsPanel from './components/DocsPanel.vue'
import MealPlanner from './pages/MealPlanner.vue' import MealPlanner from './pages/MealPlanner.vue'
import Converter from './pages/Converter.vue' import Converter from './pages/Converter.vue'
import ShoppingList from './pages/ShoppingList.vue' import ShoppingList from './pages/ShoppingList.vue'
const page = ref('meal') const page = ref('meal')
const showInfo = ref(false) const showInfo = ref(false)
const showDocs = ref(false)
</script> </script>
<style scoped> <style scoped>

View File

@@ -0,0 +1,505 @@
<template>
<Transition name="slide-right">
<div v-if="modelValue" class="docs-panel" role="dialog" aria-label="Guida utente">
<!-- Header sticky: back arrow + titolo centrato + spacer bilanciante -->
<div class="docs-header">
<button class="btn-back" @click="$emit('update:modelValue', false)" aria-label="Torna indietro">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
</button>
<span class="docs-title">Guida</span>
<!-- spacer identico al btn-back per centrare il titolo -->
<div aria-hidden="true" class="header-spacer" />
</div>
<!-- Pill nav scrollabile IntersectionObserver aggiorna la pill attiva -->
<div class="section-nav" role="tablist" aria-label="Sezioni">
<button
v-for="s in sections"
:key="s.id"
:class="['nav-pill', { active: activeSection === s.id }]"
role="tab"
:aria-selected="activeSection === s.id"
@click="scrollToSection(s.id)"
>{{ s.label }}</button>
</div>
<!-- Contenuto scorrevole -->
<div class="docs-scroll" ref="scrollEl">
<!-- Pasti -->
<section data-section="pasti">
<div class="section-head">
<span class="section-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2"/>
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</span>
<h2 class="section-heading">Pasti</h2>
</div>
<div class="doc-card">
<h3 class="card-title">Aggiungere un alimento</h3>
<ol class="steps">
<li>Tocca il giorno per espandere la card</li>
<li>Scegli il pasto: <strong>Colazione</strong>, <strong>Pranzo</strong> o <strong>Cena</strong></li>
<li>Scrivi il nome nel campo di testo</li>
<li>Premi <kbd>+</kbd> o Invio per aggiungerlo</li>
</ol>
</div>
<div class="doc-card tip">
<p class="tip-label" aria-hidden="true">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12.01" y2="8"/><line x1="12" y1="12" x2="12" y2="16"/>
</svg>
Suggerimento
</p>
<p>Premi <strong>Genera lista della spesa</strong> in fondo alla pagina per importare automaticamente tutti gli alimenti della settimana, senza duplicati.</p>
</div>
<div class="doc-card">
<h3 class="card-title">Rimuovere un alimento</h3>
<p>Tocca il pulsante <strong>×</strong> a destra dell'elemento.</p>
</div>
<div class="doc-card">
<h3 class="card-title">Salvataggio automatico</h3>
<p>I dati vengono salvati automaticamente sul dispositivo. Non serve premere nessun tasto.</p>
</div>
</section>
<!-- ── Converti ───────────────────────────── -->
<section data-section="converti">
<div class="section-head">
<span class="section-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/>
<path d="M3 11V9a4 4 0 014-4h14"/>
<polyline points="7 23 3 19 7 15"/>
<path d="M21 13v2a4 4 0 01-4 4H3"/>
</svg>
</span>
<h2 class="section-heading">Converti</h2>
</div>
<div class="doc-card">
<h3 class="card-title">Come usarlo</h3>
<ol class="steps">
<li>Cerca l'alimento nel campo (es. <em>pollo</em>, <em>riso</em>)</li>
<li>Seleziona il metodo di cottura dall'elenco</li>
<li>Inserisci il peso in grammi</li>
<li>Il risultato appare in tempo reale</li>
<li>Premi <strong>⇄</strong> per invertire crudo ↔ cotto</li>
</ol>
</div>
<div class="doc-card">
<h3 class="card-title">Alimenti disponibili</h3>
<div class="category-table" role="list">
<div class="cat-row" v-for="cat in categories" :key="cat.label" role="listitem">
<span class="cat-label">{{ cat.label }}</span>
<span class="cat-items">{{ cat.items }}</span>
</div>
</div>
<div class="methods-row" aria-label="Metodi di cottura disponibili">
<span class="method-chip" v-for="m in cookingMethods" :key="m">{{ m }}</span>
</div>
</div>
</section>
<!-- ── Spesa ──────────────────────────────── -->
<section data-section="spesa">
<div class="section-head">
<span class="section-icon" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 01-8 0"/>
</svg>
</span>
<h2 class="section-heading">Spesa</h2>
</div>
<div class="doc-card">
<h3 class="card-title">Aggiungere un elemento</h3>
<p>Scrivi il nome nel campo in alto e premi <kbd>+</kbd> o Invio.</p>
</div>
<div class="doc-card tip">
<p class="tip-label" aria-hidden="true">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12.01" y2="8"/><line x1="12" y1="12" x2="12" y2="16"/>
</svg>
Suggerimento
</p>
<p>Vai alla tab <strong>Pasti</strong> e premi <strong>Genera lista della spesa</strong> per importare automaticamente gli alimenti pianificati.</p>
</div>
<div class="doc-card">
<h3 class="card-title">Spuntare un elemento</h3>
<p>Tocca la casella a sinistra. Gli elementi completati vengono spostati in una sezione separata con testo barrato.</p>
</div>
<div class="doc-card">
<h3 class="card-title">Rimuovere / svuotare</h3>
<p>Tocca <strong>×</strong> per rimuovere un elemento singolo, oppure <strong>Svuota lista</strong> in fondo per eliminare tutto (richiede conferma).</p>
</div>
</section>
<div class="docs-footer">BitePlan · v{{ version }} · Davide Grilli</div>
</div>
</div>
</Transition>
</template>
<script setup>
import { ref, watch, nextTick, onUnmounted } from 'vue'
import pkg from '../../package.json'
const props = defineProps({ modelValue: Boolean })
defineEmits(['update:modelValue'])
const version = pkg.version
const scrollEl = ref(null)
const activeSection = ref('pasti')
let observer = null
const sections = [
{ id: 'pasti', label: 'Pasti' },
{ id: 'converti', label: 'Converti' },
{ id: 'spesa', label: 'Spesa' },
]
const categories = [
{ label: 'Cereali e pasta', items: 'Riso (4 varietà), pasta, farro, orzo, quinoa, cous cous' },
{ label: 'Legumi secchi', items: 'Ceci, fagioli, lenticchie' },
{ label: 'Verdure', items: 'Carote, zucchine, patate, spinaci, broccoli, asparagi e altri' },
{ label: 'Carni', items: 'Pollo petto, tacchino fesa, hamburger, vitello' },
{ label: 'Pesce', items: 'Tonno, merluzzo, spigola, sogliola' },
{ label: 'Uova', items: 'Uovo al tegamino, frittata' },
]
const cookingMethods = ['Bollitura', 'Padella', 'Forno', 'Friggitrice ad aria']
watch(() => props.modelValue, async (open) => {
if (open) {
await nextTick()
setupObserver()
activeSection.value = 'pasti'
scrollEl.value?.scrollTo({ top: 0 })
} else {
observer?.disconnect()
observer = null
}
})
function setupObserver() {
if (!scrollEl.value) return
observer?.disconnect()
// rootMargin: entra in zona attiva quando la sezione raggiunge il 20% dall'alto del container
observer = new IntersectionObserver(
(entries) => {
const visible = entries.filter(e => e.isIntersecting)
if (visible.length) activeSection.value = visible[0].target.dataset.section
},
{ root: scrollEl.value, rootMargin: '0px 0px -65% 0px', threshold: 0 }
)
scrollEl.value.querySelectorAll('[data-section]').forEach(el => observer.observe(el))
}
function scrollToSection(id) {
const el = scrollEl.value?.querySelector(`[data-section="${id}"]`)
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
onUnmounted(() => observer?.disconnect())
</script>
<style scoped>
/* ── Panel container ─────────────────────────── */
.docs-panel {
position: fixed;
inset: 0;
max-width: 480px;
margin: 0 auto;
background: var(--color-bg);
z-index: 300;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Animazione slide da destra ──────────────── */
/* enter ease-out: risposta immediata, decelera all'arrivo (feel nativo) */
.slide-right-enter-active { transition: transform 320ms cubic-bezier(0.25, 1, 0.5, 1); }
/* leave ease-in: parte lentamente, accelera — chiusura decisa */
.slide-right-leave-active { transition: transform 240ms cubic-bezier(0.55, 0, 1, 0.45); }
.slide-right-enter-from,
.slide-right-leave-to { transform: translateX(100%); }
/* ── Header ──────────────────────────────────── */
.docs-header {
display: flex;
align-items: center;
justify-content: space-between;
/* safe-area-inset-top: evita sovrapposizione con status bar su iOS */
padding: calc(env(safe-area-inset-top, 0px) + 10px) 8px 10px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.btn-back {
display: flex;
align-items: center;
justify-content: center;
min-height: 44px;
min-width: 44px;
background: none;
color: var(--color-primary);
border-radius: var(--radius-sm);
padding: 0;
transition: background var(--transition);
}
.btn-back:active {
background: var(--color-primary-muted);
opacity: 1;
}
.docs-title {
font-size: 1rem;
font-weight: 700;
color: var(--color-text);
letter-spacing: -0.01em;
}
/* spacer uguale al btn-back per centrare il titolo otticamente */
.header-spacer { width: 44px; }
/* ── Navigazione sezioni (pill) ──────────────── */
.section-nav {
display: flex;
gap: 6px;
padding: 10px 16px;
overflow-x: auto;
scrollbar-width: none;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.section-nav::-webkit-scrollbar { display: none; }
.nav-pill {
flex-shrink: 0;
font-size: 0.85rem;
font-weight: 600;
padding: 0 14px;
min-height: 32px;
border-radius: var(--radius-full);
background: var(--color-bg);
color: var(--color-muted);
border: 1px solid var(--color-border);
transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.nav-pill.active {
background: var(--color-primary);
color: #fff;
border-color: var(--color-primary);
}
/* ── Scroll container ────────────────────────── */
.docs-scroll {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 40px);
}
/* ── Section header ──────────────────────────── */
section { padding-top: 4px; }
section + section { margin-top: 8px; }
.section-head {
display: flex;
align-items: center;
gap: 10px;
padding: 24px 16px 10px;
}
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
/* quadratino con sfondo primario muted — identifica visivamente la sezione */
background: var(--color-primary-muted);
border-radius: var(--radius-sm);
color: var(--color-primary);
flex-shrink: 0;
}
.section-heading {
font-size: 1.1rem;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--color-text);
}
/* ── Card contenuto ──────────────────────────── */
.doc-card {
margin: 0 16px 8px;
background: var(--color-surface);
border-radius: var(--radius);
padding: 14px 16px;
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
font-size: 0.9rem;
line-height: 1.55;
color: var(--color-text);
}
.card-title {
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-muted);
margin-bottom: 8px;
}
/* ── Card tip / suggerimento ─────────────────── */
/* border-left accent: segnala contenuto accessorio senza competere con le card */
.doc-card.tip {
background: var(--color-primary-muted);
border-color: transparent;
border-left: 3px solid var(--color-primary);
box-shadow: none;
}
.tip-label {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--color-primary);
margin-bottom: 6px;
}
/* ── Lista numerata con badge ────────────────── */
/* counter CSS invece di <span> inline: mantiene semantica <ol> accessibile */
.steps {
list-style: none;
counter-reset: steps;
display: flex;
flex-direction: column;
gap: 9px;
margin-top: 8px;
}
.steps li {
counter-increment: steps;
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 0.9rem;
line-height: 1.5;
}
.steps li::before {
content: counter(steps);
display: flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
background: var(--color-primary);
color: #fff;
border-radius: 50%;
font-size: 0.7rem;
font-weight: 700;
flex-shrink: 0;
margin-top: 2px;
}
/* ── Kbd inline ──────────────────────────────── */
kbd {
display: inline-flex;
align-items: center;
padding: 1px 7px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 5px;
font-family: inherit;
font-size: 0.85em;
font-weight: 600;
color: var(--color-text);
}
/* ── Tabella categorie alimenti ──────────────── */
.category-table {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
overflow: hidden;
margin-top: 10px;
}
.cat-row {
display: flex;
align-items: baseline;
gap: 8px;
padding: 9px 12px;
border-bottom: 1px solid var(--color-border);
font-size: 0.86rem;
}
.cat-row:last-child { border-bottom: none; }
.cat-label {
font-weight: 600;
color: var(--color-text);
flex-shrink: 0;
min-width: 110px;
}
.cat-items { color: var(--color-muted); font-size: 0.82rem; }
/* ── Chip metodi cottura ─────────────────────── */
.methods-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
}
.method-chip {
font-size: 0.78rem;
font-weight: 600;
padding: 3px 10px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
color: var(--color-muted);
}
/* ── Footer ──────────────────────────────────── */
.docs-footer {
margin: 24px 16px 0;
text-align: center;
font-size: 0.78rem;
color: var(--color-muted);
opacity: 0.6;
}
</style>

View File

@@ -32,16 +32,15 @@
<span class="info-label">Licenza</span> <span class="info-label">Licenza</span>
<span class="info-value">EUPL v1.2</span> <span class="info-value">EUPL v1.2</span>
</div> </div>
<div class="info-row"> <button class="info-row info-row--btn" @click="$emit('open-docs')" aria-label="Apri guida">
<span class="info-label">Documentazione</span> <span class="info-label">Guida</span>
<a class="info-link" href="https://docs.biteplan.example.com" target="_blank" rel="noopener"> <span class="info-link">
Apri Apri
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/> <polyline points="9 18 15 12 9 6"/>
<polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>
</svg> </svg>
</a> </span>
</div> </button>
</div> </div>
</div> </div>
@@ -52,7 +51,7 @@
import pkg from '../../package.json' import pkg from '../../package.json'
import appIcon from '../../assets/icon-only.png' import appIcon from '../../assets/icon-only.png'
defineProps({ modelValue: Boolean }) defineProps({ modelValue: Boolean })
defineEmits(['update:modelValue']) defineEmits(['update:modelValue', 'open-docs'])
const version = pkg.version const version = pkg.version
</script> </script>
@@ -150,6 +149,16 @@ const version = pkg.version
.info-row:last-child { border-bottom: none; } .info-row:last-child { border-bottom: none; }
/* riga cliccabile: reset button, mantiene layout identico alle righe statiche */
.info-row--btn {
width: 100%;
min-height: unset;
border-radius: 0;
background: var(--color-surface);
text-align: left;
}
.info-row--btn:active { background: var(--color-bg); opacity: 1; }
.info-label { .info-label {
font-size: 0.9rem; font-size: 0.9rem;
color: var(--color-text); color: var(--color-text);

View File

@@ -27,7 +27,7 @@
class="result-item" class="result-item"
@click="selectItem(r)" @click="selectItem(r)"
> >
<span class="result-food">{{ r.food }}</span> <span class="result-food">{{ capFirst(r.food) }}</span>
<span class="result-method">{{ capFirst(r.method) }}</span> <span class="result-method">{{ capFirst(r.method) }}</span>
</li> </li>
</ul> </ul>
@@ -37,7 +37,7 @@
<div v-if="selected" class="converter-card"> <div v-if="selected" class="converter-card">
<div class="card-top"> <div class="card-top">
<div class="food-info"> <div class="food-info">
<span class="food-name">{{ selected.food }}</span> <span class="food-name">{{ capFirst(selected.food) }}</span>
<span class="food-sep">·</span> <span class="food-sep">·</span>
<span class="food-method">{{ capFirst(selected.method) }}</span> <span class="food-method">{{ capFirst(selected.method) }}</span>
</div> </div>
@@ -64,7 +64,7 @@
</div> </div>
</div> </div>
<button class="btn-swap" @click="swapDirection" :title="direction === 'rawToCooked' ? 'Inverti direzione' : 'Inverti direzione'"> <button class="btn-swap" @click="swapDirection" aria-label="Inverti direzione">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/> <polyline points="17 1 21 5 17 9"/>
<path d="M3 11V9a4 4 0 014-4h14"/> <path d="M3 11V9a4 4 0 014-4h14"/>
@@ -73,12 +73,12 @@
</svg> </svg>
</button> </button>
<div class="calc-side output-side" :class="{ 'has-result': result !== null }"> <div class="calc-side output-side">
<div class="calc-label">{{ direction === 'rawToCooked' ? 'cotto' : 'crudo' }}</div> <div class="calc-label">{{ direction === 'rawToCooked' ? 'cotto' : 'crudo' }}</div>
<div class="calc-output"> <div class="calc-output">
<span v-if="result !== null" class="output-value">{{ result }}</span> <span v-if="result !== null" class="output-value">{{ result }}</span>
<span v-else class="output-placeholder"></span> <span v-else class="output-placeholder"></span>
<span class="calc-unit" :class="{ visible: result !== null }">g</span> <span v-if="result !== null" class="calc-unit">g</span>
</div> </div>
</div> </div>
</div> </div>
@@ -206,7 +206,7 @@ function swapDirection() {
.result-item:last-child { border-bottom: none; } .result-item:last-child { border-bottom: none; }
.result-item:active { background: var(--color-bg); } .result-item:active { background: var(--color-bg); }
.result-food { font-weight: 600; text-transform: capitalize; } .result-food { font-weight: 600; }
.result-method { font-size: 0.85rem; color: var(--color-muted); } .result-method { font-size: 0.85rem; color: var(--color-muted); }
/* ── converter card ───────────────────────────────── */ /* ── converter card ───────────────────────────────── */
@@ -231,12 +231,11 @@ function swapDirection() {
gap: 6px; gap: 6px;
} }
.food-name { font-weight: 700; font-size: 1rem; text-transform: capitalize; } .food-name { font-weight: 700; font-size: 1rem; }
.food-sep { color: var(--color-border); font-size: 1.1rem; } .food-sep { color: var(--color-border); font-size: 1.1rem; }
.food-method { font-size: 0.9rem; color: var(--color-muted); } .food-method { font-size: 0.9rem; color: var(--color-muted); }
.btn-reset { .btn-reset {
background: none;
color: var(--color-primary); color: var(--color-primary);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
@@ -284,7 +283,7 @@ function swapDirection() {
/* override globale per input dentro la card */ /* override globale per input dentro la card */
.calc-input { .calc-input {
font-size: 1.5rem; font-size: 1.6rem;
font-weight: 700; font-weight: 700;
letter-spacing: -0.02em; letter-spacing: -0.02em;
border: none; border: none;
@@ -308,6 +307,8 @@ function swapDirection() {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--color-muted); color: var(--color-muted);
align-self: flex-end;
padding-bottom: 6px;
} }
/* bottone swap centrale */ /* bottone swap centrale */
@@ -332,33 +333,31 @@ function swapDirection() {
opacity: 1; opacity: 1;
} }
/* colonna output */ /* colonna output: underline visivo per simmetria con il lato input */
.output-side .calc-output { .calc-output {
display: flex; display: flex;
align-items: baseline; align-items: center;
gap: 4px; gap: 4px;
min-height: 44px; min-height: 44px;
align-items: center; border-bottom: 2px solid var(--color-border);
padding-bottom: 4px;
} }
.output-value { .output-value {
font-size: 2rem; font-size: 1.6rem;
font-weight: 800; font-weight: 800;
letter-spacing: -0.03em; letter-spacing: -0.03em;
color: var(--color-primary); color: var(--color-primary);
line-height: 1;
} }
.output-placeholder { .output-placeholder {
font-size: 2rem; font-size: 1.6rem;
font-weight: 300; font-weight: 300;
color: var(--color-border); color: var(--color-border);
line-height: 1;
} }
/* nasconde "g" finché non c'è un risultato */
.calc-unit { visibility: hidden; }
.calc-input-wrap .calc-unit,
.has-result .calc-unit { visibility: visible; }
/* ── footer card ─────────────────────────────────── */ /* ── footer card ─────────────────────────────────── */
.card-footer { .card-footer {
padding: 10px 16px 14px; padding: 10px 16px 14px;