feat: menus mobile corrigidos, sincronia multi-dispositivo e editor WYSIWYG

- Menus: dropdowns agora com position:fixed posicionado via JS — no celular
  o overflow-x:auto da menubar cortava os itens (bug relatado)
- Sincronia celular/desktop: PUT aceita base_hash opcional; divergência →
  409 {server_hash} e diálogo "Conflito de edição" (recarregar | salvar por
  cima); abas limpas recarregam sozinhas no focus/visibilitychange (debounce)
- WYSIWYG (Ctrl+M): contenteditable com render md→html no servidor e
  serialização html→md própria (títulos, negrito/itálico/tachado, listas
  com task, citação, código, blocos, links, tabelas, entidades); HTML edita
  preservando tags/atributos, sanitizando script/on* na entrada; toolbar
  usa execCommand no modo rico; colar é sempre texto puro; txt não aplica
- store/api: ErrConflict + teste de conflito na store e na API
- README: WYSIWYG, sincronia, base_hash, nota do fix de menu
This commit is contained in:
lgcosta committed 2026-11-04 13:03:58 -03:00
1 parent 8b355e8edb
commit 3b91c1a509
8 files changed
+456 -57

No files matched your search

+24 -9
View File
@@ -41,8 +41,12 @@ ocupa a tela inteira; menus e toolbar rolam horizontalmente com botões só de
- **Barra de ferramentas de formatação**: negrito, itálico, tachado, títulos H1/H2, listas,
citação, link, código inline e bloco de código — inserindo sintaxe Markdown **ou** HTML conforme
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á.
- **Modo Edição ↔ WYSIWYG ↔ Preview** (`Ctrl+E/M/R`): o WYSIWYG (`Ctrl+M`) renderiza a nota num
`contenteditable` e sincroniza com o fonte a cada tecla — Markdown vira HTML editável (negrito,
títulos, listas, citação, código, links, tabelas) e volta a Markdown ao editar/alternar; HTML é
editado visualmente com tags/atributos preservados (scripts e `on*` são removidos ao abrir).
O Preview renderiza Markdown no servidor (goldmark, GFM, HTML cru escapado); HTML em
`<iframe sandbox>`; texto puro como está. Em texto puro o WYSIWYG não se aplica.
- **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
@@ -78,18 +82,27 @@ ocupa a tela inteira; menus e toolbar rolam horizontalmente com botões só de
(autor, data, mensagem, hash SHA-256 e parent). O painel de histórico mostra o **diff unificado**
de cada revisão e permite **restaurar** qualquer versão anterior (a restauração vira uma nova
revisão — nada se perde). Conteúdo idêntico é deduplicado (content-addressed).
- **Sincronia entre dispositivos** (celular + desktop no mesmo servidor):
- Cada aba lembra o hash em que se baseia (`base_hash`). Ao salvar, se o servidor mudou desde
então, a API responde **409** e a UI abre o diálogo **Conflito de edição** com as duas saídas:
*Recarregar do servidor* (descarta o texto local) ou *Salvar por cima* (a versão remota fica
no histórico e é restaurável).
- Abas **sem edição pendente** são recarregadas automaticamente quando a janela volta ao foco
(`visibilitychange`/`focus`, com debounce), com aviso discreto.
- **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.
sem rótulos, **dropdowns dos menus posicionados como `fixed`** (o `overflow-x` da menubar não
os corta mais em telas pequenas), 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), `Alt+W`/`Alt+←/→`/`Alt+1..9`
(abas), barra de status com arquivo, formato, linha/coluna, contagem de palavras e estado de
salvamento.
atalhos `Ctrl+S`, `Ctrl+N`, `Ctrl+E/M/R` (edição/wysiwyg/preview), `Ctrl+H` (histórico), `F9`
(barra 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
@@ -160,8 +173,10 @@ curl -H "Authorization: Bearer $T" $B/api/notes/<id> # nota + conteú
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","categories":["<id>"],"content":"editado","author":"lg","message":"revisão 2"}' \
-d '{"title":"Olá","format":"md","categories":["<id>"],"content":"editado","author":"lg","message":"revisão 2","base_hash":"<hash carregado>"}' \
$B/api/notes/<id>
# base_hash é opcional: se enviado e diferente do atual → 409 {error, server_hash, updated_at}
# (omitir/enviar vazio = força o salvamento)
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 \