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:
1 parent
9bc5de4c56
commit
7e89207b70
10 files changed
+1737
-489
No files matched your search
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user