feat: set Mountain View's content on a rounded card over a tinted canvas

This commit is contained in:
Linus Rath
2026-10-12 00:37:59 +02:00
parent 2534c4fde5
commit ae151cec25
9 changed files with 52 additions and 21 deletions
+23 -2
View File
@@ -971,6 +971,26 @@ optgroup {
--mv-search-radius: 8px;
--mv-popover-w: 352px;
--mv-popover-radius: 28px;
--mv-card-radius: 16px;
/* Surfaces, tinted from the theme's primary so any theme keeps the same
relationships: a faintly tinted canvas behind the chrome, a plain card
for the content, a slightly deeper search field, a selected-row pill and
a soft Compose button. */
--mv-canvas: color-mix(in oklab, var(--color-primary) 4%, var(--color-background));
--mv-search-bg: color-mix(in oklab, var(--color-primary) 9%, var(--color-background));
--mv-nav-hover: color-mix(in oklab, var(--color-foreground) 7%, var(--mv-canvas));
--mv-nav-selected: color-mix(in oklab, var(--color-primary) 18%, var(--color-background));
--mv-compose-bg: color-mix(in oklab, var(--color-primary) 24%, var(--color-background));
--mv-compose-fg: color-mix(in oklab, var(--color-primary) 30%, var(--color-foreground));
--mv-row-read: color-mix(in oklab, var(--color-primary) 3%, var(--color-background));
}
/* The content card: everything that is not chrome sits on one rounded sheet
inset from the canvas. */
[data-interface-layout="mountain-view"] [data-mv-card] {
border-radius: var(--mv-card-radius);
background-color: var(--color-background);
overflow: hidden;
}
/* Search: a filled 48px field that lifts onto a card while focused, rather
@@ -979,14 +999,15 @@ optgroup {
height: var(--mv-search-h);
border-radius: var(--mv-search-radius);
border-color: transparent;
background-color: var(--color-muted);
background-color: var(--mv-search-bg);
padding-inline-start: 3rem;
font-size: 1rem;
}
[data-interface-layout="mountain-view"] .mv-topbar input[data-search-input]:hover {
border-color: transparent;
background-color: var(--color-accent);
background-color: var(--mv-search-bg);
box-shadow: 0 1px 1px 0 rgb(0 0 0 / 0.08), 0 1px 3px 1px rgb(0 0 0 / 0.04);
}
[data-interface-layout="mountain-view"] .mv-topbar input[data-search-input]:focus-visible {
+2 -2
View File
@@ -1729,7 +1729,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
);
return (
<div className={cn("flex flex-col bg-background overflow-hidden pt-[env(safe-area-inset-top)]", isEmbedded ? "h-full" : "h-dvh")}>
<div className={cn("flex flex-col overflow-hidden pt-[env(safe-area-inset-top)]", mvShell ? "bg-[var(--mv-canvas)]" : "bg-background", isEmbedded ? "h-full" : "h-dvh")}>
<AppTopBannerSlot />
{refreshIndicator}
{mvShell && (
@@ -1741,7 +1741,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
activeAppId={inlineApp?.id ?? null}
/>
)}
<div className={cn("relative flex flex-1 min-h-0 overflow-hidden", isMobile && "flex-col")}>
<div data-mv-card={mvShell ? "" : undefined} className={cn("relative flex flex-1 min-h-0 overflow-hidden", isMobile && "flex-col", mvShell && "mb-4 mx-4")}>
{/* Left Navigation Rail (hidden when embedded in Pro shell) */}
{!isMobile && !isEmbedded && !mvShell && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
+2 -2
View File
@@ -901,7 +901,7 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
};
return (
<div className={cn("flex flex-col bg-background overflow-hidden pt-[env(safe-area-inset-top)]", isEmbedded ? "h-full" : "h-dvh")}>
<div className={cn("flex flex-col overflow-hidden pt-[env(safe-area-inset-top)]", mvShell ? "bg-[var(--mv-canvas)]" : "bg-background", isEmbedded ? "h-full" : "h-dvh")}>
<AppTopBannerSlot />
{refreshIndicator}
{mvShell && (
@@ -913,7 +913,7 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
activeAppId={inlineApp?.id ?? null}
/>
)}
<div className={cn("flex flex-1 min-h-0 overflow-hidden", isMobile && "flex-col")}>
<div data-mv-card={mvShell ? "" : undefined} className={cn("flex flex-1 min-h-0 overflow-hidden", isMobile && "flex-col", mvShell && "mb-4 mx-4")}>
{/* Navigation Rail - desktop only (hidden when embedded in Pro shell) */}
{!isMobile && !isEmbedded && !mvShell && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
+2 -2
View File
@@ -593,7 +593,7 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
if (!isAuthenticated) return null;
return (
<div className={cn("flex flex-col bg-background overflow-hidden pt-[env(safe-area-inset-top)]", isEmbedded ? "h-full" : "h-dvh")}>
<div className={cn("flex flex-col overflow-hidden pt-[env(safe-area-inset-top)]", mvShell ? "bg-[var(--mv-canvas)]" : "bg-background", isEmbedded ? "h-full" : "h-dvh")}>
<AppTopBannerSlot />
{refreshIndicator}
{mvShell && (
@@ -605,7 +605,7 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
activeAppId={inlineApp?.id ?? null}
/>
)}
<div className="flex flex-1 min-h-0 overflow-hidden">
<div data-mv-card={mvShell ? "" : undefined} className={cn("flex flex-1 min-h-0 overflow-hidden", mvShell && "mb-4 mx-4")}>
{!isMobile && !isEmbedded && !mvShell && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
<NavigationRail
+4 -3
View File
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
import {
BookUser,
Calendar,
Filter,
Grid3x3,
HardDrive,
Keyboard,
@@ -12,6 +11,7 @@ import {
Menu,
Settings,
Shield,
SlidersHorizontal,
iconForName,
} from "@/components/icons";
import type { AppIcon as AppIconType } from "@/components/icons";
@@ -161,7 +161,7 @@ export function MvTopBar({
return (
<header
className="mv-topbar flex items-center h-16 gap-2 ps-1 pe-4 shrink-0 bg-background"
className="mv-topbar flex items-center h-16 gap-2 ps-1 pe-4 shrink-0 bg-[var(--mv-canvas)]"
data-mv-topbar=""
>
{/* Brand block: occupies the sidebar's own column width */}
@@ -192,6 +192,7 @@ export function MvTopBar({
onClear={search.onClear}
onSelectContact={search.onSelectContact}
disabled={search.disabled}
placeholder={t("search_placeholder")}
/>
<button
type="button"
@@ -208,7 +209,7 @@ export function MvTopBar({
title={tSearch("toggle_filters")}
aria-label={tSearch("toggle_filters")}
>
<Filter className="w-5 h-5" />
<SlidersHorizontal className="w-5 h-5" />
{!search.filtersOpen && search.activeFilterCount > 0 && (
<span className="absolute top-1.5 end-1.5 grid place-items-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-primary-foreground">
{search.activeFilterCount}
+3 -1
View File
@@ -1256,7 +1256,9 @@ export function Sidebar({
data-mv-rail-peek={hoverExpanded ? "" : undefined}
className={cn(
"relative flex flex-col h-full border-e transition-all duration-300 overflow-hidden",
"bg-secondary border-border",
// Mountain View's navigation sits on the canvas, with no rule between
// it and the content card.
mvShell ? "bg-[var(--mv-canvas)] border-transparent" : "bg-secondary border-border",
"max-lg:w-full",
// Mountain View's collapsed column is 72px (--mv-rail-w), wide enough
// for its round compose button; the default layout's rail is 48px.
+10 -5
View File
@@ -3862,7 +3862,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
return (
<DragDropProvider>
<div ref={appRootRef} className={cn("flex flex-col bg-background overflow-hidden pt-[env(safe-area-inset-top)]", isEmbedded ? "h-full" : "h-dvh")}>
<div ref={appRootRef} className={cn("flex flex-col overflow-hidden pt-[env(safe-area-inset-top)]", mvShell ? "bg-[var(--mv-canvas)]" : "bg-background", isEmbedded ? "h-full" : "h-dvh")}>
<AppTopBannerSlot />
{refreshIndicator}
{isRateLimited && rateLimitSecondsLeft !== null && (
@@ -4030,8 +4030,9 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
</ErrorBoundary>
</div>
{/* Sidebar resize handle (desktop only, hidden when collapsed) */}
{!isMobile && !isTablet && !sidebarCollapsed && !inlineApp && !fullscreenReading && (
{/* Sidebar resize handle (desktop only, hidden when collapsed, and
absent under Mountain View, whose navigation has a fixed width) */}
{!isMobile && !isTablet && !sidebarCollapsed && !inlineApp && !fullscreenReading && !mvShell && (
<ResizeHandle
onResizeStart={() => { dragStartWidth.current = sidebarWidth; setIsResizing(true); }}
onResize={(delta) => setSidebarWidth(dragStartWidth.current + delta)}
@@ -4040,8 +4041,12 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
/>
)}
{/* Main Content Area */}
<div className={cn("flex flex-col flex-1 min-w-0 h-full", inlineApp && "hidden")}>
{/* Main Content Area. Under Mountain View it is the rounded card
inset from the canvas. */}
<div
data-mv-card={mvShell ? "" : undefined}
className={cn("flex flex-col flex-1 min-w-0", mvShell ? "mb-4 me-4" : "h-full", inlineApp && "hidden")}
>
<div className={cn("relative flex flex-1 min-h-0", isHorizontalMailLayout && "md:flex-col")}>
{/* Email List - full width on mobile, fixed width/height on tablet/desktop */}
<div
+4 -2
View File
@@ -24,6 +24,8 @@ interface SearchBoxProps {
title?: string;
/** Focus on mount — the mobile search panel opens straight to the keyboard. */
autoFocus?: boolean;
/** Overrides the default placeholder, which carries the `/` shortcut hint. */
placeholder?: string;
}
/**
@@ -33,7 +35,7 @@ interface SearchBoxProps {
* reuses the composer's contact autocomplete lookup so directory principals,
* address-book contacts and recent recipients all surface here too.
*/
export function SearchBox({ value, onChange, onSubmit, onClear, onSelectContact, disabled = false, title, autoFocus }: SearchBoxProps) {
export function SearchBox({ value, onChange, onSubmit, onClear, onSelectContact, disabled = false, title, autoFocus, placeholder }: SearchBoxProps) {
const t = useTranslations("sidebar");
const listId = `search-suggestions-${useId().replace(/:/g, "")}`;
@@ -124,7 +126,7 @@ export function SearchBox({ value, onChange, onSubmit, onClear, onSelectContact,
<Input
type="search"
autoFocus={autoFocus}
placeholder={t("search_placeholder_hint")}
placeholder={placeholder ?? t("search_placeholder_hint")}
value={value}
onChange={(e) => {
onChange(e.target.value);
+2 -2
View File
@@ -869,7 +869,7 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
// Desktop layout
return (
<div className={cn("flex flex-col bg-background pt-[env(safe-area-inset-top)]", isEmbedded ? "h-full" : "h-dvh")}>
<div className={cn("flex flex-col pt-[env(safe-area-inset-top)]", mvShell ? "bg-[var(--mv-canvas)]" : "bg-background", isEmbedded ? "h-full" : "h-dvh")}>
<AppTopBannerSlot />
{mvShell && (
<MvTopBar
@@ -880,7 +880,7 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
activeAppId={inlineApp?.id ?? null}
/>
)}
<div className="flex flex-1 min-h-0">
<div data-mv-card={mvShell ? "" : undefined} className={cn("flex flex-1 min-h-0", mvShell && "mb-4 mx-4")}>
{!isEmbedded && !mvShell && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
<NavigationRail