mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: set Mountain View's content on a rounded card over a tinted canvas
This commit is contained in:
+23
-2
@@ -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 {
|
||||
|
||||
@@ -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)' }}>
|
||||
|
||||
@@ -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)' }}>
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user