Simplify the sidebar into a led primary group + demoted rest
The flat 13-item list read as clutter and reused icons (Team/Meetings, Work/Sprints, Inbox/Approvals all shared a glyph). Now: - Primary group leads with the core loop: Home, Work, Team, Messages, Runs, Files, Approvals. - A divider demotes the occasional surfaces: Inbox, Documents, Knowledge, Meetings, Sprints, Social — still one click away, nothing orphaned (search can't route to several of them). - Distinct calendar/target/inbox/folder icons end the collisions. - Every nav glyph is the 18px icon bound in a 24px box; the collapsed rail centers icons instead of hiding them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
02c9da6fe9
commit
ea9f27916e
4 files changed
+59
-22
No files matched your search
@@ -180,9 +180,11 @@
|
||||
@media (max-width: 980px) {
|
||||
.sidebar { width: 64px; }
|
||||
.sidebar .brand-name,
|
||||
.sidebar .nav-item span:not(.nav-badge),
|
||||
.sidebar .nav-item span:not(.nav-badge):not(.icon-box),
|
||||
.sidebar-foot-name,
|
||||
.sidebar .search-bar > span:not(.search-kbd) { display: none; }
|
||||
.sidebar .nav-item { justify-content: center; }
|
||||
.sidebar .nav-badge { display: none; }
|
||||
.sidebar .search-bar { justify-content: center; padding: var(--sp-2); }
|
||||
.sidebar .search-kbd { display: none; }
|
||||
.content { padding: var(--sp-5) var(--sp-4) var(--sp-8); }
|
||||
|
||||
@@ -41,6 +41,8 @@ const PATHS: Record<string, ReactNode> = {
|
||||
refresh: <><path d="M21 12a9 9 0 1 1-3-6.7" /><path d="M21 3v6h-6" /></>,
|
||||
pause: <><rect x="6" y="5" width="4" height="14" rx="1" /><rect x="14" y="5" width="4" height="14" rx="1" /></>,
|
||||
shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />,
|
||||
calendar: <><rect x="3" y="4.5" width="18" height="16" rx="2" /><path d="M3 9h18M8 2.5v4M16 2.5v4" /></>,
|
||||
target: <><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="3.6" /></>,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof PATHS | (string & {});
|
||||
|
||||
@@ -32,22 +32,29 @@ interface NavSpec {
|
||||
* internal scroll regions, so the content area must not scroll as one block. */
|
||||
const FLUSH_ROUTES = new Set<RouteId>(["chat", "settings", "files", "social", "runs"]);
|
||||
|
||||
/** The daily-driver surfaces — the core operating loop. */
|
||||
const primaryNav: NavSpec[] = [
|
||||
{ id: "home", label: "Home", icon: "home" },
|
||||
{ id: "inbox", label: "Inbox", icon: "approve" },
|
||||
{ id: "work", label: "Work", icon: "work" },
|
||||
{ id: "approvals", label: "Approvals", icon: "approve" },
|
||||
{ id: "team", label: "Team", icon: "team" },
|
||||
{ id: "chat", label: "Messages", icon: "chat" },
|
||||
{ id: "files", label: "Files", icon: "docs" },
|
||||
{ id: "runs", label: "Runs", icon: "send" },
|
||||
{ id: "files", label: "Files", icon: "folder" },
|
||||
{ id: "approvals", label: "Approvals", icon: "approve" },
|
||||
];
|
||||
|
||||
/** Less-frequent surfaces, demoted under a divider so the primary set leads. */
|
||||
const secondaryNav: NavSpec[] = [
|
||||
{ id: "inbox", label: "Inbox", icon: "inbox" },
|
||||
{ id: "docs", label: "Documents", icon: "file" },
|
||||
{ id: "knowledge", label: "Knowledge", icon: "sparkle" },
|
||||
{ id: "runs", label: "Runs", icon: "send" },
|
||||
{ id: "meetings", label: "Meetings", icon: "team" },
|
||||
{ id: "sprints", label: "Sprints", icon: "work" },
|
||||
{ id: "meetings", label: "Meetings", icon: "calendar" },
|
||||
{ id: "sprints", label: "Sprints", icon: "target" },
|
||||
{ id: "social", label: "Social", icon: "social" },
|
||||
];
|
||||
|
||||
const allNav: NavSpec[] = [...primaryNav, ...secondaryNav];
|
||||
|
||||
export function Shell({
|
||||
snapshot,
|
||||
route,
|
||||
@@ -104,17 +111,13 @@ export function Shell({
|
||||
</button>
|
||||
<nav className="nav">
|
||||
{primaryNav.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
aria-label={item.label}
|
||||
className={`nav-item ${route === item.id ? "active" : ""}`.trim()}
|
||||
onClick={() => setRoute(item.id)}
|
||||
>
|
||||
<Icon name={item.icon} size={15} />
|
||||
<span>{item.label}</span>
|
||||
{renderBadge(badges[item.id])}
|
||||
</button>
|
||||
<NavButton key={item.id} item={item} active={route === item.id} badge={badges[item.id]} onSelect={setRoute} />
|
||||
))}
|
||||
</nav>
|
||||
<div className="nav-divider" />
|
||||
<nav className="nav">
|
||||
{secondaryNav.map((item) => (
|
||||
<NavButton key={item.id} item={item} active={route === item.id} badge={badges[item.id]} onSelect={setRoute} />
|
||||
))}
|
||||
</nav>
|
||||
<div className="sidebar-spacer" />
|
||||
@@ -122,10 +125,11 @@ export function Shell({
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Settings"
|
||||
aria-current={route === "settings" ? "page" : undefined}
|
||||
className={`nav-item ${route === "settings" ? "active" : ""}`.trim()}
|
||||
onClick={() => setRoute("settings")}
|
||||
>
|
||||
<Icon name="settings" size={15} />
|
||||
<span className="icon-box"><Icon name="settings" size={18} /></span>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -167,8 +171,34 @@ export function Shell({
|
||||
}
|
||||
|
||||
|
||||
function NavButton({
|
||||
item,
|
||||
active,
|
||||
badge,
|
||||
onSelect,
|
||||
}: {
|
||||
item: NavSpec;
|
||||
active: boolean;
|
||||
badge: number | undefined;
|
||||
onSelect: (route: RouteId) => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={item.label}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`nav-item ${active ? "active" : ""}`.trim()}
|
||||
onClick={() => onSelect(item.id)}
|
||||
>
|
||||
<span className="icon-box"><Icon name={item.icon} size={18} /></span>
|
||||
<span>{item.label}</span>
|
||||
{renderBadge(badge)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function labelFor(route: RouteId): string {
|
||||
const spec = primaryNav.find((item) => item.id === route);
|
||||
const spec = allNav.find((item) => item.id === route);
|
||||
if (spec) return spec.label;
|
||||
if (route === "settings") return "Settings";
|
||||
throw new Error(`Unknown route: ${route}`);
|
||||
|
||||
@@ -195,14 +195,17 @@ a { color: inherit; }
|
||||
.nav-item:active { transform: scale(0.985); }
|
||||
.nav-item.active { background: var(--panel-3); color: var(--text-1); font-weight: 560; }
|
||||
.nav-item.active svg { color: var(--text-1); }
|
||||
.nav-item svg { color: var(--text-3); flex-shrink: 0; transition: color var(--motion-fast) var(--ease-out); }
|
||||
.nav-item span:not(.nav-badge) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.nav-item svg { color: var(--text-3); transition: color var(--motion-fast) var(--ease-out); }
|
||||
.nav-item .icon-box { color: var(--text-3); }
|
||||
.nav-item.active .icon-box { color: var(--text-1); }
|
||||
.nav-item span:not(.nav-badge):not(.icon-box) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.nav-badge {
|
||||
margin-left: auto;
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.nav-divider { height: 1px; background: var(--border-soft); margin: var(--sp-2); }
|
||||
.org-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
Reference in new issue
Block a user