### Fase 11 — Item tags + importance UX

**MVP2 · 3/6.** Tercera fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14).

**Estado: 📋 Planificada. Cierra dos huecos en la experiencia del item de compra: clasificación libre por tags (scope colectivo) y reordenación explícita por arrastrar (importance).**

**Objetivo: enriquecer el item sin tocar el dominio core. La columna `sort_order` ya existe en `shopping_items` (migración 005) y el store ya escribe `update sort_order` en batch al reordenar (`apps/web/src/lib/stores/lists.ts:224`); falta el handle de drag visible en la fila de item. Los tags son nuevos — modelo + UI + filtros.**

**Decisiones clave:**
- Tags **scoped al colectivo**, no globales. Un mismo string ("vegano") en dos colectivos = dos filas distintas. Justifica la FK directa a `collectives`.
- Sin jerarquía. Tag plano. Color opcional (8 presets, sin custom picker en MVP).
- Filtro por tag aplica intersección (item con todos los tags seleccionados), no unión.
- Reordenación: drag handle solo visible en desktop o tras long-press en mobile (evita interferir con swipe-delete).

---

#### 11.1 Modelo de tags

**Fichero:** `supabase/migrations/021_item_tags.sql`.

```sql
create table public.item_tags (
    id              uuid        primary key default gen_random_uuid(),
    collective_id   uuid        not null references public.collectives(id) on delete cascade,
    name            text        not null,
    color           text        not null default 'slate' check (color in ('slate','red','amber','green','sky','indigo','pink','stone')),
    created_at      timestamptz not null default now(),
    unique (collective_id, name)
);
alter table public.item_tags enable row level security;

create policy "select_member" on public.item_tags
    for select using (public.is_member(collective_id));
create policy "mutate_member" on public.item_tags
    for all using (public.is_member(collective_id))
    with check (public.is_member(collective_id));

create table public.shopping_item_tags (
    item_id  uuid not null references public.shopping_items(id) on delete cascade,
    tag_id   uuid not null references public.item_tags(id) on delete cascade,
    primary key (item_id, tag_id)
);
alter table public.shopping_item_tags enable row level security;

create policy "select_via_item" on public.shopping_item_tags
    for select using (public.is_member(public.list_collective_id((select list_id from public.shopping_items where id = item_id))));
create policy "mutate_via_item" on public.shopping_item_tags
    for all using (public.is_member(public.list_collective_id((select list_id from public.shopping_items where id = item_id))))
    with check (public.is_member(public.list_collective_id((select list_id from public.shopping_items where id = item_id))));

create index shopping_item_tags_tag_idx on public.shopping_item_tags (tag_id);
```

**Tareas:**

- [ ] **11.1.1** Migración 021 (arriba).
- [ ] **11.1.2** Añadir tabla al publication realtime (`alter publication supabase_realtime add table public.item_tags, public.shopping_item_tags`).
- [ ] **11.1.3** Tests pgTAP `supabase/tests/011_item_tags.sql`: insert tag como miembro OK; insert como no-miembro denegado; unique (collective_id, name) enforce; join tag de otro colectivo a item denegado por RLS; cascade delete del colectivo limpia ambas tablas.
- [ ] **11.1.4** `just db-types` para regenerar `packages/types/src/database.ts`.

---

#### 11.2 Stores + tipos de dominio

**Ficheros:** `apps/web/src/lib/stores/tags.ts` (nuevo), `packages/types/src/domain.ts`.

- [ ] **11.2.1** Tipo `ItemTag = { id; collective_id; name; color }` + helper `ItemWithTags = ShoppingItem & { tags: ItemTag[] }`.
- [ ] **11.2.2** Store `tagsStore` con `loadTags(collectiveId)`, `createTag(...)`, `deleteTag(id)`, suscripción realtime por colectivo activo.
- [ ] **11.2.3** Extender `loadListItems()` en `lists.ts` para join lateral de tags (una sola query con `select *, tags:item_tags(*)` via PostgREST embedding sobre `shopping_item_tags`).
- [ ] **11.2.4** Funciones `attachTag(itemId, tagId)` / `detachTag(itemId, tagId)` — escriben optimistic en el item local + insert/delete en `shopping_item_tags`.

---

#### 11.3 UI — chips, editor, filtro

**Ficheros:** `apps/web/src/lib/components/TagChip.svelte` (nuevo), `apps/web/src/lib/components/TagPicker.svelte` (nuevo), `apps/web/src/routes/(app)/lists/[id]/+page.svelte`.

- [ ] **11.3.1** `TagChip` — pill pequeño (text-xs px-2 py-0.5 rounded-full) con `bg-{color}-100 text-{color}-700` y variante dark. No interactivo por defecto; prop `onRemove` opcional añade ícono X.
- [ ] **11.3.2** `TagPicker` — combobox: input filtra tags existentes del colectivo + opción "Crear `{query}`" si no coincide. Selección múltiple. Re-usable desde item modal y filtro de lista.
- [ ] **11.3.3** Item row: chips renderizan inline al lado del nombre, líneas en wrap. Tap en chip filtra la lista por ese tag.
- [ ] **11.3.4** Item modal (existing): nueva sección "Etiquetas" con `TagPicker`.
- [ ] **11.3.5** Header de `/lists/[id]`: fila de tags activos como filtro. Selección persiste en query string (`?tags=foo,bar`) para deep-linking. Filtro es intersección.
- [ ] **11.3.6** Settings → gestión de tags del colectivo: `/settings` nueva subsección "Etiquetas" — lista con rename + borrar + cambio de color. Sin pantalla nueva, todo inline.

---

#### 11.4 Importance — drag-reorder UI

**Ficheros:** `apps/web/src/routes/(app)/lists/[id]/+page.svelte`, `apps/web/src/lib/utils/drag.ts` (nuevo o existente — verificar tareas; ver `apps/web/src/routes/(app)/tasks/[id]/+page.svelte` que ya reordena).

**Auditoría previa:** confirmar si tasks reordena con HTML5 DnD o con un helper Pointer/Touch. Replicar el mismo patrón en items para consistencia.

- [ ] **11.4.1** Drag handle (icono `GripVertical` de lucide) en cada item row. Visible siempre en `md:` y up; en mobile aparece tras long-press (300ms) en cualquier zona del item que no sea checkbox/nombre.
- [ ] **11.4.2** Reordenación local optimista; al soltar, batch update via `reorderItems(list)` ya existente en `lists.ts:224`.
- [ ] **11.4.3** Sincronización realtime: si llega un UPDATE de `sort_order` desde otro cliente, reaplicar orden sin animación destructiva (FLIP transition opcional).
- [ ] **11.4.4** Mantener compatibilidad con swipe-delete en mobile — long-press debe ganar sobre swipe horizontal solo si el dedo se mueve <8px en los primeros 300ms.

---

#### 11.5 Tests

- [ ] **11.5.1** Vitest integración `tests/integration/item-tags.test.ts`: crear tag, asociar a item, verificar select; intentar asociar tag de colectivo X a item del colectivo Y → RLS error.
- [ ] **11.5.2** Vitest unit `TagPicker.test.ts`: filtrado por substring, "Crear nuevo" aparece cuando no hay match exacto, selección múltiple.
- [ ] **11.5.3** Playwright `tests/e2e/tags.test.ts`:
  - TG-01 Ana crea tag "vegano", lo asigna a 3 items, filtra lista por "vegano" → 3 items visibles.
  - TG-02 query string `?tags=vegano,oferta` filtra por intersección.
  - TG-03 Borja (otro colectivo) no ve los tags de Ana.
- [ ] **11.5.4** Playwright `tests/e2e/reorder-items.test.ts`:
  - RO-01 Ana arrastra item del puesto 3 al 1 → recarga preserva orden.
  - RO-02 Realtime: Ana reordena, Borja (mismo colectivo) ve el orden nuevo sin reload.

---

#### 11.Z Verificación + cierre

- [ ] **11.Z.1** `just test-all` verde (suma esperada: 259 + 5 pgTAP + ~6 vitest + 5 playwright ≈ 275).
- [ ] **11.Z.2** Lighthouse sobre `/lists/[id]` con 50 items + 8 tags activos: Performance ≥ 85, no degradación vs baseline pre-fase-11.
- [ ] **11.Z.3** Actualizar `messages/en.json` + `messages/es.json` con strings nuevos (placeholder, "Crear tag", "Filtrar por etiqueta", etc.).
- [ ] **11.Z.4** Documentar en `docs/history/fase-11-item-tags-importance.md`.

---

### Riesgos / notas

- **`is_member` en policy de `shopping_item_tags` hace subquery doble** (item → list → collective). Si el plan EXPLAIN muestra coste alto, materializar via función `item_collective_id(uuid) returns uuid stable`.
- **Long-press en iOS Safari** dispara el menú nativo de selección de texto. Necesario `touch-action: none` + `user-select: none` en el item row mientras drag está armado.
- **Realtime con embedding (`select *, tags:...`)** — los eventos llegan por tabla individual. Hay que escuchar `shopping_item_tags` y refrescar el item afectado, no asumir que el embedding se actualiza solo.

### Scope explícito fuera

- Tags "system" sugeridos automáticamente. Solo creación manual.
- Color picker custom. Solo 8 presets.
- Tags en notas o tareas. Solo en `shopping_items`.
- Importar tags de un colectivo a otro.
