chore: separate the header brand from the navigator
The space switcher and the navigator pills shared one grammar (rounded pill, same padding, 16-20px glyph, labels on the same rail), so the header read as two rows of a list. Make the brand a title (24px mark, 15px semibold, small chevron), keep only the bordered compose control in the header, move search to the trailing end of the navigator, and size the nav pills as 28px squares to match compose.
This commit is contained in:
parent
6346bed57f
commit
02caf98c7b
5 changed files with 49 additions and 33 deletions
|
|
@ -63,8 +63,6 @@ import {
|
||||||
import TagsSection from "./TagsSection";
|
import TagsSection from "./TagsSection";
|
||||||
import ViewsSection from "./ViewsSection";
|
import ViewsSection from "./ViewsSection";
|
||||||
|
|
||||||
const SIDEBAR_HEADER_ACTION_CLASSES =
|
|
||||||
"size-7 shrink-0 rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50";
|
|
||||||
const SIDEBAR_HEADER_PRIMARY_ACTION_CLASSES =
|
const SIDEBAR_HEADER_PRIMARY_ACTION_CLASSES =
|
||||||
"size-7 shrink-0 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50";
|
"size-7 shrink-0 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50";
|
||||||
|
|
||||||
|
|
@ -324,8 +322,9 @@ interface GlobalNavItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The compact navigator is intentionally horizontal. Its 16px glyph plus 8px padding
|
* The compact navigator is intentionally horizontal. Its 16px glyph plus 6px padding
|
||||||
* on each side makes the collapsed control an exact 32px square. Expanding the
|
* on each side makes the collapsed control an exact 28px square, the same box as the
|
||||||
|
* header's compose control. Expanding the
|
||||||
* label only opens the text track, so the artwork and surface never jump.
|
* label only opens the text track, so the artwork and surface never jump.
|
||||||
*/
|
*/
|
||||||
const navPillClasses = (active: boolean) =>
|
const navPillClasses = (active: boolean) =>
|
||||||
|
|
@ -336,7 +335,7 @@ const NavPillLabel = ({ expanded, label, children }: { expanded: boolean; label:
|
||||||
aria-hidden={!expanded || undefined}
|
aria-hidden={!expanded || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid min-w-0 transition-[grid-template-columns,padding] duration-200 ease-out motion-reduce:transition-none",
|
"grid min-w-0 transition-[grid-template-columns,padding] duration-200 ease-out motion-reduce:transition-none",
|
||||||
expanded ? "grid-cols-[1fr] ps-2" : "grid-cols-[0fr] ps-0",
|
expanded ? "grid-cols-[1fr] ps-2.5" : "grid-cols-[0fr] ps-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex min-w-0 items-center gap-1.5 overflow-hidden">
|
<span className="flex min-w-0 items-center gap-1.5 overflow-hidden">
|
||||||
|
|
@ -353,7 +352,7 @@ const GlobalNavigation = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const { memoDetail, memoScope, setMemoScope, setMobileOpen } = useAppSidebar();
|
const { memoDetail, memoScope, setMemoScope, setMobileOpen, setQuickFindOpen } = useAppSidebar();
|
||||||
const { filters } = useMemoFilterContext();
|
const { filters } = useMemoFilterContext();
|
||||||
const routeKind = getSidebarRouteKind(location.pathname);
|
const routeKind = getSidebarRouteKind(location.pathname);
|
||||||
const resolvedScope = resolveMemoScope(location.pathname, {
|
const resolvedScope = resolveMemoScope(location.pathname, {
|
||||||
|
|
@ -442,7 +441,7 @@ const GlobalNavigation = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<nav className={cn("flex h-8 items-center gap-1", SIDEBAR_RAIL_CLASSES)} aria-label="Primary">
|
<nav className={cn("flex h-7 items-center gap-1", SIDEBAR_RAIL_CLASSES)} aria-label="Primary">
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
onOpenChange={(open, eventDetails) => {
|
onOpenChange={(open, eventDetails) => {
|
||||||
|
|
@ -522,6 +521,28 @@ const GlobalNavigation = () => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* Search is a place to go, not a header action: it closes the navigator's row
|
||||||
|
so the header keeps only the brand and the bordered compose control. */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("common.search")}
|
||||||
|
className={cn("ms-auto", navPillClasses(false))}
|
||||||
|
onClick={() => {
|
||||||
|
setMobileOpen(false);
|
||||||
|
setQuickFindOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={SIDEBAR_NAV_LEADING_SLOT_CLASSES} aria-hidden="true">
|
||||||
|
<SearchIcon className="size-4 opacity-75" strokeWidth={1.8} />
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">{t("common.search")}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</nav>
|
</nav>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|
@ -553,28 +574,14 @@ const SidebarBrand = ({ className, size = "md" }: { className?: string; size?: "
|
||||||
const AppSidebar = ({ className }: { className?: string }) => {
|
const AppSidebar = ({ className }: { className?: string }) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const { setMobileOpen, setQuickFindOpen } = useAppSidebar();
|
const { setMobileOpen } = useAppSidebar();
|
||||||
const { canOpen: canCompose, openEditor } = useGlobalMemoEditor();
|
const { canOpen: canCompose, openEditor } = useGlobalMemoEditor();
|
||||||
return (
|
return (
|
||||||
<aside className={cn("flex h-full w-full select-none flex-col bg-sidebar text-sidebar-foreground", className)}>
|
<aside className={cn("flex h-full w-full select-none flex-col bg-sidebar text-sidebar-foreground", className)}>
|
||||||
<div data-sidebar-header className={cn("flex h-13 shrink-0 items-center justify-between gap-2", SIDEBAR_RAIL_CLASSES)}>
|
<div data-sidebar-header className={cn("flex h-13 shrink-0 items-center justify-between gap-2", SIDEBAR_RAIL_CLASSES)}>
|
||||||
<SidebarBrand className="min-w-0" size="header" />
|
<SidebarBrand className="min-w-0" size="header" />
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
className={SIDEBAR_HEADER_ACTION_CLASSES}
|
|
||||||
onClick={() => {
|
|
||||||
setMobileOpen(false);
|
|
||||||
setQuickFindOpen(true);
|
|
||||||
}}
|
|
||||||
aria-label={t("common.search")}
|
|
||||||
>
|
|
||||||
<SearchIcon className="size-4" strokeWidth={1.8} />
|
|
||||||
</Button>
|
|
||||||
{canCompose && <NewMemoAction onClick={openEditor} />}
|
{canCompose && <NewMemoAction onClick={openEditor} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<GlobalNavigation />
|
<GlobalNavigation />
|
||||||
<div className="mx-3 mt-2 border-t border-border/70" />
|
<div className="mx-3 mt-2 border-t border-border/70" />
|
||||||
<div className={cn("min-h-0 flex-1 overflow-y-auto overflow-x-hidden pt-2 pb-3 [scrollbar-width:thin]", SIDEBAR_RAIL_CLASSES)}>
|
<div className={cn("min-h-0 flex-1 overflow-y-auto overflow-x-hidden pt-2 pb-3 [scrollbar-width:thin]", SIDEBAR_RAIL_CLASSES)}>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ function SpaceSwitcher({ className, size = "md" }: { className?: string; size?:
|
||||||
? `${selectedSpace?.title || t("space.current")}${showSelectedSpaceUid && selectedSpaceUid ? ` (${selectedSpaceUid})` : ""}`
|
? `${selectedSpace?.title || t("space.current")}${showSelectedSpaceUid && selectedSpaceUid ? ` (${selectedSpaceUid})` : ""}`
|
||||||
: t("common.memos");
|
: t("common.memos");
|
||||||
const brandSize = size === "header" ? "header" : "md";
|
const brandSize = size === "header" ? "header" : "md";
|
||||||
const spaceMarkSize = size === "header" ? "sm" : "md";
|
const spaceMarkSize = size === "header" ? "header" : "md";
|
||||||
|
|
||||||
const handleMenuOpenChange = (open: boolean) => {
|
const handleMenuOpenChange = (open: boolean) => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
@ -99,15 +99,15 @@ function SpaceSwitcher({ className, size = "md" }: { className?: string; size?:
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className={cn("flex min-w-0 items-center overflow-hidden", size === "header" ? "gap-1" : "gap-1.5")}>
|
<span className={cn("flex min-w-0 items-center overflow-hidden", size === "header" ? "gap-2" : "gap-1.5")}>
|
||||||
{selectedSpaceName ? (
|
{selectedSpaceName ? (
|
||||||
<>
|
<>
|
||||||
<SpaceMark size={spaceMarkSize} />
|
<SpaceMark size={spaceMarkSize} />
|
||||||
<span data-sidebar-label className="flex min-w-0 flex-1 flex-col justify-center overflow-hidden">
|
<span data-sidebar-label className="flex min-w-0 flex-1 flex-col justify-center overflow-hidden">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"block truncate text-[14px] tracking-[-0.01em] text-foreground",
|
"block truncate tracking-[-0.01em] text-foreground",
|
||||||
size === "header" ? "font-semibold leading-5" : "font-medium leading-4",
|
size === "header" ? "text-[15px] font-semibold leading-5" : "text-[14px] font-medium leading-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedSpace?.title || t("space.current")}
|
{selectedSpace?.title || t("space.current")}
|
||||||
|
|
@ -127,7 +127,11 @@ function SpaceSwitcher({ className, size = "md" }: { className?: string; size?:
|
||||||
<MemosLogo compact size={brandSize} />
|
<MemosLogo compact size={brandSize} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<ChevronsUpDownIcon aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground/70" strokeWidth={1.8} />
|
<ChevronsUpDownIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("shrink-0 text-muted-foreground/70", size === "header" ? "size-3" : "size-3.5")}
|
||||||
|
strokeWidth={1.8}
|
||||||
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,28 @@ import { cva } from "class-variance-authority";
|
||||||
* The sidebar is drawn on one shared rail: inset surfaces start 12px from the shell,
|
* The sidebar is drawn on one shared rail: inset surfaces start 12px from the shell,
|
||||||
* visible artwork starts at 20px, and first-level labels start at 44px. The full-bleed
|
* visible artwork starts at 20px, and first-level labels start at 44px. The full-bleed
|
||||||
* footer action pads its content directly onto those same rails. Rows reserve a 20px
|
* footer action pads its content directly onto those same rails. Rows reserve a 20px
|
||||||
* artwork column; compact navigation reaches the label rail with an 8px gap.
|
* artwork column. Compact navigation is a 28px square like the header's compose control,
|
||||||
|
* so its glyph sits 2px inside the artwork rail and reaches the label rail with a 10px gap.
|
||||||
*/
|
*/
|
||||||
export const SIDEBAR_RAIL_CLASSES = "px-3";
|
export const SIDEBAR_RAIL_CLASSES = "px-3";
|
||||||
// Content rows reserve 20px so 16px icons and 20px marks share a stable label rail.
|
// Content rows reserve 20px so 16px icons and 20px marks share a stable label rail.
|
||||||
export const SIDEBAR_LEADING_SLOT_CLASSES = "flex size-5 shrink-0 items-center justify-center";
|
export const SIDEBAR_LEADING_SLOT_CLASSES = "flex size-5 shrink-0 items-center justify-center";
|
||||||
// Compact nav uses the visible 16px glyph itself as the cell, keeping 8px x/y padding.
|
// Compact nav uses the visible 16px glyph itself as the cell, keeping 6px x/y padding.
|
||||||
export const SIDEBAR_NAV_LEADING_SLOT_CLASSES = "flex size-4 shrink-0 items-center justify-center";
|
export const SIDEBAR_NAV_LEADING_SLOT_CLASSES = "flex size-4 shrink-0 items-center justify-center";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Brand controls hug their content, ordinary rows fill the inset rail, and the account
|
* Brand controls hug their content, ordinary rows fill the inset rail, and the account
|
||||||
* action fills the footer shell (or becomes a rail-aligned square when collapsed).
|
* action fills the footer shell (or becomes a rail-aligned square when collapsed).
|
||||||
|
* The header brand is the sidebar's title, not a row: a 24px mark on the artwork rail and
|
||||||
|
* a larger label, so it reads as a different kind of object from the 16px nav pills below.
|
||||||
* Heights stay fixed so data cannot change a surface's padding or vertical position.
|
* Heights stay fixed so data cannot change a surface's padding or vertical position.
|
||||||
*/
|
*/
|
||||||
export const sidebarSurfaceVariants = cva("min-w-0 items-center", {
|
export const sidebarSurfaceVariants = cva("min-w-0 items-center", {
|
||||||
variants: {
|
variants: {
|
||||||
role: {
|
role: {
|
||||||
row: "flex h-7 w-full gap-1 rounded-md px-2 text-ui",
|
row: "flex h-7 w-full gap-1 rounded-md px-2 text-ui",
|
||||||
navPill: "relative flex h-8 rounded-md px-2",
|
navPill: "relative flex h-7 rounded-md px-1.5",
|
||||||
headerBrand: "flex h-9 max-w-full gap-1 rounded-md px-2",
|
headerBrand: "flex h-9 max-w-full gap-2 rounded-md px-2",
|
||||||
mobileBrand: "flex h-9 max-w-full gap-1.5 rounded-md px-1",
|
mobileBrand: "flex h-9 max-w-full gap-1.5 rounded-md px-1",
|
||||||
account: "flex h-9 w-full gap-1 rounded-none px-5",
|
account: "flex h-9 w-full gap-1 rounded-none px-5",
|
||||||
accountCollapsed: "ms-3 flex size-9 rounded-md p-2",
|
accountCollapsed: "ms-3 flex size-9 rounded-md p-2",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface Props {
|
||||||
// wherever the two are listed together.
|
// wherever the two are listed together.
|
||||||
const COMPACT_SCALE = {
|
const COMPACT_SCALE = {
|
||||||
md: { mark: "size-7 rounded-[7px]", gap: "gap-1.5", title: "text-[14px]", weight: "font-medium" },
|
md: { mark: "size-7 rounded-[7px]", gap: "gap-1.5", title: "text-[14px]", weight: "font-medium" },
|
||||||
header: { mark: "size-5 rounded-[5px]", gap: "gap-1", title: "text-[14px] leading-5", weight: "font-semibold" },
|
header: { mark: "size-6 rounded-[6px]", gap: "gap-2", title: "text-[15px] leading-5", weight: "font-semibold" },
|
||||||
sm: { mark: "size-5 rounded-[5px]", gap: "gap-1.5", title: "text-ui", weight: "font-medium" },
|
sm: { mark: "size-5 rounded-[5px]", gap: "gap-1.5", title: "text-ui", weight: "font-medium" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ const MARK_SCALE = {
|
||||||
xl: { mark: "size-11 rounded-lg", icon: "size-6" },
|
xl: { mark: "size-11 rounded-lg", icon: "size-6" },
|
||||||
lg: { mark: "size-9 rounded-[7px]", icon: "size-5" },
|
lg: { mark: "size-9 rounded-[7px]", icon: "size-5" },
|
||||||
md: { mark: "size-7 rounded-[7px]", icon: "size-4" },
|
md: { mark: "size-7 rounded-[7px]", icon: "size-4" },
|
||||||
|
/** Primary chrome: kept in step with MemosLogo's header scale. */
|
||||||
|
header: { mark: "size-6 rounded-[6px]", icon: "size-3.5" },
|
||||||
sm: { mark: "size-5 rounded-[5px]", icon: "size-3" },
|
sm: { mark: "size-5 rounded-[5px]", icon: "size-3" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue