docs(plan): v1.0 cycle — fases 17–20 (versioning + list flow + emojis + euskera)

Four planned-fase docs for the first tagged release.

- Fase 17 — semver git tags + CHANGELOG.md (Keep-a-Changelog, [new]/
  [fix]/[tweak] prefixes), one big v0.0.0-beta backfill block covering
  Fases 0–16, About-modal in /settings rendering bundled CHANGELOG via
  ?raw import, deploy-script tag check + tag column in .deploys.log.
- Fase 18 — shopping list flow: required title (client + DB tighten),
  per-collective per-prefix #N auto-numbering parsed from typed title,
  collectives.default_list_title, collective_list_titles catalog + admin
  RPCs, autocomplete union (catalog + last-10). Migration 027.
- Fase 19 — EMOJI_CATALOG with faces/food/animals/objects categories
  (~280 codepoints, Unicode 14 baseline for iOS<17 compat), new
  EmojiPicker.svelte replacing the 8-emoji hardcoded selector at 4
  sites. No migration.
- Fase 20 — Euskera locale: messages/eu.json via machine-translate
  seed flagged with [needs review], users.language check constraint
  extended to ('en','es','eu'), accept-language parser + selector in
  /settings. Migration 028. v1.0.0 tag lands at the end of this fase.

README + CLAUDE.md updated with the v1.0 cycle pointers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 01:01:34 +02:00
parent 1638d2f6e8
commit 427fe33c9c
6 changed files with 576 additions and 0 deletions

View File

@@ -10,6 +10,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
**Fase 16: loading spinner**`Spinner.svelte` (sizes sm/md/lg = 16/24/40px, `currentColor` for theme, `motion-reduce:animate-none`, `role="status"` + sr-only `m.loading()`) integrated at 7 loading sites (auth splash + search + notes archive + create-collective modal + manage page: members-list / invite generator / add-common-item button). No DB migration; reuses existing `m.loading()`. Added 5 Vitest unit (SP-U-01..05) + 3 Playwright e2e (SP-E-01..03). Vitest config gained `resolve.conditions: ['browser']` + `server.deps.inline: [/^svelte/]` so Svelte 5's `mount()` resolves the browser entry; see `docs/history/fase-16-spinner.md`. **Final cumulative MVP2 (fases 916): 177 pgTAP + 179 Vitest integration + 52 Vitest unit + 96 Playwright + 1 gated rate-limit = 505 tests green; 3 skipped.**
**v1.0 — planned (📋):** four fases on `mvp2` branch (will retag at the end). Aimed at producing the first tagged release: **Fase 17 versioning** — git tags (`vX.Y.Z` semver), `CHANGELOG.md` in repo root (Keep-a-Changelog format, `[new]/[fix]/[tweak]` prefixes), one big `[v0.0.0-beta]` backfill section covering Fases 016, About-modal in `/settings` rendering bundled CHANGELOG via `?raw` import, deploy-script tag check + tag column in `.deploys.log` (`plan/fase-17-versioning-changelog.md`) | **Fase 18 shopping list flow** — required title (client + DB tightening), `#N` auto-numbering parsed from prefix per collective, `collectives.default_list_title` + `collective_list_titles` catalog with admin-only RPCs, autocomplete union catalog + last-10 titles (`plan/fase-18-shopping-list-flow.md`, migration 027) | **Fase 19 emoji avatars**`EMOJI_CATALOG` lazy-load per category (faces / food / animals / objects, ~280 codepoints, Unicode 14 baseline for iOS<17 compat), new `EmojiPicker.svelte` replacing the 8-emoji hardcoded selector at 4 sites (`plan/fase-19-emoji-avatars.md`, no migration) | **Fase 20 Euskera locale**`messages/eu.json` via machine-translate seed + `[needs review]` marker, `users.language` check constraint extended to `('en','es','eu')`, `accept-language` parser + selector in `/settings`; v1.0.0 tag lands at the end of this fase (`plan/fase-20-euskera-locale.md`, migration 028).
## Documentation Map
- `README.md` — full development plan, confirmed tech stack, Justfile reference, technical warnings

View File

@@ -28,6 +28,11 @@
| ↳ Fase 14 — PWA hardening | ✅ |
| ↳ Fase 15 — Common items management | ✅ |
| ↳ Fase 16 — Loading spinner | ✅ |
| **v1.0** — Fase 17 → 20 | 📋 Planificada |
| ↳ Fase 17 — Versioning + CHANGELOG | 📋 |
| ↳ Fase 18 — Shopping list flow | 📋 |
| ↳ Fase 19 — Emoji avatars | 📋 |
| ↳ Fase 20 — Euskera locale | 📋 (cierra `v1.0.0`) |
| Suite de tests (pgTAP + Vitest + Playwright) | ✅ **505 tests verdes** (177 pgTAP + 179 integración + 52 unit + 96 E2E) + 1 rate-limit gated (`just test-rate-limit`). 3 skipped en `just test-all` (2 Realtime presence — upstream `handle_out/3` bug; 1 rate-limit — gated). |
---
@@ -63,6 +68,10 @@ Las fases ya cerradas (0, 1, 2a) reflejan este patrón retroactivamente: la secc
| **MVP2 — Fase 14** | [fase-14-pwa-hardening.md](plan/fase-14-pwa-hardening.md) | 35 días |
| **MVP2 — Fase 15** | [fase-15-common-items.md](plan/fase-15-common-items.md) | 35 días |
| **MVP2 — Fase 16** | [fase-16-spinner.md](plan/fase-16-spinner.md) | 1 día |
| **v1.0 — Fase 17** | [fase-17-versioning-changelog.md](plan/fase-17-versioning-changelog.md) | 23 días |
| **v1.0 — Fase 18** | [fase-18-shopping-list-flow.md](plan/fase-18-shopping-list-flow.md) | 35 días |
| **v1.0 — Fase 19** | [fase-19-emoji-avatars.md](plan/fase-19-emoji-avatars.md) | 12 días |
| **v1.0 — Fase 20** | [fase-20-euskera-locale.md](plan/fase-20-euskera-locale.md) | 12 días (+ review humana) |
**Total estimado MVP (Fase 06): 1116 semanas. MVP2 (Fase 914, rama `mvp2`): 1014 semanas. Fase 7+8 entre MVP y MVP2 como cleanup post-launch.**

View File

@@ -0,0 +1,126 @@
### Fase 17 — Versioning, git tags + CHANGELOG + about modal
**v1.0 · 1/4.** First fase of the v1.0 cycle (post-MVP2). Lands the version-tracking infrastructure that every later v1.0 fase will register entries against.
**Estado: 📋 Planificada.** Atiende cuatro asks acoplados:
1. **Git tags** — usar `vMAJOR.MINOR.PATCH` semver. Tag creado localmente; el script de deploy verifica que `HEAD` apunta a un tag antes de empezar (warn pero no bloquea).
2. **CHANGELOG.md** — formato Keep-a-Changelog. Cada release una sección `## [vX.Y.Z] — YYYY-MM-DD` con sub-bloques `### Added / Changed / Fixed`. Cada bullet anotado `[new]`, `[fix]` o `[tweak]`. Backfill de toda la historia previa como una sola sección beta.
3. **About modal en `/settings`** — abre desde la sección "Acerca de" (ya existe el chip de versión, Fase 14.3.4); el modal renderiza `CHANGELOG.md` completo, importado en build-time (`?raw`) — sin fetch en runtime.
4. **v1.0.0** se etiqueta al cierre de Fase 20 (Euskera), no aquí.
**Decisiones clave:**
- Versión canónica vive en el `package.json` de la raíz (lo usa Fase 14.3 via `__APP_VERSION__`). Bump manual antes de tag.
- `CHANGELOG.md` en raíz, no en `docs/`. Tooling como GitHub releases lo busca ahí por convención.
- Backfill BETA = **una sola sección** `## [v0.0.0-beta] — 2026-05-19 — MVP + MVP2 (Fases 016)` con 3050 bullets organizados por área (auth, lists, tasks, notes, PWA, infra). Sin sub-secciones por fase — los plan/history docs ya cubren ese nivel.
- Modal de changelog: parser markdown ligero (no librería; basta `marked` si ya está en el bundle, si no `String.replace` para `###`/`-`/`**`). Solo renderiza secciones — no enlaces externos.
- Tag creation: manual `git tag -a vX.Y.Z -m "..."` por ahora. No automatizar push de tags hasta que el flujo esté probado.
---
#### 17.1 CHANGELOG.md backfill
**Fichero:** nuevo `CHANGELOG.md` en raíz.
**Tareas:**
- [ ] **17.1.1** Crear `CHANGELOG.md` con cabecera estándar Keep-a-Changelog:
```markdown
# Changelog
All notable changes are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [v0.0.0-beta] — 2026-05-19 — MVP + MVP2 (Fases 016)
...
```
- [ ] **17.1.2** Backfill el bloque BETA leyendo cada `docs/history/fase-*.md` + `plan/fase-*.md`. 3050 bullets agrupados:
- **Auth + identity** (Fase 1, 7, 8)
- **Shopping lists + items** (Fase 2a, 2b, 4, 5, 7, 10, 11, 15)
- **Tasks + notes** (Fase 3, 9.3)
- **Search** (Fase 4)
- **Mobile UX + accessibility** (Fase 5, 9, 16)
- **PWA + offline** (Fase 6, 14)
- **Collective management** (Fase 10, 12, 13)
- **Server admin** (Fase 13)
- **i18n** (Fase 0, 4)
- **Infra + deploy** (Fase 0, 6, 8, 14.3, plus the off-stack proxy + Caddy + backup/rollback work)
- [ ] **17.1.3** Cada bullet con prefix `[new]` / `[fix]` / `[tweak]`. Ejemplos:
- `[new] Item tags scoped per collective + drag-reorder (Fase 11).`
- `[fix] auth.users role/aud trigger to keep PostgREST routing past Keycloak provisioning (migrations 013014).`
- `[tweak] Edge proxy off-stack — internal Caddy on :3000 + external TLS terminator.`
---
#### 17.2 About modal + changelog viewer
**Ficheros:** nuevo `apps/web/src/lib/components/ChangelogModal.svelte`, edits a `apps/web/src/routes/(app)/settings/+page.svelte`.
**Tareas:**
- [ ] **17.2.1** Importar el CHANGELOG en build-time: añadir `import changelogRaw from '../../../../CHANGELOG.md?raw'` en el módulo del modal. Verificar que el path se resuelve desde `apps/web/` (3 niveles arriba); ajustar si falla bajo el adapter-node.
- [ ] **17.2.2** Parser ligero inline: split por líneas, formatear `##`/`###` como headers, `-` como list items, `**text**` como bold via `<strong>`. Sin XSS surface — el input es estático del repo, no usuario.
- [ ] **17.2.3** `ChangelogModal.svelte`:
- Trigger: prop `open: boolean` + `onClose`.
- Layout: overlay + panel centered (reusar el patrón de `CreateCollectiveModal.svelte`).
- Header: "Historial de cambios" + close icon.
- Body: scrollable, max-h-[70vh]. Sticky version chips a la izquierda (opcional).
- Cierra con Esc o click en overlay.
- [ ] **17.2.4** En `/settings`, debajo del chip de versión existente, añadir un botón discreto "Ver historial" (text-xs subrayado) que setea `showChangelog = true`.
---
#### 17.3 Deploy script tag check
**Fichero:** `infra/scripts/deploy-erosi.sh`.
- [ ] **17.3.1** Al inicio (antes del rsync), si `git describe --exact-match --tags HEAD 2>/dev/null` falla (HEAD no es un tag), imprimir advertencia amarilla:
```
WARNING: HEAD is not at a tagged commit. Deploys without a tag won't
map cleanly to CHANGELOG entries. Continue? [5s abort]
```
Sleep 5s con Ctrl-C abort. Si está taggeado, mostrar el tag al lado de la SHA en el banner.
- [ ] **17.3.2** Cuando esté en un tag, incluir el tag (no solo la SHA) en `.deploys.log` como columna 4 opcional: `<ts>\t<sha>\t<backup>\t<tag-or-empty>`. Mantener compat: las 3 columnas anteriores siguen ahí.
- [ ] **17.3.3** `rollback-erosi.sh --list` muestra el tag si está presente.
---
#### 17.4 Tests
- [ ] **17.4.1** Vitest unit `apps/web/src/lib/components/ChangelogModal.test.ts`:
- CHM-U-01 modal cerrado no renderiza nada.
- CHM-U-02 modal abierto renderiza el `<h1>` del CHANGELOG.
- CHM-U-03 click en overlay dispara `onClose`.
- CHM-U-04 tecla Esc dispara `onClose`.
- [ ] **17.4.2** Playwright `tests/e2e/changelog.test.ts`:
- CL-01 Ana abre `/settings`, clica "Ver historial", modal aparece con la cabecera "v0.0.0-beta" visible.
- CL-02 Esc cierra el modal.
- [ ] **17.4.3** Unit linter: chequeo del CHANGELOG con un script `scripts/check-changelog.mjs` que valida (a) cabecera Keep-a-Changelog presente, (b) cada release tiene formato `## [vX.Y.Z] — YYYY-MM-DD`, (c) cada bullet empieza con `[new]`/`[fix]`/`[tweak]`. Wire en `just check`.
---
#### 17.Z Verificación + cierre
- [ ] **17.Z.1** `just test-all` verde. Suma esperada: +4 unit + 2 e2e + 0 pgTAP = +6.
- [ ] **17.Z.2** Smoke: abrir `/settings` en prod, ver el botón "Ver historial", abrir el modal, verificar contenido.
- [ ] **17.Z.3** Bump `package.json` version → `1.0.0-rc.0` para empezar el ciclo v1.0 (el `1.0.0` se reserva para el cierre de Fase 20). Tag `v1.0.0-rc.0` desde HEAD.
- [ ] **17.Z.4** Documentar en `docs/history/fase-17-versioning-changelog.md`.
- [ ] **17.Z.5** Strings Paraglide nuevos: `settings_about_view_changelog`, `changelog_modal_title`, `changelog_modal_close`.
---
### Riesgos / notas
- **`?raw` import del CHANGELOG**: Vite lo bundlea como string al servir el módulo. Bundle size sube ~520KB. Si crece más allá de eso (varias releases), considerar lazy-load del modal vía dynamic `import()`.
- **Tag check no bloquea el deploy**: warn + sleep, no abort. Mantener el flujo "deploy en cualquier commit" disponible para hotfixes urgentes.
- **Linter de CHANGELOG**: no fail-loud en pre-commit hooks; rompe el flujo si alguien edita el CHANGELOG manualmente. Mejor warn-only.
- **Markdown parser inline**: lo suficiente para Keep-a-Changelog (headers, lists, bold). Si el CHANGELOG crece con tablas, code blocks, links → swap a `marked` (que ya está si se añade un plugin de markdown para algún otro flujo) o `micromark-light`.
### Scope explícito fuera
- Auto-generación del changelog desde commits (conventional-commits + standard-version). Manual por ahora.
- GitHub Releases sync. Solo tags locales.
- Push de tags al remoto desde el deploy script. Manual.
- Versionado de la API/REST. Solo del cliente.
- Notificación push de "nueva versión" — Fase 14.1 ya cubre el toast.

View File

@@ -0,0 +1,189 @@
### Fase 18 — Shopping list creation flow (required title + numbered prefix + autocomplete + defaults)
**v1.0 · 2/4.** Segunda fase del ciclo v1.0.
**Estado: 📋 Planificada.** Cuatro asks acoplados sobre el momento "creo una lista nueva":
1. **Título requerido** — no permitir crear sin nombre. Cliente bloquea + DB tightens.
2. **Auto-numerado `#N`** — si el título escrito coincide con un prefijo usado en el colectivo (ej. `Compra`), sugerir `Compra #N` con `N = max(N) + 1`. El `#N` es **parte del título** (campo `name`), no columna separada.
3. **Default list title** — admin del colectivo configura un título por defecto que prefilla el input del modal create-list.
4. **Catálogo de títulos** — admin gestiona una tabla de títulos sugeridos (CRUD inline en `/collective/manage`); aparecen en el autocomplete junto a los últimos 10 títulos reales.
**Decisiones clave:**
- `shopping_lists.name` ya es `NOT NULL` (migración 005). Falta enforce mínimo en cliente + check de longitud > 0 con trim.
- Nuevo `collectives.default_list_title text` (nullable). Vacío = no prefill.
- Nuevo `collective_list_titles (collective_id, title text, primary key)` — mirror del shape de `item_frequency`. Solo admin escribe via RPC.
- Autocomplete: union de (a) `collective_list_titles` + (b) últimos 10 `name` distintos de `shopping_lists` (mismo colectivo). Deduplicate case-insensitive.
- `#N` parsing: regex `^(.+?)\s*(?:#(\d+))?\s*$`. Prefijo = grupo 1. Si el usuario escribe `Compra`, miramos `name ILIKE 'Compra %' OR name ILIKE 'Compra'` y extraemos el `#N` maximal. Sugerimos `Compra #N+1`. Si el usuario escribe `Compra #3` explícitamente, no auto-bump.
- Numbering scope = per-collective per-prefix (case-insensitive prefix match).
---
#### 18.1 Modelo
**Fichero:** `supabase/migrations/027_list_title_catalog.sql`.
```sql
-- Default per-collective title (single string, nullable).
alter table public.collectives
add column default_list_title text;
-- Catalog of admin-curated suggested titles.
create table public.collective_list_titles (
collective_id uuid not null references public.collectives(id) on delete cascade,
title text not null,
created_at timestamptz not null default now(),
primary key (collective_id, title)
);
alter table public.collective_list_titles enable row level security;
-- Members read; only RPC writes.
create policy "select_member" on public.collective_list_titles
for select using (public.is_member(collective_id));
create policy "deny_direct_writes" on public.collective_list_titles
for all using (false) with check (false);
-- Admin-only RPCs.
create or replace function public.add_list_title(
p_collective_id uuid, p_title text
) returns void
language plpgsql security definer
set search_path = public, auth
as $$
declare
v_role text;
v_norm text := trim(p_title);
begin
if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if;
if v_norm = '' then raise exception 'title cannot be empty' using errcode = 'P0001'; end if;
select role into v_role from public.collective_members
where collective_id = p_collective_id and user_id = auth.uid();
if v_role is distinct from 'admin' then
raise exception 'only admins can curate list titles' using errcode = 'P0002';
end if;
insert into public.collective_list_titles (collective_id, title)
values (p_collective_id, v_norm)
on conflict do nothing;
end; $$;
create or replace function public.remove_list_title(
p_collective_id uuid, p_title text
) returns void
language plpgsql security definer
set search_path = public, auth
as $$
declare v_role text;
begin
if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if;
select role into v_role from public.collective_members
where collective_id = p_collective_id and user_id = auth.uid();
if v_role is distinct from 'admin' then
raise exception 'only admins can curate list titles' using errcode = 'P0002';
end if;
delete from public.collective_list_titles
where collective_id = p_collective_id and title = trim(p_title);
end; $$;
grant execute on function public.add_list_title(uuid, text) to authenticated;
grant execute on function public.remove_list_title(uuid, text) to authenticated;
```
**Tareas:**
- [ ] **18.1.1** Migración 027.
- [ ] **18.1.2** Tests pgTAP `supabase/tests/019_list_title_catalog.sql`:
- default_list_title default NULL, admin update OK, member update denied via RLS.
- add_list_title como admin OK, idempotente por unique.
- add_list_title como member → P0002.
- add_list_title con título vacío → P0001.
- remove_list_title como admin borra fila.
- remove_list_title como guest → P0002.
- [ ] **18.1.3** `just db-types`.
---
#### 18.2 Stores
**Fichero:** nuevo `apps/web/src/lib/stores/listTitles.ts`, extender `apps/web/src/lib/stores/lists.ts`.
- [ ] **18.2.1** Store `listTitles`:
- `loadTitleCatalog(collectiveId)` — select * from `collective_list_titles`.
- `addTitle(collectiveId, title)` / `removeTitle(collectiveId, title)` — RPCs.
- [ ] **18.2.2** Helper `fetchTitleSuggestions(collectiveId, prefix)` — union catalog + últimos 10 `shopping_lists.name` distintos. Dedupe case-insensitive. Limit 15.
- [ ] **18.2.3** Helper `computeNextNumber(collectiveId, basePrefix)`:
- Query: `select name from shopping_lists where collective_id = $1 and name ILIKE $2 and deleted_at is null`.
- Pattern `$2 = basePrefix || ' #%'` plus `basePrefix` exact.
- Parse trailing `#(\d+)` con regex. Tomar `max(N) + 1`. Si ninguno tiene número, devolver `1` solo si el prefijo bare existe (ya hay una sin número), de otro modo `null`.
- [ ] **18.2.4** Tests integración `packages/test-utils/tests/list-title-flow.test.ts`:
- LT-INT-01 admin add → suggestions incluyen el título.
- LT-INT-02 member add → P0002.
- LT-INT-03 computeNextNumber con 3 listas `Compra #1`, `Compra #2`, `Compra #5` → devuelve 6.
- LT-INT-04 computeNextNumber sin matches → null.
- [ ] **18.2.5** Tests unit `apps/web/src/lib/utils/list-title.test.ts` (extraer la regex + numbering en un módulo puro):
- LT-U-01 parse de "Compra #5" → `{ prefix: 'Compra', number: 5 }`.
- LT-U-02 parse de "Compra" → `{ prefix: 'Compra', number: null }`.
- LT-U-03 parse de "#5" (sin prefix) → no autonumera (returns null prefix).
- LT-U-04 nextNumber sobre lista de números `[1,2,5]` → 6.
- LT-U-05 nextNumber sobre lista vacía → 1.
---
#### 18.3 UI — create-list modal
**Ficheros:** ubicación actual de creación de lista (probablemente `apps/web/src/routes/(app)/lists/+page.svelte` o similar — auditar).
- [ ] **18.3.1** Input prefilled con `currentCollective.default_list_title || ''`.
- [ ] **18.3.2** Mientras el usuario escribe:
- Si el typed value matches un prefijo conocido + no incluye `#N`, mostrar un chip discreto `Sugerencia: Compra #6` (texto pequeño debajo del input, click rellena el input).
- Suggestion list aparece como dropdown con `fetchTitleSuggestions(prefix)`. Mismo patrón que `TagPicker` (Fase 11).
- [ ] **18.3.3** Submit deshabilitado si `name.trim() === ''`. Sin spinner cuando deshabilitado.
- [ ] **18.3.4** Si submit ocurre y el título matches un prefijo conocido sin `#N` y `computeNextNumber` devuelve un valor, **auto-suffix** el `#N` antes de insertar. Visible para el usuario en un toast no intrusivo: "Creada como `Compra #6`".
**Decisión UX:** auto-suffix solo si el typed value coincide con un título del catálogo. Para prefijos libres (no en catalog), respetar lo que el usuario escribió tal cual.
---
#### 18.4 UI — `/collective/manage` catálogo
**Fichero:** `apps/web/src/routes/(app)/collective/manage/+page.svelte`.
- [ ] **18.4.1** Nueva subsección "Títulos sugeridos" debajo de "Items frecuentes":
- Input `default_list_title` (text input, single-line, save on blur).
- Lista de títulos curados con botón "Borrar" por fila.
- Botón "Añadir título" + modal pequeño con input + Save.
- [ ] **18.4.2** Members ven la sección read-only (lista visible; sin acciones). Guests no la ven.
---
#### 18.5 Tests E2E
- [ ] **18.5.1** Playwright `tests/e2e/list-title-flow.test.ts`:
- **LTF-01** Ana setea `default_list_title = "Compra"` en manage; crea lista nueva → input prefilled "Compra"; submit → lista creada como "Compra" (no `#1` aún porque no había prefix-match en catalog antes).
- **LTF-02** Ana añade "Compra" al catálogo; crea 3 listas; verifica que la 3ª se nombra "Compra #2" automáticamente (la 1ª = "Compra", 2ª = "Compra #1" si #1 ausente; ajustar según `computeNextNumber` exact behavior).
- **LTF-03** Ana intenta crear con input vacío → botón disabled, no fire.
- **LTF-04** Borja (member) en manage ve la subsección con acciones disabled.
---
#### 18.Z Verificación + cierre
- [ ] **18.Z.1** `just test-all` verde. Suma esperada: +6 pgTAP + 4 integración + 5 unit + 4 e2e = +19.
- [ ] **18.Z.2** Smoke: crear 3 listas seguidas con prefijo común; verificar numbering correcto.
- [ ] **18.Z.3** Entrada en CHANGELOG: `[new] Required list titles + auto-numbered prefix + admin-curated title catalog (Fase 18).`
- [ ] **18.Z.4** Documentar en `docs/history/fase-18-shopping-list-flow.md`.
---
### Riesgos / notas
- **Race condition en numbering**: dos usuarios crean a la vez con prefijo `Compra`. Ambos clientes resuelven `computeNextNumber → 6`. Resultado: dos `Compra #6`. Mitigación posible: unique constraint `(collective_id, name)` — pero rompe casos donde es deliberado tener duplicados. Aceptar el race; documentar.
- **Caso "el usuario quiere típo libre"**: si el typed value no matches un prefix conocido, no auto-suffix. Asegurar que el chip de sugerencia solo aparece para matches del catálogo.
- **Default title sensible al colectivo activo**: al cambiar de colectivo en la sidebar, el modal create-list debe re-leer `currentCollective.default_list_title`. Reactivo vía store.
- **i18n**: el `#N` formato es universal; no traducir. Strings nuevos: `create_list_input_placeholder`, `create_list_suggestion_chip`, `create_list_auto_numbered_toast`, `manage_list_titles_section_title`, `manage_default_list_title_label`.
### Scope explícito fuera
- Plantillas de items (lista con items pre-cargados). Solo título.
- Numbering por mes/año (formato `Compra 2026-05`). Solo `#N` integer.
- Edición masiva del catálogo via CSV. Solo CRUD inline.
- Sugerencias cross-collective (títulos de otros colectivos del mismo usuario).

View File

@@ -0,0 +1,114 @@
### Fase 19 — Emoji avatars expansion (food + animal categories)
**v1.0 · 3/4.** Tercera fase del ciclo v1.0.
**Estado: 📋 Planificada.** Petición concreta: el avatar picker actual (`/settings`, también en colective-onboarding y `CreateCollectiveModal`) ofrece 8 emojis hardcoded. Ampliar con **todos los emojis oficiales** de food + animals según Unicode CLDR.
**Objetivo: agrupar emojis en categorías navegables (no scroll infinito flat), añadir food + animals completos (~270 glyphs combinados según Unicode 15), mantener performance del picker.**
**Decisiones clave:**
- Fuente de verdad: lista hardcoded en `apps/web/src/lib/data/emoji-catalog.ts`, no fetched. Generada una sola vez desde Unicode CLDR; commit del JSON. Tamaño ~510KB.
- Categorías visibles: **Caras** (mantiene los 8 actuales + ~30 más comunes), **Comida** (todos los food/drink), **Animales** (todos los animal/nature), **Objetos** (un pequeño set útil). El picker abre por defecto en "Caras"; pestañas en la cabecera.
- Sin emoji search en MVP de la fase — solo navegación por pestaña. Search si crece.
- Variantes de skin tone parked.
---
#### 19.1 Catálogo
**Fichero:** nuevo `apps/web/src/lib/data/emoji-catalog.ts`.
```ts
export type EmojiCategory = 'faces' | 'food' | 'animals' | 'objects';
export const EMOJI_CATALOG: Record<EmojiCategory, readonly string[]> = {
faces: ['😀', '😎', '🥳', '🤓', '👻', '🤖', '👽', '🥸', /* ~30 más */],
food: ['🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', /* ~85 más, todo Unicode "Food & Drink" */],
animals: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', /* ~150 más, todo "Animals & Nature" */],
objects: ['🏠', '🏡', '🌳', '🌻', '⭐', '🔥', '🎁', '🎈', /* ~20 más */],
} as const;
export const CATEGORY_LABELS: Record<EmojiCategory, string> = {
faces: 'Caras',
food: 'Comida',
animals: 'Animales',
objects: 'Objetos',
};
```
**Tareas:**
- [ ] **19.1.1** Generar los arrays. Fuente recomendada: [unicode-emoji-json](https://github.com/muan/unicode-emoji-json) `data-by-group.json`. Copiar el bloque "Food & Drink" + "Animals & Nature". Filtrar variantes (eliminar items con ZWJ secuencias raras como `🐕‍🦺` si Vite no las renderiza consistente). Mantener Unicode 15 baseline (sin Unicode 16 que iOS aún no incluye).
- [ ] **19.1.2** Tamaño final esperado: ~280 strings, ~10KB del bundle. Verificar.
- [ ] **19.1.3** Generador deterministic — un script `scripts/build-emoji-catalog.mjs` ejecutable manualmente que rebuild el `.ts` desde una copia local del JSON. No corre en CI; solo cuando Unicode se actualiza.
---
#### 19.2 Componente EmojiPicker
**Fichero:** nuevo `apps/web/src/lib/components/EmojiPicker.svelte`.
- [ ] **19.2.1** Props: `selected: string | null`, `onSelect: (emoji: string) => void`, `onClose?: () => void`.
- [ ] **19.2.2** Layout: header con pestañas (4 botones), body grid `grid-cols-8` (responsive `sm:grid-cols-10`), cada celda 32×32 con `text-2xl`. Click selecciona + dispara `onSelect`.
- [ ] **19.2.3** Categoría seleccionada en `$state`, default `'faces'`. Switch instantáneo (no animación).
- [ ] **19.2.4** Lazy render: solo monta el grid de la categoría activa (no las 4 al mismo tiempo) — evita coste de ~280 nodos vacíos.
- [ ] **19.2.5** A11y: pestañas son `role="tab"`, grid `role="grid"`, cada celda `role="gridcell"` + aria-label con el codepoint name. Keyboard nav (arrow keys) entre celdas.
---
#### 19.3 Integraciones
**Ficheros:**
- `apps/web/src/routes/(app)/settings/+page.svelte` (avatar del usuario).
- `apps/web/src/routes/(app)/onboarding/+page.svelte` (emoji inicial del colectivo).
- `apps/web/src/lib/components/CreateCollectiveModal.svelte` (emoji al crear).
- `apps/web/src/routes/(app)/collective/manage/+page.svelte` (emoji al renombrar — Fase 10.2).
- [ ] **19.3.1** Reemplazar los selectores hardcoded de emoji por `<EmojiPicker selected={...} onSelect={...} />`. Mantener el mismo padding/border del contenedor para no romper layouts.
- [ ] **19.3.2** Verificar que `users.avatar_emoji` y `collectives.avatar_emoji` siguen aceptando los nuevos glyphs (text column — sí).
---
#### 19.4 Tests
- [ ] **19.4.1** Vitest unit `apps/web/src/lib/components/EmojiPicker.test.ts`:
- EP-U-01 4 pestañas presentes.
- EP-U-02 default category = 'faces'.
- EP-U-03 click en una celda dispara `onSelect` con el emoji correcto.
- EP-U-04 cambiar pestaña re-renderiza el grid con la categoría correcta.
- EP-U-05 arrow keys mueven foco entre celdas.
- [ ] **19.4.2** Vitest unit `emoji-catalog.test.ts`:
- EC-U-01 `EMOJI_CATALOG.food.length >= 85`.
- EC-U-02 `EMOJI_CATALOG.animals.length >= 130`.
- EC-U-03 cada categoría no contiene duplicados.
- EC-U-04 no overlap entre categorías (un emoji solo en una).
- [ ] **19.4.3** Playwright `tests/e2e/emoji-picker.test.ts`:
- **EP-E-01** Ana en `/settings`, abre el picker, cambia a "Comida", selecciona 🍎, guarda → `users.avatar_emoji` actualizado, avatar renderiza 🍎.
- **EP-E-02** Onboarding: Eva (nuevo usuario) crea colectivo con emoji 🐱 desde "Animales".
---
#### 19.Z Verificación + cierre
- [ ] **19.Z.1** `just test-all` verde. Suma esperada: +9 unit + 2 e2e = +11.
- [ ] **19.Z.2** Smoke en iOS Safari + Android Chrome — verificar que TODOS los emojis del catálogo renderizan (no tofu □). Si algunos no, eliminar de la lista.
- [ ] **19.Z.3** Bundle-size check: bundle gz total no debe subir más de ~10KB respecto al baseline.
- [ ] **19.Z.4** Entrada en CHANGELOG: `[new] Emoji picker expanded with full food + animals categories (Fase 19).`
- [ ] **19.Z.5** Documentar en `docs/history/fase-19-emoji-avatars.md`.
---
### Riesgos / notas
- **iOS vs Android emoji rendering**: algunos Unicode 15 codepoints (🩷, 🩵, 🪿) aún no están en iOS 16 (sí en 17.4+). Filtrar a Unicode 14 si la base de usuarios incluye iOS < 17. Documentar la decisión.
- **CLDR shortName as aria-label**: para a11y, necesitamos el nombre del emoji ("red apple"). Si añadir el JSON completo de nombres infla el bundle, fallback a `aria-label={emoji}` (lee como "ÄPFel" en VoiceOver — feo pero funcional).
- **ZWJ sequences**: 👨‍👩‍👧 (familia) o 🏳️‍🌈 (rainbow flag) son secuencias múltiples. Los excluimos del catálogo en MVP de la fase (variantes de skin tone tampoco).
- **Performance del grid grande**: ~280 nodos en el DOM si renderizamos todas las pestañas. Por eso lazy-render por pestaña activa.
### Scope explícito fuera
- Buscar emoji por nombre.
- Skin tone variants.
- Recently-used (tracking de los últimos emojis del usuario).
- Custom emojis subidos por el colectivo.
- Sincronización del emoji con el sistema operativo del usuario.

View File

@@ -0,0 +1,136 @@
### Fase 20 — Euskera locale (eu)
**v1.0 · 4/4.** Última fase del ciclo v1.0. Tras cerrarla, se taggea `v1.0.0`.
**Estado: 📋 Planificada.** Añadir Euskera (Basque, ISO 639-1 `eu`) como tercer idioma soportado, sumándose a `en` y `es`. Tras esta fase la app está disponible para los hogares vasco-parlantes.
**Estado actual (auditado 2026-05-19):**
- Paraglide en `apps/web/messages/{en,es}.json`. Compile-time tree-shaken por idioma.
- Selector de idioma actual: `users.language text` (probablemente `en` | `es` por check constraint o convención). Auto-detect ya en Fase 10.8.
- ~300+ keys en cada locale (estimado por el tamaño actual).
**Decisiones clave:**
- **Machine-translate seed + flag for review (opción B):** generar `messages/eu.json` con traducción automática (DeepL-formatted-text o GPT-class) marcando cada valor con sufijo `[needs review]` para que la persona euskera-hablante pueda repasar y borrar el marcador.
- Soporte de idiomas declarados en la lista canónica: `apps/web/src/lib/i18n/supported.ts` (verificar nombre exacto, puede que viva en `paraglide` config). Añadir `'eu'`.
- `users.language` accept `'eu'` en el check constraint (si existe). Si no hay constraint, añadirlo ahora con 3 valores.
- Auto-detect (Fase 10.8): reconocer `eu` / `eu-ES` / `eu-FR` en `accept-language`.
- Selector visible: `/settings` ya tiene un control de idioma. Añadir opción "Euskera".
---
#### 20.1 Migración: language check
**Fichero:** `supabase/migrations/028_users_language_eu.sql`.
```sql
-- Drop the existing check (if it exists; some seeds may not have it).
alter table public.users
drop constraint if exists users_language_check;
-- Re-add with eu allowed.
alter table public.users
add constraint users_language_check
check (language in ('en','es','eu'));
```
**Tareas:**
- [ ] **20.1.1** Migración 028.
- [ ] **20.1.2** Tests pgTAP `supabase/tests/020_users_language_eu.sql`:
- update a `'eu'` accepted.
- update a `'fr'` rejected by check.
- [ ] **20.1.3** `just db-types`.
---
#### 20.2 Locale file
**Fichero:** nuevo `apps/web/messages/eu.json`.
- [ ] **20.2.1** Pipeline de generación (manual, una vez):
1. Tomar `messages/es.json` como source-of-truth (lengua romance + base traductor más fiable hacia euskera que `en`).
2. Traducir cada valor con DeepL API o GPT con prompt "Translate to Euskera (Basque), keep ICU placeholders intact, preserve punctuation, formal register".
3. Marcar cada valor traducido con sufijo ` [needs review]` antes del cierre de la string. **Excepción**: las keys con placeholders ICU (`{name}`, `{count}`) llevan también el sufijo pero la review debe verificar que los placeholders no se hayan movido a posiciones gramaticalmente raras para euskera (SOV order vs SVO).
- [ ] **20.2.2** Validar el JSON: misma key set que `en.json` y `es.json`. Script `scripts/check-locales.mjs` que diff las keys y falla si faltan o sobran.
- [ ] **20.2.3** Bundle Paraglide rebuild: `pnpm --filter @colectivo/web exec paraglide-compile` (o cualquiera que sea el comando — verificar en `package.json`).
---
#### 20.3 Wiring
**Ficheros:**
- `apps/web/src/lib/i18n/*` (configuración de Paraglide — locales soportados).
- `apps/web/src/lib/utils/accept-language.ts` (Fase 10.8 — añadir `'eu'` al set conocido).
- `apps/web/src/routes/(app)/settings/+page.svelte` (selector de idioma).
**Tareas:**
- [ ] **20.3.1** Añadir `'eu'` al array de idiomas en la config de Paraglide (`project.inlang/settings.json` u otro fichero del plugin).
- [ ] **20.3.2** `accept-language.ts` parser: reconocer `eu`, `eu-ES`, `eu-FR``'eu'`.
- [ ] **20.3.3** Selector de idioma en `/settings`: nueva opción "Euskera". Label en cada idioma:
- `en`: "Basque"
- `es`: "Euskera"
- `eu`: "Euskera"
- [ ] **20.3.4** Cambiar idioma a `eu` recarga Paraglide tags sin reload de página (`setLanguageTag('eu')`) + escribe `users.language = 'eu'`.
---
#### 20.4 Tests
- [ ] **20.4.1** Vitest unit `apps/web/src/lib/utils/accept-language.test.ts` (extender):
- LANG-U-EU-01 `eu``'eu'`.
- LANG-U-EU-02 `eu-ES``'eu'`.
- LANG-U-EU-03 `eu-FR;q=0.5,en``'eu'` (preferencia primaria).
- [ ] **20.4.2** Vitest integración `language-bootstrap.test.ts` (extender):
- LANG-INT-EU-01 user setea language=eu, update OK.
- LANG-INT-EU-02 user intenta language='fr' → check constraint violation.
- [ ] **20.4.3** Playwright `tests/e2e/euskera.test.ts`:
- **EU-01** Ana cambia idioma a Euskera en `/settings` → strings nav cambian (Listas → "Erosketak", o lo que diga el JSON), sin reload.
- **EU-02** Eva (usuario nuevo) con header `Accept-Language: eu` → onboarding renderiza en Euskera.
---
#### 20.5 Review checklist (post-translation)
Antes del v1.0.0 release, abrir issue/asignar tarea para alguien euskera-hablante:
- [ ] **20.5.1** Repasar `messages/eu.json`, eliminar el sufijo ` [needs review]` de cada string corregido.
- [ ] **20.5.2** Para keys con plurales (`item_count_one`, `item_count_other` si existen), verificar que ambos son correctos (euskera no distingue como inglés/español; puede ser una sola forma).
- [ ] **20.5.3** Términos del dominio: "colectivo", "lista", "tarea", "nota", "compra". Acordar la traducción con el cliente (no asumir que la máquina la acertó).
---
#### 20.Z Verificación + cierre
- [ ] **20.Z.1** `just test-all` verde. Suma esperada: +2 pgTAP + 3 unit + 2 integración + 2 e2e = +9.
- [ ] **20.Z.2** Smoke manual: rotar las 3 pantallas top-level (`/lists`, `/tasks`, `/notes`) en cada idioma. Ninguna debe mostrar la key cruda (`__SKIPPED__` o el id).
- [ ] **20.Z.3** Bundle-size check: el chunk de `eu` es lazy-loaded por Paraglide; verificar que solo se carga cuando el user lo elige. No debe pesar más que `es.json`.
- [ ] **20.Z.4** Entrada en CHANGELOG: `[new] Euskera (Basque) locale (Fase 20). Machine-translated seed pending native review.`
- [ ] **20.Z.5** Documentar en `docs/history/fase-20-euskera-locale.md`.
---
#### 20.ZZ v1.0.0 release
- [ ] **20.ZZ.1** Bump `package.json` version → `1.0.0`.
- [ ] **20.ZZ.2** Editar `CHANGELOG.md`: mover entradas de `[Unreleased]` a `## [v1.0.0] — YYYY-MM-DD`. Incluir resumen de las 4 fases del ciclo (1720).
- [ ] **20.ZZ.3** `git tag -a v1.0.0 -m "v1.0.0 — versioning + shopping list flow + emoji avatars + euskera"`.
- [ ] **20.ZZ.4** Deploy desde el tag. Smoke en prod.
- [ ] **20.ZZ.5** Anuncio interno + nota sobre euskera review.
---
### Riesgos / notas
- **Calidad de la traducción automática**: euskera no es un idioma soportado de primera por DeepL (verificar fecha actual). GPT-4 class lo hace pero con errores. El sufijo `[needs review]` es explícito para no shipear texto incorrecto sin marcar.
- **ICU placeholders en euskera**: el orden de palabras (SOV) puede empujar `{name}` a posiciones no naturales en inglés/español. Revisar.
- **Selector ya soporta `system` (auto-detect)**: por la Fase 10.8 ya hay lógica de fallback. Asegurar que `eu` propaga por todo el árbol.
- **Test fixture users**: si los seeds (Ana, Borja, …) tienen `language='es'` hardcoded, ningún test rompe — solo los nuevos.
### Scope explícito fuera
- Galego, Català, Português, Francés. Solo Euskera en este release.
- Pluralización ICU avanzada (one/two/few/many/other) — euskera usa pluralización simple.
- Traducción de los strings de Keycloak (login screen). Ese es el operador del Keycloak externo.
- Localización del CHANGELOG. Solo en inglés.
- Localización de los nombres de los emojis (Fase 19). Aria-label sigue siendo el codepoint name CLDR en inglés.