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

+18 -16
View File
@@ -22,20 +22,20 @@ func (s *Server) serveIndex(w http.ResponseWriter, r *http.Request) {
// noteInput é o corpo aceito por POST/PUT de notas.
type noteInput struct {
Title string `json:"title"`
Format string `json:"format"`
Category string `json:"category"`
Content string `json:"content"`
Author string `json:"author"`
Message string `json:"message"`
Title string `json:"title"`
Format string `json:"format"`
Categories []string `json:"categories"` // IDs da árvore de categorias
Content string `json:"content"`
Author string `json:"author"`
Message string `json:"message"`
}
func (in noteInput) fields() (string, string, string, string, string, string) {
func (in noteInput) fields() (string, string, []string, string, string, string) {
f := in.Format
if f == "" {
f = store.FormatTxt
}
return in.Title, f, in.Category, in.Content, in.Author, in.Message
return in.Title, f, in.Categories, in.Content, in.Author, in.Message
}
// noteView é a representação completa (meta + conteúdo) de uma nota.
@@ -46,10 +46,12 @@ type noteView struct {
func (s *Server) handleListNotes(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
recursive := q.Get("recursive") != "0"
notes, err := s.st.List(store.Filter{
Category: q.Get("category"),
Tag: q.Get("tag"),
Query: q.Get("q"),
Category: q.Get("category"),
Recursive: recursive,
Tag: q.Get("tag"),
Query: q.Get("q"),
})
if err != nil {
mapStoreError(w, err)
@@ -67,8 +69,8 @@ func (s *Server) handleCreateNote(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "corpo JSON inválido")
return
}
title, format, category, content, author, message := in.fields()
n, err := s.st.Create(title, format, category, content, author, message)
title, format, categories, content, author, message := in.fields()
n, err := s.st.Create(title, format, categories, content, author, message)
if err != nil {
mapStoreError(w, err)
return
@@ -91,8 +93,8 @@ func (s *Server) handleUpdateNote(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "corpo JSON inválido")
return
}
title, format, category, content, author, message := in.fields()
n, err := s.st.Update(r.PathValue("id"), title, format, category, content, author, message)
title, format, categories, content, author, message := in.fields()
n, err := s.st.Update(r.PathValue("id"), title, format, categories, content, author, message)
if err != nil {
mapStoreError(w, err)
return
@@ -159,7 +161,7 @@ func (s *Server) handleCategories(w http.ResponseWriter, r *http.Request) {
return
}
if cats == nil {
cats = []store.CatCount{}
cats = []store.CategoryView{}
}
writeJSON(w, http.StatusOK, cats)
}