feat: layout mobile, abas múltiplas e categorias em árvore

- Responsivo ≤760px: sidebar vira gaveta com backdrop, painel de versões em tela
  cheia, toolbar/menu rolam sem rótulos, alvos de toque, dvh + safe-area, fonte
  do editor ≥16px (anti-zoom iOS)
- Abas: várias notas abertas com estado sujo por aba, ✕ com confirmação,
  clique do meio, Alt+W / Alt+←/→ / Alt+1..9; abas e rascunhos sobrevivem à
  sessão via localStorage
- Categorias: modelo vira árvore (categories.json) com notas em múltiplas
  categorias; explorador no sidebar com expandir/renomear/mover/excluir
  (sem ciclos, GC de vínculos) e seletor multi-marca no editor; filtro por
  pasta recursivo por padrão; migração automática do campo único legado
- API: POST/PUT/DELETE /api/categories; notas aceitam categories: [ids]
- Testes: store (árvore, ciclo, migração, GC) e api (CRUD de categorias)
This commit is contained in:
lgcosta committed 2026-11-04 11:54:15 -03:00
1 parent 9bc5de4c56
commit 7e89207b70
10 files changed
+1737 -489

No files matched your search

+35 -11
View File
@@ -36,9 +36,20 @@ restauração) e autenticação por **token** (Bearer para API / cookie HttpOnly
o formato ativo (desabilitada em texto puro).
- **Modo Edição ↔ Preview**: Markdown é renderizado no servidor (goldmark, GFM, HTML cru escapado);
HTML é pré-visualizado em `<iframe sandbox>`; texto puro é exibido como está.
- **Categorias/pastas** no painel lateral e **tags `#tag`** extraídas automaticamente do conteúdo
(cabeçalhos Markdown como `# Título` não contam — só `#tag` sem espaço). Filtro por categoria,
por tag e busca em título/categoria/conteúdo.
- **Categorias como árvore de pastas**: o painel lateral traz um explorador de arquivos —
categorias aninhadas (subpastas), expansão/colapso, contagem de notas por pasta e menu de
contexto (Nova subpasta / Renomear / Excluir, sem ciclos). Uma nota pode pertencer a
**várias categorias** ao mesmo tempo (seletor multi-marca no topo do editor). Filtro por
pasta é recursivo por padrão (`?recursive=0` desliga); há também "Todas as notas" e
"Sem categoria". Dados antigos com o campo único `category` são migrados automaticamente
na abertura (vira categoria raiz de mesmo nome).
- **Tags `#tag`** extraídas automaticamente do conteúdo (cabeçalhos Markdown como `# Título`
não contam — só `#tag` sem espaço), com filtro por nuvem de tags e busca em
título/categorias/conteúdo.
- **Abas de arquivos**: abra várias notas ao mesmo tempo (clique na lista, `Ctrl+N` ou botão
✚); cada aba tem estado sujo próprio (●), botão ✕ com confirmação, fechar com clique do
meio, trocar com `Alt+←/→` ou `Alt+1..9`, fechar com `Alt+W`. As abas abertas (inclusive
rascunhos não salvos) são restauradas entre sessões via `localStorage`.
- **Preferências do editor** (menu Editar → Preferências… ou `Ctrl+,`, em diálogo próprio):
- **Números de linha** (gutter alinhado mesmo com quebra de linha; alternável pelo menu Exibir).
- **Marcador de coluna** customizável — padrão **80** (linha vertical no editor, exibida com
@@ -60,10 +71,15 @@ restauração) e autenticação por **token** (Bearer para API / cookie HttpOnly
- **Autenticação por token**: sem token, a interface exibe um diálogo pedindo o token; ele é
guardado em **cookie HttpOnly/SameSite=Lax** (30 dias). A API também aceita
`Authorization: Bearer <token>` para scripts.
- **Responsivo para celular** (≤760px): barra lateral vira gaveta com backdrop (☰ fecha ao
tocar uma nota), painel de versões ocupa a tela toda, toolbar e menus rolam horizontalmente
sem rótulos, alvos de toque ampliados, diálogos em largura total, fonte do editor com mínimo
de 16px (evita zoom automático do iOS), altura em `dvh` e `safe-area-inset` para notch.
- **Estilo Mousepad**: tema claro Greybird-like, menus nativos (Arquivo/Editar/Exibir/Ajuda),
atalhos `Ctrl+S`, `Ctrl+N`, `Ctrl+E/R` (editar/preview), `Ctrl+H` (histórico), `F9` (barra
lateral), `Ctrl+B/I/K` (formatação), `Ctrl+,` (preferências), barra de status com arquivo,
formato, linha/coluna, contagem de palavras e estado de salvamento.
lateral), `Ctrl+B/I/K` (formatação), `Ctrl+,` (preferências), `Alt+W`/`Alt+←/→`/`Alt+1..9`
(abas), barra de status com arquivo, formato, linha/coluna, contagem de palavras e estado de
salvamento.
## Estrutura do projeto
@@ -121,19 +137,25 @@ Todos os endpoints sob `/api/*` exigem o token, exceto `login/logout/me` e o fro
```bash
T=$NOTES_TOKEN; B=http://localhost:8080
curl -H "Authorization: Bearer $T" $B/api/notes # lista (filtros: ?category=&tag=&q=)
curl -H "Authorization: Bearer $T" $B/api/notes # lista (?category=<id|none>&recursive=0&tag=&q=)
curl -H "Authorization: Bearer $T" $B/api/notes/<id> # nota + conteúdo
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","category":"dev","content":"# Oi #linux"}' $B/api/notes
curl -s -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","categories":["<id>"],"content":"# Oi #linux"}' $B/api/notes
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","category":"dev","content":"editado","author":"lg","message":"revisão 2"}' \
-d '{"title":"Olá","format":"md","categories":["<id>"],"content":"editado","author":"lg","message":"revisão 2"}' \
$B/api/notes/<id>
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions # histórico
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions/<hash>/diff # diff unificado
curl -X POST -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions/<hash>/restore \
-H 'Content-Type: application/json' -d '{"author":"lg"}' # restaurar
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/raw # download .txt/.md/.html
curl -H "Authorization: Bearer $T" $B/api/tags $B/api/categories
curl -H "Authorization: Bearer $T" $B/api/tags $B/api/categories # agregações
# árvore de categorias (lista plana; parent="" é raiz)
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"name":"devops","parent":""}' $B/api/categories
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"name":"infra","parent":"<novo-pai>"}' $B/api/categories/<id> # renomear/mover
curl -X DELETE -H "Authorization: Bearer $T" $B/api/categories/<id> # remove subtree
```
## Como funciona o versionamento
@@ -142,7 +164,9 @@ curl -H "Authorization: Bearer $T" $B/api/tags $B/api/categories
Git): conteúdo repetido ocupa um único blob.
- Cada nota tem `data/notes/<id>/meta.json` (estado atual) e `history.json` (lista ordenada de
revisões: `hash`, `parent`, `author`, `when`, `message`, `bytes`).
- Salvar sem alterar o conteúdo atualiza apenas os metadados (título/categoria/formato) — sem
- A árvore de categorias fica em `data/categories.json` (lista plana com `parent`; nomes únicos
entre irmãos; mover para dentro de si mesma é recusado).
- Salvar sem alterar o conteúdo atualiza apenas os metadados (título/categorias/formato) — sem
revisão nova. Restaurar cria uma revisão nova apontando para o blob antigo.
- Apagar uma nota faz GC dos blobs que ficaram sem referência.