feat(sidebar): headings that fold, a group head that is not indented, and colour where it is edited
Three things reported from use, all in the sidebar. Projekter, Delt med mig, Filtre and Etiketter fold, the way a project group already does. Kept on the account rather than in localStorage, and for the same reason the group's own `collapsed` is: folding a heading says "this is the part of my work I am not in right now", and that is as true on the laptop as on the desktop. The sidebar's *width* is the opposite case and stays per browser. A JSON array of keys rather than a column each, so a renamed heading is not a migration. The group heading is no longer indented — only what is filed under it. The colour dot sat as a sibling before the fold button and pushed the whole row in, and a heading that starts further in than the rows above it says the heading is inside something. And the colour is chosen while renaming. The dot was itself the button, which gave the heading four controls on a line narrow enough to crowd the name; both are edits to the same thing, so they belong in the same moment. The form closes on `focusout` and only when focus has left it entirely, and the swatches refuse focus on mousedown — Safari and Firefox do not focus a button on click, so `relatedTarget` would be null and the form would shut under the pointer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b169591209
commit
04a7ba1024
@@ -27,17 +27,62 @@ type userJSON struct {
|
||||
Locale string `json:"locale"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
// SidebarCollapsed is which sidebar headings this person has folded away. On
|
||||
// the account rather than in the browser, because folding one is a statement
|
||||
// about the work and not about the screen — the sidebar's width is the
|
||||
// opposite case and stays in localStorage.
|
||||
SidebarCollapsed []string `json:"sidebar_collapsed"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func toUserJSON(u *store.User) userJSON {
|
||||
collapsed := u.SidebarCollapsed
|
||||
// Never null in the response: a client that has to check for both an absent
|
||||
// array and an empty one will one day check for only one of them.
|
||||
if collapsed == nil {
|
||||
collapsed = []string{}
|
||||
}
|
||||
return userJSON{
|
||||
ID: u.ID, Email: u.Email, Name: u.Name, AvatarColor: u.AvatarColor,
|
||||
Timezone: u.Timezone, Locale: u.Locale, IsAdmin: u.IsAdmin,
|
||||
TOTPEnabled: u.TOTPEnabled, CreatedAt: u.CreatedAt.Format(time.RFC3339),
|
||||
TOTPEnabled: u.TOTPEnabled, SidebarCollapsed: collapsed,
|
||||
CreatedAt: u.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// handleSetSidebarSections records which sidebar headings are folded away.
|
||||
//
|
||||
// Its own route rather than a field on the profile: that one backs a form with a
|
||||
// save button and validates three fields together, and this writes on every click
|
||||
// of a chevron. Sharing the path would mean a fold sending a name and a timezone
|
||||
// it was never asked to change — and a validation error on one of those failing a
|
||||
// fold.
|
||||
//
|
||||
// The keys are the sidebar's and are not checked here. An unknown one costs
|
||||
// nothing: the sidebar folds the headings it recognises and ignores the rest, so
|
||||
// a heading that is renamed or removed leaves a harmless entry rather than a
|
||||
// migration.
|
||||
func (s *Server) handleSetSidebarSections(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Sections []string `json:"sections"`
|
||||
}
|
||||
if err := decodeJSON(w, r, &req); err != nil {
|
||||
return
|
||||
}
|
||||
// A cap, because this is a list the client decides the length of. Twenty is far
|
||||
// more headings than the sidebar has ever had.
|
||||
if len(req.Sections) > 20 {
|
||||
writeError(w, http.StatusBadRequest, CodeBadRequest, "too many sections")
|
||||
return
|
||||
}
|
||||
user := userFrom(r.Context())
|
||||
if err := s.db.SetSidebarCollapsed(r.Context(), user.ID, req.Sections); err != nil {
|
||||
s.storeError(w, r, "set sidebar sections", err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// --- sign in -----------------------------------------------------------------
|
||||
|
||||
type loginRequest struct {
|
||||
|
||||
Reference in New Issue
Block a user