Per m's v1 picks (2026-05-29):
- Q6 (icon picker): yes, with curated keys + SVG registry.
- Q8 (show_count badge): yes, opt-in checkbox + sidebar badge.
Icon registry (web/icons.go):
- 7 curated keys: folder (default), clock, star, tag, inbox, box,
file-text. Each maps to a Feather-style 24x24 SVG matching the rest
of the projax sidebar aesthetic. Returns template.HTML so layout.tmpl
emits markup verbatim. Unknown / nil keys fall back to folder.
- RenderViewIcon(*string) is template-callable; IconRegistryKeys()
feeds the editor's <select>.
- Funcs map in web/server.go gains a "renderIcon" entry.
show_count badge (web/server.go + web/templates/layout.tmpl):
- render() now computes per-saved-view counts when ANY view in the
list has ShowCount=true. One ListAll per render, shared across all
show-count views; for each opted-in view the persisted filter_json
is decoded into a TreeFilter and matched against every item.
- Counts pass to the template as UserViewCounts (slug → count). The
template renders {{index $counts $slug}} inside a nav-badge span
next to the view's name.
Template updates:
- layout.tmpl: replaces the diamond-glyph placeholder with
{{renderIcon .Icon}}; show_count views emit a .nav-badge next to
their name.
- view_editor.tmpl: icon <select> now sourced from IconKeys data
(the editor handler passes IconRegistryKeys()).
CSS additions:
- nav-badge: muted-color, surface-background, pill-shaped, pushed to
the right via margin-left:auto so the badge aligns with the row's
end regardless of name length.
- nav-item-user-view.active .nav-badge: switches to accent border +
color so the active row's badge stays legible.
Tests:
- TestSidebarShowCountBadge — seeds show_count=true view, asserts
.nav-badge markup in the sidebar.
- TestSidebarIconRenders — seeds icon=star view, asserts the
distinctive star polygon path lands in the sidebar SVG.
Drag-reorder UI stays parked (m's Q7=(b) v2). sort_order column is
server-assigned MAX+1 on create; the column was wired in slice A and
ReorderViews is ready for slice G's followup.
44 lines
3.6 KiB
Go
44 lines
3.6 KiB
Go
package web
|
|
|
|
import "html/template"
|
|
|
|
// Phase 5j slice G — icon registry per m's Q6 pick (2026-05-29). The
|
|
// curated set of keys mirrors the editor's <select> options so the round-
|
|
// trip works: editor save persists the key string, layout renders the SVG
|
|
// at look-up time. Unknown / empty keys fall back to the default folder
|
|
// glyph.
|
|
//
|
|
// Stored as html/template.HTML so layout.tmpl can emit the markup
|
|
// directly without html-escaping the angle brackets. Each SVG is sized
|
|
// to 18px square and inherits currentColor like the existing nav-icon
|
|
// glyphs.
|
|
|
|
var iconRegistry = map[string]template.HTML{
|
|
"folder": template.HTML(`<svg class="nav-icon user-view-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`),
|
|
"clock": template.HTML(`<svg class="nav-icon user-view-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`),
|
|
"star": template.HTML(`<svg class="nav-icon user-view-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`),
|
|
"tag": template.HTML(`<svg class="nav-icon user-view-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.59 13.41 13.42 20.58a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>`),
|
|
"inbox": template.HTML(`<svg class="nav-icon user-view-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>`),
|
|
"box": template.HTML(`<svg class="nav-icon user-view-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`),
|
|
"file-text": template.HTML(`<svg class="nav-icon user-view-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>`),
|
|
}
|
|
|
|
// RenderViewIcon returns the SVG for an icon key, falling back to the
|
|
// folder default for nil or unknown keys. Template-callable so
|
|
// layout.tmpl can emit `{{renderIcon .Icon}}`.
|
|
func RenderViewIcon(icon *string) template.HTML {
|
|
key := "folder"
|
|
if icon != nil && *icon != "" {
|
|
if _, ok := iconRegistry[*icon]; ok {
|
|
key = *icon
|
|
}
|
|
}
|
|
return iconRegistry[key]
|
|
}
|
|
|
|
// IconRegistryKeys returns the available icon keys in display order, for
|
|
// the editor's <select>. The first key (folder) is the default.
|
|
func IconRegistryKeys() []string {
|
|
return []string{"folder", "clock", "star", "tag", "inbox", "box", "file-text"}
|
|
}
|