fix(ui): tag counts, j/k nav, stream layout, search alignment

- Tag rail counts now reflect cards-only when in cards view
  (ListTags accepts cardsOnly filter, JS passes it per view)
- j/k navigation scoped to visible (intent/search filtered) list
- scrollSelectedIntoView works in both stream and cards view
- Entity items wrap title/desc/preview in .entity-content flex
  container so tags/pills align right consistently
- Title no longer eaten by description/body (flex-shrink + min-width)
- Search bar centered in header with margin auto
- switchView awaits loadEntities+loadTags to fix stale intent counts
This commit is contained in:
2026-05-16 17:51:04 -04:00
parent ab07f631a7
commit 8bfa9b15ed
5 changed files with 92 additions and 18 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ type TagResponse struct {
func listTags(store *db.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tags, err := store.ListTags()
cardsOnly := r.URL.Query().Get("cards_only") == "true"
tags, err := store.ListTags(cardsOnly)
if err != nil {
writeInternalError(w, err)
return
+6 -2
View File
@@ -5,12 +5,16 @@ type TagCount struct {
Count int
}
func (s *Store) ListTags() ([]TagCount, error) {
func (s *Store) ListTags(cardsOnly bool) ([]TagCount, error) {
where := "WHERE e.deleted_at IS NULL"
if cardsOnly {
where += " AND e.card_type IS NOT NULL"
}
rows, err := s.db.Query(`
SELECT t.tag, COUNT(*) as cnt
FROM entity_tags t
JOIN entities e ON t.entity_id = e.id
WHERE e.deleted_at IS NULL
` + where + `
GROUP BY t.tag
ORDER BY t.tag`)
if err != nil {
+37 -3
View File
@@ -4,7 +4,7 @@ import "testing"
func TestListTags_Empty(t *testing.T) {
s := testStore(t)
tags, err := s.ListTags()
tags, err := s.ListTags(false)
if err != nil {
t.Fatal(err)
}
@@ -19,7 +19,7 @@ func TestListTags_Counts(t *testing.T) {
s.Create(&Entity{Body: "b", Glyph: GlyphNote, Tags: []string{"ops"}})
s.Create(&Entity{Body: "c", Glyph: GlyphNote, Tags: []string{"home"}})
tags, err := s.ListTags()
tags, err := s.ListTags(false)
if err != nil {
t.Fatal(err)
}
@@ -50,7 +50,7 @@ func TestListTags_ExcludesDeleted(t *testing.T) {
s.Create(&Entity{Body: "alive", Glyph: GlyphNote, Tags: []string{"here"}})
tags, err := s.ListTags()
tags, err := s.ListTags(false)
if err != nil {
t.Fatal(err)
}
@@ -61,3 +61,37 @@ func TestListTags_ExcludesDeleted(t *testing.T) {
t.Errorf("expected 'here', got %q", tags[0].Tag)
}
}
func TestListTags_CardsOnly(t *testing.T) {
s := testStore(t)
s.Create(&Entity{Body: "fluid", Glyph: GlyphNote, Tags: []string{"ops", "shared"}})
ct := CardSnippet
s.Create(&Entity{Body: "card", Glyph: GlyphNote, Tags: []string{"ops", "code"}, CardType: &ct})
all, err := s.ListTags(false)
if err != nil {
t.Fatal(err)
}
if len(all) != 3 {
t.Fatalf("all tags: expected 3, got %d", len(all))
}
cards, err := s.ListTags(true)
if err != nil {
t.Fatal(err)
}
if len(cards) != 2 {
t.Fatalf("card tags: expected 2, got %d", len(cards))
}
counts := map[string]int{}
for _, tc := range cards {
counts[tc.Tag] = tc.Count
}
if counts["ops"] != 1 {
t.Errorf("ops count: expected 1 (card only), got %d", counts["ops"])
}
if counts["code"] != 1 {
t.Errorf("code count: expected 1, got %d", counts["code"])
}
}