feat(v0.6): auto-group Projects by namespace + Active-now band + cross-device prefs

The flat Projects grid was overwhelming at 50-100 repos. Explored the UX with
4 parallel agents (auto-grouping / manual folders / density-segmentation /
competitive research); all converged on namespace auto-grouping as the primary
fix — the dotted repo names (Billo.Platform.*, Billo.Infrastructure.*) already
encode the hierarchy, so it's a string-split, not a taxonomy the user builds.

Frontend:
- groupProjects(): depth-2 namespace grouping, >=2-member threshold (singletons
  fall to "Other"), degrade-to-flat when repos share no prefix, running projects
  duplicated into a pinned "Active now" section.
- displayLabel(): cards show the namespace tail (Payment, not
  Billo.Platform.Payment); full name in Active/Other.
- Collapsible sticky group headers with an "N active" badge so a collapsed
  section never hides running work; search force-expands matching groups.
- public/prefs.ts: server-first load with a localStorage offline mirror, migrates
  the legacy per-device proj-favs.

Backend (server-side prefs so favourites + collapse-state follow the user across
devices — localStorage was per-device, which broke the multi-device premise):
- src/http/prefs-store.ts: ~/.web-terminal-prefs.json store (mirrors push-store),
  every field bounded + sanitized.
- GET /prefs (read-only) + PUT /prefs (Origin-guarded); UiPrefs type +
  PREFS_STORE_PATH config.

Tests: +12 prefs-store, +21 groupProjects/displayLabel. Full suite 1227 passed.
Verified live: grouping renders, collapse persists to the server and survives a
full page reload (cross-device sync proven end-to-end).
This commit is contained in:
Yaojia Wang
2026-07-01 04:25:57 +02:00
parent 7b3ba8491a
commit 03612323c0
10 changed files with 581 additions and 1 deletions

View File

@@ -974,13 +974,84 @@ body {
box-shadow: 0 0 0 2px var(--accent-soft);
}
/* Grid — mirrors .mg-grid layout */
/* Outer container is now a vertical stack of collapsible namespace sections. */
.proj-grid {
display: flex;
flex-direction: column;
gap: 22px;
}
/* The actual card grid — one per group (and the flat fallback). Mirrors .mg-grid. */
.proj-grid-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px;
}
/* ── Namespace group section ─────────────────────────────────────────────── */
.proj-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.proj-group-head {
display: flex;
align-items: center;
gap: 9px;
padding: 4px 2px;
position: sticky; /* keep the namespace visible while scrolling a long group */
top: 0;
z-index: 1;
background: var(--bg);
user-select: none;
}
.proj-group-head[role='button'] {
cursor: pointer;
border-radius: 7px;
}
.proj-group-head[role='button']:hover {
background: var(--surface-1);
}
.proj-group-head[role='button']:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.proj-group-caret {
color: var(--text-faint);
font-size: 11px;
width: 12px;
flex: none;
text-align: center;
}
.proj-group-label {
color: var(--text);
font-weight: 600;
font-size: 13px;
letter-spacing: 0.01em;
}
.proj-group-count {
color: var(--text-faint);
font-size: 12px;
font-family: Menlo, Consolas, monospace;
}
/* "N active" badge on a namespace/other header — never lose running work behind a caret. */
.proj-group-active {
color: var(--green);
font-size: 11px;
margin-left: 2px;
}
/* "Active now" header — pinned, accent-tinted, non-collapsible. */
.proj-group-head.proj-group-active .proj-group-caret {
color: var(--green);
font-size: 9px;
}
.proj-group-head.proj-group-active .proj-group-label {
color: var(--accent);
}
.proj-group.collapsed {
gap: 0;
}
/* Project card */
.proj-card {
display: flex;