# Menù in basso — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** mettere in QuantoBasta il menù in basso di sistema (Ricette, Categorie, Impostazioni; su iPhone anche Cerca), come deciso in `docs/superpowers/specs/2026-09-25-menu-in-basso-design.md`.

**Architecture:** la pagina Principale della pila principale diventa un `createNativeBottomTabNavigator` (tab native di react-native-screens: su iOS la tab bar Liquid Glass, su Android la barra Material). Il contenuto di oggi di Principale va in `SezioneRicette`; GestioneCategorie e Impostazioni escono dalla pila principale e diventano sezioni. Le differenze fra iOS e Android le decide un modulo puro, `logica-menu.ts`; i Recenti della ricerca un altro, `logica-ricerca.ts`.

**Tech Stack:** Expo SDK 57, React Native 0.86, TypeScript, React Navigation 7 (`@react-navigation/bottom-tabs/unstable` 7.19), react-native-screens 4.26, `node --test`.

## Global Constraints

- Repo di lavoro: `/Users/paolo/dev/quantobasta`, ramo `menu-in-basso` (da creare da `main`). Mai lavorare su `main`.
- Commenti, nomi e messaggi di commit in **italiano**, nello stile dei file vicini: il commento dice il perché.
- **Nessuna decisione nei `.tsx`**: `node --test` non carica i componenti, quindi ogni scelta (per sistema, per stato) sta in `src/ui/logica-*.ts` con i suoi test. Nei `.tsx` niente `Platform.OS === ...` scritto a mano: si chiama una funzione di `logica-menu.ts` passandole `Platform.OS`.
- Prima di ogni commit: `npm test` e `npx tsc --noEmit` puliti.
- Ogni commit finisce con una riga vuota e poi:
  ```
  Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
  Claude-Session: https://claude.ai/code/session_01RWR5c8CCX2MqNN8fuq9sYH
  ```
- iPhone: voci SF Symbols `book`, `folder`, `gearshape`; Cerca con `tabBarSystemItem: 'search'` (etichetta di sistema, nessuna `tabBarLabel`); `tabBarMinimizeBehavior: 'onScrollDown'`.
- Android: solo tre voci, icone PNG in `assets/menu/` (`ricette`, `categorie`, `impostazioni`), `tabBarLabelVisibilityMode: 'labeled'`.
- Colore della voce scelta: `colori.accento`.
- Recenti: al massimo **10**, chiave `ricerca.recenti` nella tabella `impostazioni`, JSON di id.
- Niente adb su dispositivi che non sono l'emulatore `emulator-5580` (guardare `adb devices -l`: `emulator-5554` è BlueStacks di un'altra sessione).

---

### Task 1: Le decisioni per sistema, la dipendenza e le icone Android

**Files:**
- Create: `src/ui/logica-menu.ts`
- Create: `src/ui/logica-menu.test.ts`
- Create: `assets/menu/ricette.png`, `ricette@2x.png`, `ricette@3x.png`, e lo stesso per `categorie`, `impostazioni`
- Modify: `package.json`, `package-lock.json` (con `npx expo install`)

**Interfaces:**
- Produces:
  ```ts
  export type Sistema = 'ios' | 'android' | string; // Platform.OS
  export function conSezioneCerca(os: Sistema): boolean;      // true solo per 'ios'
  export function ricercaNellaPagina(os: Sistema): boolean;   // true salvo 'ios'
  export function piuNellaBarra(os: Sistema): boolean;        // true solo per 'ios'
  export function contenutoSopraIlMenu(os: Sistema): boolean; // true salvo 'ios'
  export function margineSotto(os: Sistema, insetSistema: number): number; // 0 dove contenutoSopraIlMenu, altrimenti insetSistema
  ```

- [ ] **Step 1: Ramo e dipendenza**

```bash
cd /Users/paolo/dev/quantobasta && git checkout main && git pull && git checkout -b menu-in-basso
npx expo install @react-navigation/bottom-tabs
```
Expected: `package.json` guadagna `@react-navigation/bottom-tabs` (^7.19) e `@react-navigation/native` sale a ^7.4.

- [ ] **Step 2: Il test che fallisce** — `src/ui/logica-menu.test.ts`:

```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';

import {
  conSezioneCerca, contenutoSopraIlMenu, margineSotto, piuNellaBarra, ricercaNellaPagina,
} from './logica-menu.ts';

test('su iPhone la ricerca è una sezione del menù, su Android sta in cima alla pagina', () => {
  assert.equal(conSezioneCerca('ios'), true);
  assert.equal(ricercaNellaPagina('ios'), false);
  assert.equal(conSezioneCerca('android'), false);
  assert.equal(ricercaNellaPagina('android'), true);
});

test('il + sta nella barra su iPhone, tondo in basso su Android', () => {
  assert.equal(piuNellaBarra('ios'), true);
  assert.equal(piuNellaBarra('android'), false);
});

test('su Android il contenuto finisce sopra la barra, su iPhone ci scorre sotto', () => {
  assert.equal(contenutoSopraIlMenu('android'), true);
  assert.equal(contenutoSopraIlMenu('ios'), false);
  assert.equal(margineSotto('android', 24), 0);
  assert.equal(margineSotto('ios', 34), 34);
});

test('un sistema che non è iOS si comporta come Android', () => {
  assert.equal(conSezioneCerca('web'), false);
  assert.equal(piuNellaBarra('web'), false);
});
```

- [ ] **Step 3: Vederlo fallire**

Run: `node --test src/ui/logica-menu.test.ts`
Expected: FAIL, il modulo non esiste.

- [ ] **Step 4: Il modulo** — `src/ui/logica-menu.ts`:

```ts
/**
 * Dove iPhone e Android fanno il menù a modo loro. Qui e non nei .tsx perché
 * `node --test` non carica i componenti: una scelta scritta dentro una
 * schermata si romperebbe senza che nessun test se ne accorga.
 *
 * iPhone (iOS 26 e 27): la tab bar di vetro, con la lente come voce del menù,
 * e il + in alto a destra come nelle app di Apple (Note, Promemoria). Il
 * contenuto scorre sotto il vetro.
 *
 * Android (Material 3): la barra in basso piatta e a tutta larghezza, la
 * ricerca a pastiglia in cima alla pagina e il + tondo in basso, che sono le
 * sue regole. La barra è opaca e copre il fondo della pagina: il contenuto si
 * ferma sopra di lei. Tutto quello che non è iOS si comporta come Android.
 */
export type Sistema = string;

const ios = (os: Sistema): boolean => os === 'ios';

/** La quarta voce del menù, la lente: solo su iPhone. */
export function conSezioneCerca(os: Sistema): boolean {
  return ios(os);
}

/** Il campo di ricerca in cima a Ricette e dentro una categoria: dove non c'è la lente. */
export function ricercaNellaPagina(os: Sistema): boolean {
  return !ios(os);
}

/** Il + in alto a destra nella barra (iPhone) invece che tondo in basso (Android). */
export function piuNellaBarra(os: Sistema): boolean {
  return ios(os);
}

/** Se le pagine del menù devono finire sopra la barra invece di scorrerle sotto. */
export function contenutoSopraIlMenu(os: Sistema): boolean {
  return !ios(os);
}

/**
 * Il margine in fondo alle liste di una sezione. Dove il contenuto finisce
 * sopra la barra, la barra ha già preso anche lo spazio dei tasti di sistema:
 * aggiungerlo di nuovo lascerebbe un vuoto.
 */
export function margineSotto(os: Sistema, insetSistema: number): number {
  return contenutoSopraIlMenu(os) ? 0 : insetSistema;
}
```

- [ ] **Step 5: Vederlo passare**

Run: `node --test src/ui/logica-menu.test.ts`
Expected: PASS, 4 test.

- [ ] **Step 6: Le icone Android** (la libreria su Android vuole immagini: le disegniamo dai glifi Material che `@expo/vector-icons` ha già):

```bash
cd /Users/paolo/dev/quantobasta && mkdir -p assets/menu && python3 - <<'EOF'
import json
from PIL import Image, ImageDraw, ImageFont
base = 'node_modules/@expo/vector-icons/build/vendor/react-native-vector-icons/'
font_path = base + 'Fonts/MaterialIcons.ttf'
glifi = json.load(open(base + 'glyphmaps/MaterialIcons.json'))
for nome, glifo in [('ricette', 'menu-book'), ('categorie', 'folder'), ('impostazioni', 'settings')]:
    for scala, px in [(1, 24), (2, 48), (3, 72)]:
        im = Image.new('RGBA', (px, px), (0, 0, 0, 0))
        ImageDraw.Draw(im).text((0, 0), chr(glifi[glifo]), font=ImageFont.truetype(font_path, px), fill=(0, 0, 0, 255))
        im.save(f'assets/menu/{nome}{"" if scala == 1 else f"@{scala}x"}.png')
EOF
ls assets/menu
```
Expected: 9 file PNG.

- [ ] **Step 7: Controlli e commit**

```bash
npm test && npx tsc --noEmit
git add src/ui/logica-menu.ts src/ui/logica-menu.test.ts assets/menu package.json package-lock.json
git commit -m "Le scelte del menù per sistema in logica-menu, la libreria delle tab native e le icone Android"
```
(con le due righe finali delle Global Constraints)

---

### Task 2: I Recenti della ricerca

**Files:**
- Create: `src/ui/logica-ricerca.ts`
- Create: `src/ui/logica-ricerca.test.ts`

**Interfaces:**
- Produces:
  ```ts
  export const RECENTI_MASSIMO = 10;
  export const CHIAVE_RECENTI = 'ricerca.recenti';
  export function conRecente(ids: string[], id: string): string[];
  export function leggiRecenti(salvato: string | null): string[];
  export function recentiVisibili<R extends { id: string }>(ids: string[], ricette: R[]): R[];
  ```

- [ ] **Step 1: Il test che fallisce** — `src/ui/logica-ricerca.test.ts`:

```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';

import {
  CHIAVE_RECENTI, RECENTI_MASSIMO, conRecente, leggiRecenti, recentiVisibili,
} from './logica-ricerca.ts';

test('una ricetta aperta dalla ricerca va in testa ai Recenti', () => {
  assert.deepEqual(conRecente([], 'a'), ['a']);
  assert.deepEqual(conRecente(['b', 'c'], 'a'), ['a', 'b', 'c']);
});

test('riaperta, torna in testa senza doppioni', () => {
  assert.deepEqual(conRecente(['b', 'a', 'c'], 'a'), ['a', 'b', 'c']);
});

test('i Recenti sono al massimo dieci: esce la più vecchia', () => {
  const pieni = Array.from({ length: RECENTI_MASSIMO }, (_, i) => `r${i}`);
  const dopo = conRecente(pieni, 'nuova');
  assert.equal(dopo.length, RECENTI_MASSIMO);
  assert.equal(dopo[0], 'nuova');
  assert.equal(dopo.includes(`r${RECENTI_MASSIMO - 1}`), false);
});

test('il testo salvato si legge, e se è rotto si riparte da zero', () => {
  assert.deepEqual(leggiRecenti(null), []);
  assert.deepEqual(leggiRecenti(''), []);
  assert.deepEqual(leggiRecenti('non è json'), []);
  assert.deepEqual(leggiRecenti('{"a":1}'), []);
  assert.deepEqual(leggiRecenti('[1,2]'), []);
  assert.deepEqual(leggiRecenti(JSON.stringify(['a', 'b'])), ['a', 'b']);
});

test('le ricette cancellate spariscono dai Recenti, l\'ordine resta', () => {
  const ricette = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
  assert.deepEqual(recentiVisibili(['c', 'x', 'a'], ricette), [{ id: 'c' }, { id: 'a' }]);
});

test('la chiave è quella della tabella impostazioni', () => {
  assert.equal(CHIAVE_RECENTI, 'ricerca.recenti');
});
```

- [ ] **Step 2: Vederlo fallire**

Run: `node --test src/ui/logica-ricerca.test.ts`
Expected: FAIL, il modulo non esiste.

- [ ] **Step 3: Il modulo** — `src/ui/logica-ricerca.ts`:

```ts
/**
 * I Recenti della ricerca, come in Foto: le ultime ricette aperte dalla
 * sezione Cerca, la più recente per prima. Si ricordano nella tabella
 * `impostazioni`, come l'ultima ricetta aperta, in un testo JSON: una lista
 * di id.
 *
 * Solo le ricette aperte dalla ricerca, non tutte quelle aperte: le altre
 * l'utente le ritrova dove le ha prese, e i Recenti diventerebbero un doppione
 * dell'elenco.
 */
export const RECENTI_MASSIMO = 10;
export const CHIAVE_RECENTI = 'ricerca.recenti';

/** L'id in testa, senza doppioni, e la lista tagliata a RECENTI_MASSIMO. */
export function conRecente(ids: string[], id: string): string[] {
  return [id, ...ids.filter((altro) => altro !== id)].slice(0, RECENTI_MASSIMO);
}

/**
 * Dal testo salvato alla lista. Un testo che manca, o che non è una lista di
 * stringhe (scritto a metà, o da una versione diversa), vale «nessun
 * recente»: per i Recenti non si ferma la ricerca.
 */
export function leggiRecenti(salvato: string | null): string[] {
  if (salvato === null || salvato === '') return [];
  try {
    const valore: unknown = JSON.parse(salvato);
    return Array.isArray(valore) && valore.every((v) => typeof v === 'string') ? valore : [];
  } catch {
    return [];
  }
}

/** Le ricette nell'ordine dei Recenti; quelle che non ci sono più (cancellate) si saltano. */
export function recentiVisibili<R extends { id: string }>(ids: string[], ricette: R[]): R[] {
  const perId = new Map(ricette.map((r) => [r.id, r]));
  return ids.flatMap((id) => {
    const r = perId.get(id);
    return r === undefined ? [] : [r];
  });
}
```

- [ ] **Step 4: Vederlo passare**

Run: `node --test src/ui/logica-ricerca.test.ts`
Expected: PASS, 6 test.

- [ ] **Step 5: Controlli e commit**

```bash
npm test && npx tsc --noEmit
git add src/ui/logica-ricerca.ts src/ui/logica-ricerca.test.ts
git commit -m "I Recenti della ricerca: in testa, senza doppioni, al massimo dieci"
```

---

### Task 3: Il menù

La struttura: Principale diventa il menù, il suo contenuto di oggi diventa `SezioneRicette`, Categorie e Impostazioni diventano sezioni. La sezione Cerca in questo task è una pagina provvisoria col solo titolo: la riempie il Task 5.

**Files:**
- Create: `src/ui/schermate/SezioneRicette.tsx` (il contenuto di oggi di `Principale.tsx`)
- Create: `src/ui/componenti/SopraIlMenu.tsx`
- Create: `src/ui/schermate/Cerca.tsx` (provvisoria)
- Modify: `src/ui/schermate/Principale.tsx` (diventa il menù)
- Modify: `src/ui/navigazione.ts`
- Modify: `src/ui/App.tsx` (pila principale, `apriRicetta`, piega del Duo)
- Modify: `src/ui/schermate/GestioneCategorie.tsx`, `src/ui/schermate/Impostazioni.tsx` (tipi, niente freccia, `SopraIlMenu`)
- Modify: `src/i18n/index.ts`, `src/i18n/it.ts`, `src/i18n/en.ts`

**Interfaces:**
- Consumes: `contenutoSopraIlMenu`, `conSezioneCerca` (Task 1).
- Produces:
  ```ts
  // navigazione.ts
  export type ParametriMenu = {
    Ricette: undefined;
    Categorie: undefined;
    Impostazioni: undefined;
    Cerca: undefined;
  };
  export type ParametriNav = {
    Principale: NavigatorScreenParams<ParametriMenu> | undefined;
    Dosatore: { ricettaId: string };
    Modifica: { ricettaId: string | null };
    Anteprima: { uri: string };
  };
  export type PropsSezione<N extends keyof ParametriMenu> = CompositeScreenProps<
    NativeBottomTabScreenProps<ParametriMenu, N>,
    NativeStackScreenProps<ParametriNav>
  >;
  // PropsSfoglia resta, ma il genitore ora è la sezione:
  export type PropsSfoglia<N extends keyof ParametriSfoglia> = CompositeScreenProps<
    NativeStackScreenProps<ParametriSfoglia, N>,
    PropsSezione<'Ricette'>
  >;
  // componenti/SopraIlMenu.tsx
  export default function SopraIlMenu(props: { style?: StyleProp<ViewStyle>; children: ReactNode }): JSX.Element;
  ```
  Chiavi i18n nuove: `'menu.ricette'`, `'menu.categorie'`, `'menu.impostazioni'`, `'cerca.titolo'`, `'cerca.segnaposto'`, `'cerca.recenti'`.

- [ ] **Step 1: Le traduzioni** — in `src/i18n/index.ts` aggiungere all'unione `Chiave`:

```ts
  | 'menu.ricette' | 'menu.categorie' | 'menu.impostazioni'
  | 'cerca.titolo' | 'cerca.segnaposto' | 'cerca.recenti'
```
In `src/i18n/it.ts`:
```ts
  'menu.ricette': 'Ricette',
  'menu.categorie': 'Categorie',
  'menu.impostazioni': 'Impostazioni',
  'cerca.titolo': 'Cerca',
  'cerca.segnaposto': 'Cerca fra le ricette',
  'cerca.recenti': 'Recenti',
```
In `src/i18n/en.ts`:
```ts
  'menu.ricette': 'Recipes',
  'menu.categorie': 'Categories',
  'menu.impostazioni': 'Settings',
  'cerca.titolo': 'Search',
  'cerca.segnaposto': 'Search recipes',
  'cerca.recenti': 'Recent',
```

- [ ] **Step 2: I tipi** — `src/ui/navigazione.ts`: togliere `GestioneCategorie` e `Impostazioni` da `ParametriNav`, cambiare `Principale` e aggiungere `ParametriMenu`, `PropsSezione`, il nuovo `PropsSfoglia` come nel blocco Interfaces. Import da aggiungere:

```ts
import type { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';
import type { NativeBottomTabScreenProps } from '@react-navigation/bottom-tabs/unstable';
```
Aggiornare il commento d'intestazione: la pagina di partenza è il menù (Ricette, Categorie, Impostazioni, e su iPhone Cerca); la pila dello sfoglia vive nella sezione Ricette.

- [ ] **Step 3: `SopraIlMenu`** — `src/ui/componenti/SopraIlMenu.tsx`:

```tsx
/**
 * Il contenitore delle pagine del menù. Su Android la barra in basso è opaca
 * e sta sopra l'app: senza questo il fondo delle liste e il + tondo finivano
 * sotto di lei (visto nella prova sull'emulatore). L'area sicura di
 * react-native-screens sa quanto è alta la barra, tasti di sistema compresi,
 * e ferma il contenuto lì. Su iPhone invece il contenuto scorre sotto il vetro
 * della tab bar, ed è il vetro a farsi vedere: lì è una View e basta.
 *
 * Quale dei due lo decide contenutoSopraIlMenu, in logica-menu.ts.
 */
import type { ReactNode } from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import type { StyleProp, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-screens/experimental';

import { contenutoSopraIlMenu } from '../logica-menu.ts';

export default function SopraIlMenu({ style, children }: { style?: StyleProp<ViewStyle>; children: ReactNode }) {
  if (!contenutoSopraIlMenu(Platform.OS)) return <View style={[stili.pieno, style]}>{children}</View>;
  return (
    <SafeAreaView edges={{ bottom: true }} insetType="all" style={[stili.pieno, style]}>
      {children}
    </SafeAreaView>
  );
}

const stili = StyleSheet.create({ pieno: { flex: 1 } });
```

- [ ] **Step 4: `SezioneRicette`** — creare `src/ui/schermate/SezioneRicette.tsx` spostando dentro TUTTO il contenuto di oggi di `Principale.tsx` (commento d'intestazione, import, `Sfoglia`, componente, stili) con due cambi:
  1. il componente si chiama `SezioneRicette` (`export default function SezioneRicette()`);
  2. il `<View style={stili.riga}>` esterno diventa `<SopraIlMenu style={stili.riga}>` (import di `../componenti/SopraIlMenu.tsx`), e `stili.riga` perde `flex: 1` (lo mette SopraIlMenu): `riga: { flexDirection: 'row' }`.
  Il commento d'intestazione dice: la sezione Ricette del menù; contiene la pila dello sfoglia e, sugli schermi larghi, il pannello della ricetta.

- [ ] **Step 5: `Cerca` provvisoria** — `src/ui/schermate/Cerca.tsx`:

```tsx
/** La sezione Cerca del menù (solo iPhone). Provvisoria: la riempie il passo della ricerca. */
import { View } from 'react-native';

import { useColori } from '../tema.ts';

export default function Cerca() {
  const colori = useColori();
  return <View style={{ flex: 1, backgroundColor: colori.fondo }} />;
}
```

- [ ] **Step 6: Il menù** — `src/ui/schermate/Principale.tsx`, contenuto intero:

```tsx
/**
 * La pagina di partenza: il menù in basso. Su iPhone è la tab bar di sistema
 * (la capsula di vetro di iOS 26), con Ricette, Categorie, Impostazioni e la
 * lente; su Android la barra in basso di Material, senza la lente, perché lì
 * la ricerca sta in cima alla pagina. Le differenze le decide logica-menu.ts.
 *
 * Le icone: su iPhone i simboli di Apple (SF Symbols), su Android immagini,
 * perché la libreria lì non ha i simboli Material; le immagini vengono dai
 * glifi Material di @expo/vector-icons (assets/menu/).
 *
 * La ricetta, la modifica e l'anteprima di un import sono pagine della pila
 * principale, sopra questa: aprendole il menù sparisce, come in WhatsApp
 * dentro una chat.
 *
 * `@react-navigation/bottom-tabs/unstable`: la parte nativa è segnata così da
 * React Navigation. Con iOS 27 la lente resta dentro la capsula invece che nel
 * suo cerchio: quando react-native-screens la staccherà, si aggiorna la
 * libreria e basta (vedi il progetto, 2026-09-25).
 */
import { Platform } from 'react-native';
import { createNativeBottomTabNavigator } from '@react-navigation/bottom-tabs/unstable';
import type { NativeBottomTabIcon } from '@react-navigation/bottom-tabs/unstable';
import type { SFSymbol } from 'sf-symbols-typescript';

import type { ParametriMenu } from '../navigazione.ts';
import { useApp } from '../contesto.ts';
import { conSezioneCerca } from '../logica-menu.ts';
import { useColori } from '../tema.ts';
import Cerca from './Cerca.tsx';
import GestioneCategorie from './GestioneCategorie.tsx';
import Impostazioni from './Impostazioni.tsx';
import SezioneRicette from './SezioneRicette.tsx';

const Menu = createNativeBottomTabNavigator<ParametriMenu>();

const ANDROID = {
  ricette: require('../../../assets/menu/ricette.png'),
  categorie: require('../../../assets/menu/categorie.png'),
  impostazioni: require('../../../assets/menu/impostazioni.png'),
};

/** Il simbolo di Apple su iPhone, l'immagine su Android. */
function icona(simbolo: SFSymbol, immagine: number): NativeBottomTabIcon {
  return Platform.OS === 'ios' ? { type: 'sfSymbol', name: simbolo } : { type: 'image', source: immagine };
}

export default function Principale() {
  const { testo } = useApp();
  const colori = useColori();
  return (
    <Menu.Navigator
      screenOptions={{
        headerShown: false,
        tabBarActiveTintColor: colori.accento,
        tabBarMinimizeBehavior: 'onScrollDown',
        tabBarLabelVisibilityMode: 'labeled',
      }}
    >
      <Menu.Screen
        name="Ricette"
        component={SezioneRicette}
        options={{ title: testo('menu.ricette'), tabBarIcon: icona('book', ANDROID.ricette) }}
      />
      <Menu.Screen
        name="Categorie"
        component={GestioneCategorie}
        options={{ title: testo('menu.categorie'), tabBarIcon: icona('folder', ANDROID.categorie) }}
      />
      <Menu.Screen
        name="Impostazioni"
        component={Impostazioni}
        options={{ title: testo('menu.impostazioni'), tabBarIcon: icona('gearshape', ANDROID.impostazioni) }}
      />
      {conSezioneCerca(Platform.OS) && (
        <Menu.Screen name="Cerca" component={Cerca} options={{ tabBarSystemItem: 'search' }} />
      )}
    </Menu.Navigator>
  );
}
```
Nota: la scelta fra simbolo e immagine è la sola cosa per sistema scritta qui, ed è una traduzione di formato richiesta dall'API (non una decisione di prodotto). Se `sf-symbols-typescript` non è risolvibile, usare `type SFSymbol = Extract<NativeBottomTabIcon, { type: 'sfSymbol' }>['name']`.

- [ ] **Step 7: La pila principale** — in `src/ui/App.tsx`:
  - togliere gli import e le `<Stack.Screen>` di `GestioneCategorie` e `Impostazioni`;
  - aggiornare il commento d'intestazione (la rotta iniziale è Principale, cioè il menù).

- [ ] **Step 8: Schermi larghi, `apriRicetta` porta su Ricette** — in `App.tsx`, dentro `apriRicetta`, prima del `if (due) return;`:

```ts
        scegli(id);
        if (due) {
          // Sugli schermi larghi la ricetta va nel pannello di destra, che sta
          // nella sezione Ricette: se si arriva da un'altra (la ricerca), il
          // menù ci passa da solo.
          if (navigazione.isReady()) navigazione.navigate('Principale', { screen: 'Ricette' });
          return;
        }
```

- [ ] **Step 9: La piega del Duo porta su Ricette** — nell'effetto del cambio di larghezza di `App.tsx`, subito dopo il `navigazione.dispatch(CommonActions.reset(...))`:

```ts
    // Aprendo il Duo con una ricetta a tutto schermo la ricetta passa nel
    // pannello, che sta nella sezione Ricette: il menù ci va con lei.
    if (due && dopo.selezionata !== null) navigazione.navigate('Principale', { screen: 'Ricette' });
```

- [ ] **Step 10: Categorie e Impostazioni come sezioni**
  - `GestioneCategorie.tsx`: firma `export default function GestioneCategorie({ navigation }: PropsSezione<'Categorie'>)`, import di `PropsSezione` al posto di `PropsSchermata`; nella `<Barra>` togliere `onIndietro` ed `etichettaIndietro`; la `View` radice (quella con il `PanResponder`) va avvolta in `<SopraIlMenu>`; sostituire `const sotto = useSafeAreaInsets().bottom;` con `const sotto = margineSotto(Platform.OS, useSafeAreaInsets().bottom);` (import di `margineSotto` e `Platform`). Ogni `navigation.navigate(...)` verso rotte della pila principale continua a funzionare (React Navigation risale al genitore).
  - `Impostazioni.tsx`: firma `export default function Impostazioni({ navigation }: PropsSezione<'Impostazioni'>)`; nella `<Barra>` togliere `onIndietro` ed `etichettaIndietro`; la `View style={stili.pagina}` diventa `<SopraIlMenu style={stili.pagina}>`; `sotto` come sopra con `margineSotto`. `navigation.navigate('Anteprima', ...)` resta.
  - Aggiornare i commenti d'intestazione: sono sezioni del menù, non pagine aperte da qualche parte, quindi niente freccia.

- [ ] **Step 11: Categorie ed Elenco senza cartella e ingranaggio** — in `Categorie.tsx`, nella `<Barra>` togliere i due `Pressable` con `navigation.navigate('GestioneCategorie')` e `navigation.navigate('Impostazioni')` (restano nel frammento solo il cambio vista); aggiornare il commento sopra la Barra (i comandi rari sono passati al menù). I tipi di Categorie ed Elenco non cambiano nome (`PropsSfoglia`).

- [ ] **Step 12: Controlli**

Run: `npm test && npx tsc --noEmit`
Expected: puliti. Se `tsc` segnala altri `navigate('GestioneCategorie')` o `navigate('Impostazioni')` nel codice, sostituirli con `navigation.navigate('Principale', { screen: 'Categorie' })` / `{ screen: 'Impostazioni' }`.

- [ ] **Step 13: Commit**

```bash
git add -A src
git commit -m "Il menù in basso: Ricette, Categorie, Impostazioni e su iPhone la lente; la ricetta si apre sopra e lo copre"
```

---

### Task 4: Ricette e Categorie, il + e la ricerca per sistema

**Files:**
- Modify: `src/ui/schermate/Categorie.tsx`
- Modify: `src/ui/schermate/Elenco.tsx`
- Modify: `src/ui/schermate/GestioneCategorie.tsx`

**Interfaces:**
- Consumes: `piuNellaBarra`, `ricercaNellaPagina`, `margineSotto` (Task 1); `SopraIlMenu` già attorno alle sezioni (Task 3).

- [ ] **Step 1: Categorie** — in `Categorie.tsx`:
  - `const sotto = margineSotto(Platform.OS, useSafeAreaInsets().bottom);`
  - `const piuInAlto = piuNellaBarra(Platform.OS);` e `const cercaQui = ricercaNellaPagina(Platform.OS);`
  - nella `<Barra>`, dentro `azioni`, dopo il cambio vista, solo se `piuInAlto`:
    ```tsx
    {piuInAlto && (
      <Pressable
        onPress={scrivine}
        hitSlop={12}
        accessibilityRole="button"
        accessibilityLabel={testo('elenco.nuova')}
      >
        <Segno nome="piu" dimensione={26} colore={colori.accento} />
      </Pressable>
    )}
    ```
  - il blocco della ricerca in cima: la condizione `stato !== 'vuoto' && (` diventa `cercaQui && stato !== 'vuoto' && (`. Su iPhone `ricerca` resta `''`, quindi il ramo `stato === 'ricerca'` non si attiva mai: non va toccato.
  - il `<Pressable style={[stili.tondo, ...]}>` del + in fondo va reso solo se `!piuInAlto`.
  - commenti: dove la ricerca e il + stanno, e perché (Apple e Material), rimandando a logica-menu.ts.

- [ ] **Step 2: Elenco** — in `Elenco.tsx`, gli stessi quattro cambi: `sotto` con `margineSotto`; il + in `azioni` dopo il cambio vista se `piuNellaBarra`; il campo di ricerca con la condizione `cercaQui && !vuoto && (`; il tondo del + solo se `!piuInAlto`. Aggiornare il commento d'intestazione sulla ricerca (su iPhone si cerca dalla lente del menù).

- [ ] **Step 3: GestioneCategorie** — il suo tondo «Nuova categoria» segue la stessa regola: su iPhone un `Pressable` con `<Segno nome="piu" .../>` nelle `azioni` della `<Barra>` (etichetta accessibile: la stessa chiave che usa oggi il tondo), su Android il tondo resta. Condizione con `piuNellaBarra(Platform.OS)`.

- [ ] **Step 4: Controlli e commit**

```bash
npm test && npx tsc --noEmit
git add -A src
git commit -m "Su iPhone il + sale nella barra e la ricerca passa alla lente; su Android restano il tondo e la ricerca in cima"
```

---

### Task 5: La sezione Cerca (iPhone)

**Files:**
- Modify: `src/ui/schermate/Cerca.tsx` (sostituisce la provvisoria)
- Modify: `src/ui/schermate/Principale.tsx` (la sezione Cerca diventa una pila con l'intestazione di sistema)

**Interfaces:**
- Consumes: `CHIAVE_RECENTI`, `conRecente`, `leggiRecenti`, `recentiVisibili` (Task 2); `filtraRicette`, `datiRicetta` (logica-elenco.ts); `TesseraRicetta`; `percorsoFoto`; `elencoRicette`, `elencoCategorie`; `leggiImpostazione`, `salvaImpostazione`; `usePannelli().apriRicetta`.

- [ ] **Step 1: La pila di Cerca** — in `Principale.tsx`: la sezione Cerca non monta più `Cerca` direttamente ma una pila con l'intestazione di sistema (serve al campo di ricerca nativo):

```tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const PilaCerca = createNativeStackNavigator<{ Ricerca: undefined }>();

function SezioneCerca() {
  const { testo } = useApp();
  const colori = useColori();
  return (
    <PilaCerca.Navigator>
      <PilaCerca.Screen
        name="Ricerca"
        component={Cerca}
        options={{
          title: testo('cerca.titolo'),
          headerLargeTitle: true,
          headerTransparent: true,
          headerTintColor: colori.testo,
          contentStyle: { backgroundColor: colori.fondo },
        }}
      />
    </PilaCerca.Navigator>
  );
}
```
e `<Menu.Screen name="Cerca" component={SezioneCerca} .../>`. Commento: l'intestazione di sistema qui sì (altrove la barra è nostra, vedi Barra.tsx) perché il campo di ricerca di iOS vive nell'intestazione.

- [ ] **Step 2: `Cerca.tsx`** — contenuto intero:

```tsx
/**
 * La sezione Cerca del menù, solo su iPhone (su Android la ricerca sta in
 * cima alla pagina: logica-menu.ts). Fatta come la ricerca di Foto, che
 * l'utente conosce già: toccando la lente il campo si attiva da solo con la
 * tastiera e la X per chiudere; a campo vuoto i Recenti, le ultime ricette
 * aperte da qui, in una fila che si scorre di lato; scrivendo, le ricette
 * trovate in tutte le categorie, a tessere come l'elenco a griglia.
 *
 * Il campo è quello di sistema (headerSearchBarOptions): su iOS `autoFocus`
 * non c'è, quindi il fuoco lo dà il ref quando la sezione diventa quella in
 * vista. `obscureBackground: false` perché sotto il campo ci sono i Recenti e
 * devono vedersi mentre si scrive.
 *
 * Aprire una ricetta passa da apriRicetta, come dagli elenchi: sul telefono
 * la pagina a tutto schermo, sugli schermi larghi il pannello (e il menù
 * torna su Ricette). Prima la ricetta va in testa ai Recenti.
 */
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { FlatList, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { SearchBarCommands } from 'react-native-screens';

import { percorsoFoto } from '../../data/foto.ts';
import { leggiImpostazione, salvaImpostazione } from '../../data/impostazioni.ts';
import { elencoCategorie } from '../../data/categorie.ts';
import { elencoRicette } from '../../data/ricette.ts';
import type { Categoria, Ricetta } from '../../domain/tipi.ts';
import { useApp } from '../contesto.ts';
import { datiRicetta, filtraRicette } from '../logica-elenco.ts';
import { CHIAVE_RECENTI, conRecente, leggiRecenti, recentiVisibili } from '../logica-ricerca.ts';
import { usePannelli } from '../pannelli.ts';
import type { Colori } from '../tema.ts';
import { SPAZIO, TESTO, useColori, useTinte } from '../tema.ts';
import { tintaDi } from '../tinte.ts';
import { vocabolario } from '../../domain/lingua/index.ts';
import TesseraRicetta from '../componenti/TesseraRicetta.tsx';

export default function Cerca() {
  const navigation = useNavigation<NativeStackNavigationProp<{ Ricerca: undefined }>>();
  const { db, testo, lingua } = useApp();
  const { apriRicetta } = usePannelli();
  const colori = useColori();
  const tinte = useTinte();
  const stili = useMemo(() => creaStili(colori), [colori]);
  const voc = useMemo(() => vocabolario(lingua), [lingua]);
  const barra = useRef<SearchBarCommands>(null);

  const [query, setQuery] = useState('');
  const [ricette, setRicette] = useState<Ricetta[]>([]);
  const [perId, setPerId] = useState<Record<string, Categoria>>({});
  const [miniature, setMiniature] = useState<Record<string, string>>({});
  const [recenti, setRecenti] = useState<string[]>([]);

  useLayoutEffect(() => {
    navigation.setOptions({
      headerSearchBarOptions: {
        ref: barra,
        placeholder: testo('cerca.segnaposto'),
        onChangeText: (e) => setQuery(e.nativeEvent.text),
        obscureBackground: false,
        hideWhenScrolling: false,
        tintColor: colori.accento,
      },
    });
  }, [navigation, testo, colori]);

  // A ogni ritorno sulla sezione: le ricette (possono essere cambiate), i
  // Recenti, e il fuoco al campo, come in Foto. Il ritardo lascia finire la
  // transizione della tab, altrimenti il fuoco si perde.
  useFocusEffect(
    useCallback(() => {
      let vivo = true;
      (async () => {
        try {
          const [elenco, categorie, salvato] = await Promise.all([
            elencoRicette(db, { tipo: 'tutte' }),
            elencoCategorie(db),
            leggiImpostazione(db, CHIAVE_RECENTI),
          ]);
          const conFoto = elenco.filter((r) => r.foto !== null);
          const percorsi = await Promise.all(conFoto.map((r) => percorsoFoto(r.foto)));
          if (!vivo) return;
          const mappa: Record<string, string> = {};
          conFoto.forEach((r, i) => {
            const p = percorsi[i];
            if (p !== null) mappa[r.id] = p;
          });
          const indice: Record<string, Categoria> = {};
          for (const c of categorie) indice[c.id] = c;
          setRicette(elenco);
          setPerId(indice);
          setMiniature(mappa);
          setRecenti(leggiRecenti(salvato));
        } catch (errore) {
          console.warn('lettura per la ricerca fallita', errore);
        }
      })();
      const fuoco = setTimeout(() => barra.current?.focus(), 350);
      return () => {
        vivo = false;
        clearTimeout(fuoco);
      };
    }, [db]),
  );

  const apri = (id: string) => {
    const nuovi = conRecente(recenti, id);
    setRecenti(nuovi);
    salvaImpostazione(db, CHIAVE_RECENTI, JSON.stringify(nuovi)).catch((errore) => {
      console.warn('salvataggio dei Recenti fallito', errore);
    });
    apriRicetta(id);
  };

  const tessera = (r: Ricetta) => {
    const cat = r.categoriaId !== null && Object.hasOwn(perId, r.categoriaId) ? perId[r.categoriaId] : null;
    const tinta = cat === null ? { forte: colori.testoTenue, tenue: colori.bordo } : tintaDi(cat, tinte);
    const dati = [cat?.nome, ...datiRicetta(r, voc).map((d) => testo(d.chiave, d.valori))]
      .filter((x): x is string => x !== undefined)
      .join(' · ');
    return (
      <TesseraRicetta
        ricetta={r}
        categoria={cat}
        foto={Object.hasOwn(miniature, r.id) ? miniature[r.id] : null}
        tinta={tinta}
        dati={dati}
        colori={colori}
        onPress={() => apri(r.id)}
      />
    );
  };

  const trovate = useMemo(() => filtraRicette(ricette, query), [ricette, query]);
  const ricetteRecenti = useMemo(() => recentiVisibili(recenti, ricette), [recenti, ricette]);

  if (query.trim() === '') {
    return (
      <ScrollView contentInsetAdjustmentBehavior="automatic" style={stili.pagina}>
        {ricetteRecenti.length > 0 && (
          <>
            <Text style={stili.sezione}>{testo('cerca.recenti')}</Text>
            <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={stili.fila}>
              {ricetteRecenti.map((r) => (
                <View key={r.id} style={stili.recente}>
                  {tessera(r)}
                </View>
              ))}
            </ScrollView>
          </>
        )}
      </ScrollView>
    );
  }

  return (
    <FlatList
      style={stili.pagina}
      contentInsetAdjustmentBehavior="automatic"
      keyboardDismissMode="on-drag"
      keyboardShouldPersistTaps="handled"
      data={trovate}
      keyExtractor={(r) => r.id}
      numColumns={2}
      columnWrapperStyle={stili.colonne}
      contentContainerStyle={stili.griglia}
      ListEmptyComponent={<Text style={stili.nulla}>{testo('ricerca.nulla')}</Text>}
      renderItem={({ item }) => tessera(item)}
    />
  );
}

const creaStili = (colori: Colori) =>
  StyleSheet.create({
    pagina: { flex: 1, backgroundColor: colori.fondo },
    sezione: { ...TESTO.titolo, color: colori.testo, marginHorizontal: SPAZIO.l, marginTop: SPAZIO.m, marginBottom: SPAZIO.s },
    fila: { paddingHorizontal: SPAZIO.l, gap: SPAZIO.m },
    recente: { width: 160, height: 200 },
    griglia: { padding: SPAZIO.l, gap: SPAZIO.m },
    colonne: { gap: SPAZIO.m },
    nulla: { ...TESTO.corpo, color: colori.testoTenue, textAlign: 'center', marginTop: SPAZIO.xl },
  });
```
Prima di scriverlo, verificare con una ricerca nel codice i percorsi e i nomi reali di: `elencoCategorie` (modulo in `src/data/`), i tipi `Categoria`/`Ricetta` (import usato da `Elenco.tsx`), `vocabolario` (import usato da `Elenco.tsx`), `tintaDi` e `useTinte` (import usati da `Elenco.tsx`), `TESTO.titolo`/`TESTO.corpo`/`SPAZIO.*` in `tema.ts`. Copiare gli import esattamente da `Elenco.tsx`. Se `TesseraRicetta` ha un'altezza sua (`flex: 1` nella griglia), il contenitore `recente` gliela dà.

- [ ] **Step 3: Controlli e commit**

```bash
npm test && npx tsc --noEmit
git add -A src
git commit -m "La sezione Cerca come in Foto: il campo attivo, i Recenti in fila, le ricette trovate a tessere"
```

---

### Task 6: Collaudo a schermo e documenti

**Files:**
- Modify: `docs/STATO.md` (una sezione «Il menù in basso (2026-09-25)»)
- Modify: `docs/superpowers/specs/2026-09-25-menu-in-basso-design.md` (se il collaudo cambia qualcosa)

- [ ] **Step 1: iPhone** — build Release per il simulatore iPhone 17 (`5BE717F2-746F-48C7-BA3B-047059D7CCE2`, iOS 27) come in `docs/BUILD.md` (prebuild, `pod install`, `xcodebuild ... -sdk iphonesimulator`), installare, `simctl status_bar ... override --time 9:41`, fotografare e toccare con `uvx --from fb-idb idb ui tap` (serve `idb_companion --udid <UDID>`). Controllare: le quattro voci e il vetro; il menù che si rimpicciolisce scorrendo una categoria; cambia vista e + in alto a destra in Ricette, in una categoria e in Categorie; niente campo di ricerca; il + che apre una ricetta nuova; Cerca: tastiera subito, X, Recenti vuoti, una ricerca, una ricetta aperta (a tutto schermo, senza menù), tornando indietro la ricetta nei Recenti con la foto; Impostazioni senza freccia, il tema scuro, l'inglese.

- [ ] **Step 2: Android** — APK Release arm64 (`./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`, JDK 17), emulatore `QuantoBasta_Pixel` sulla porta 5580, installazione con `adb push` + `pm install -r`. Controllare: tre voci con icona ed etichetta; la ricerca in cima com'era, in Ricette e in una categoria; il + tondo **tutto sopra** la barra, in Ricette, in una categoria e in Categorie; il fondo delle liste non coperto dalla barra; una ricetta aperta a tutto schermo senza barra; tema scuro.

- [ ] **Step 3: Tablet Android** — emulatore `QuantoBasta_Tablet` (porta 5580, dopo aver chiuso il telefono): i due pannelli nella sezione Ricette, la barra sotto; aprire e piegare con `adb shell wm size 2560x1000` / `wm size reset` (mai `wm density`).

- [ ] **Step 4: Documenti e commit** — in `docs/STATO.md` la sezione con: cosa si vede sui due sistemi, la lente dentro la capsula su iOS 27 e la libreria da tenere d'occhio, il collaudo fatto e quello che resta (iPhone Duo con Xcode 27.1).

```bash
git add docs
git commit -m "Annota il menù in basso e il collaudo sui due sistemi"
```
