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

+31 -15
View File
@@ -373,27 +373,27 @@ body.no-gutter #gutter { display: none; }
}
#preview-frame { flex: 1 1 auto; width: 100%; border: 0; background: #fff; }
/* preview markdown --------------------------------------------------- */
#preview.md { white-space: normal; line-height: 1.55; }
#preview.md h1, #preview.md h2, #preview.md h3 { margin: 18px 0 8px; line-height: 1.25; }
#preview.md h1 { font-size: 1.7em; border-bottom: 1px solid var(--border-soft); padding-bottom: 4px; }
#preview.md h2 { font-size: 1.35em; }
#preview.md p { margin: 8px 0; }
#preview.md ul, #preview.md ol { margin: 8px 0; padding-left: 26px; }
#preview.md blockquote {
/* preview markdown + wysiwyg ------------------------------------------ */
#preview.md, #wysiwyg { white-space: normal; line-height: 1.55; }
#preview.md h1, #wysiwyg h1, #preview.md h2, #wysiwyg h2, #preview.md h3, #wysiwyg h3 { margin: 18px 0 8px; line-height: 1.25; }
#preview.md h1, #wysiwyg h1 { font-size: 1.7em; border-bottom: 1px solid var(--border-soft); padding-bottom: 4px; }
#preview.md h2, #wysiwyg h2 { font-size: 1.35em; }
#preview.md p, #wysiwyg p { margin: 8px 0; }
#preview.md ul, #wysiwyg ul, #preview.md ol, #wysiwyg ol { margin: 8px 0; padding-left: 26px; }
#preview.md blockquote, #wysiwyg blockquote {
margin: 10px 0; padding: 4px 14px; color: var(--muted);
border-left: 3px solid var(--accent); background: var(--panel-2);
}
#preview.md pre {
#preview.md pre, #wysiwyg pre {
background: var(--panel-2); border: 1px solid var(--border-soft);
border-radius: 4px; padding: 10px 12px; overflow-x: auto;
}
#preview.md code { background: var(--panel-2); padding: 1px 4px; border-radius: 3px; }
#preview.md pre code { background: none; padding: 0; }
#preview.md table { border-collapse: collapse; margin: 10px 0; }
#preview.md th, #preview.md td { border: 1px solid var(--border); padding: 4px 10px; }
#preview.md a { color: var(--accent-dark); }
#preview.md hr { border: 0; border-top: 1px solid var(--border); margin: 16px 0; }
#preview.md code, #wysiwyg code { background: var(--panel-2); padding: 1px 4px; border-radius: 3px; }
#preview.md pre code, #wysiwyg pre code { background: none; padding: 0; }
#preview.md table, #wysiwyg table { border-collapse: collapse; margin: 10px 0; }
#preview.md th, #wysiwyg th, #preview.md td, #wysiwyg td { border: 1px solid var(--border); padding: 4px 10px; }
#preview.md a, #wysiwyg a { color: var(--accent-dark); }
#preview.md hr, #wysiwyg hr { border: 0; border-top: 1px solid var(--border); margin: 16px 0; }
/* painel de versões --------------------------------------------------- */
#versions-panel {
@@ -903,3 +903,19 @@ html[data-theme="solar-escuro"] #revisions-list li:hover { background: #0c4450;
.dialog input { font-size: 16px; }
.tree-menu { visibility: visible; } /* toque não tem hover */
}
/* ------------------------------------------------- WYSIWYG (contenteditable) */
#wysiwyg {
flex: 1 1 auto;
overflow-y: auto;
padding: 16px 22px;
background: var(--panel);
color: var(--fg);
outline: none;
min-height: 0;
caret-color: var(--accent-dark);
font-family: var(--sans);
font-size: 15px;
}
#wysiwyg:empty::before { content: 'Comece a escrever…'; color: var(--muted); }
#wysiwyg [contenteditable="true"]:focus { outline: none; }