Volver a la wiki

Cuaderno · bloc de notas markdown personal (PR3 plan workspace tools s52)

Qué es

Cuaderno es la tercera tool del plan workspace tools s52 (PR3 de 4). Bloc de notas markdown personal — cada miembro del staff (Edu, Dani, Txell) ve solo las suyas. Vive en /tools/notebook.

Sustituye apuntes sueltos en gist/markdown local + Notion/Obsidian externos por un editor WYSIWYG integrado en el workspace, con auto-save, búsqueda, anclar/archivar y export a MD/HTML.

Editor

Stack TipTap 3.22 con extensiones:

Slash menu ’/’

Escribir / en una línea vacía abre el menú con 10 comandos agrupados:

Bloques: Heading 1/2/3, Lista con viñetas, Lista numerada, Lista de tareas, Bloque de código, Cita, Tabla 3×3, Línea horizontal.

Navegación con ↑/↓, Enter selecciona, Escape cierra. Filtra escribiendo (/head filtra a Heading 1/2/3).

Implementación: slashCommands.ts exporta createSlashExtension(updater) que retorna una Extension TipTap. Internamente usa @tiptap/suggestion. El estado del menú (open, query, position, selectedIndex, filtered) lo mantiene el componente React vía el updater callback. El render del dropdown lo hace el componente, no la extension — más control sobre estilos y aria.

Auto-save

Debounce 1s separado para título y contenido (timers distintos en useRef).

Solo title, pinned y archived disparan log de actividad. Word count se calcula en cliente: text.trim().split(/\s+/).length.

Indicador visual en toolbar: “Guardando…” / “Auto-guardado ●” (verde).

Storage

Content guardado como JSON nativo de TipTap (editor.getJSON() → JSON.stringify → columna content TEXT).

Razón: lossless, round-trip exacto. El export a markdown/HTML se hace en runtime con tiptap-markdown + editor.getHTML(). Trade-off aceptado: si en futuro se quiere migrar de TipTap, hay que serializar a markdown todo el corpus en una migration única.

Schema D1 (migration 0022)

CREATE TABLE notebook_notes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  owner_id TEXT NOT NULL,        -- email CF Access
  title TEXT NOT NULL DEFAULT 'Sin título',
  content TEXT NOT NULL DEFAULT '{}',  -- JSON TipTap
  word_count INTEGER NOT NULL DEFAULT 0,
  pinned INTEGER NOT NULL DEFAULT 0,
  archived_at TEXT,              -- soft-delete (sigue accesible en sidebar)
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_notebook_owner ON notebook_notes(owner_id, archived_at, pinned DESC, updated_at DESC);
CREATE INDEX idx_notebook_archived ON notebook_notes(archived_at);

Index compuesto sirve la query principal (lista del sidebar) sin filesort.

Scope user enforced en endpoints

CRÍTICO: el filtro WHERE owner_id = ? está en todos los endpoints (GET list, GET single, PUT, DELETE). Sin él, las notas de Dani aparecerían a Edu.

actorFrom(request) lee cf-access-authenticated-user-email. Si CF Access falla y el header no llega (caso web fallback), el owner sería ‘web’ literal — sus notas no se mezclarían con las del staff pero tampoco serían recuperables. CF Access es load-bearing para correctness aquí.

Tres secciones colapsables:

  1. Anclados (pinned=1, no archived) — siempre arriba si hay alguno.
  2. Notas (pinned=0, no archived) — sección principal.
  3. Archivadas (archived_at != NULL) — colapsada por defecto.

Búsqueda libre filtra por title. Click en una fila → carga GET /api/notebook/<id> y abre editor.

Decisión: archivadas no se ocultan completamente. Siguen visibles en su sección colapsable. Filosofía: archivar ≠ borrar. Para borrar realmente hay un botón “Eliminar nota” rojo abajo del editor (DELETE hard).

Export

Modal con dos tabs:

Acciones: Copiar al portapapeles · Descargar (<note-title>.md o .html).

No PDF: la lib pdfkit/jspdf añadiría 200+ KB y no convence el render. Decisión Edu: descargar HTML y usar la impresora PDF del sistema (Ctrl+P).

Decisiones de diseño

Footguns documentados

Próximos pasos opcionales

No bloqueantes, sólo si emerge necesidad:

Véase también

Subir