mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
Add an "Interface layout" setting at the top of Settings > Layout with two options, Default and Mountain View. Default is the existing layout and stays the default, so nothing changes unless a user opts in. Mountain View rearranges the chrome while keeping the theme's colours, icons and type: - desktop: a top bar with a wide search field (filters drop from the field), an apps grid and the account avatar instead of the left app rail; flat navigation with Starred under Inbox and the rest behind "More"; a Compose pill; one list toolbar with select-all, refresh, overflow menu, a 1-N of total range and the batch actions; rows led by a checkbox and a star; an icon-only message toolbar with Reply / Reply all / Forward under the message; compose in a docked window; an account popover with per-account unread counts; j/k cursor, x to select and g-sequences - phone: rounded search header, compact rows with the star on the preview line, and Reply / Reply all / Forward at the end of a message instead of the bottom action bar It is one setting (`interfaceLayout`), written to a data attribute on <html> that keys a block of CSS tokens in globals.css, plus a few components gated on the flag. The mail layout in force is derived (effectiveMailLayout), so the user's saved mail layout is never rewritten. The setting is listed in the admin policy tab, and the new strings are translated in all locales. Includes unit tests and a Playwright smoke test against a running instance (scripts/mountain-view-smoke/).
1020 lines
25 KiB
CSS
1020 lines
25 KiB
CSS
@import "tailwindcss";
|
|
@import "tw-animate-css";
|
|
|
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
|
|
:root {
|
|
font-size: var(--font-size-base);
|
|
|
|
--color-border: #e2e8f0;
|
|
--color-input: #e2e8f0;
|
|
--color-ring: #94a3b8;
|
|
--color-background: #ffffff;
|
|
--color-foreground: #0f172a;
|
|
--color-primary: #3b82f6;
|
|
--color-primary-foreground: #ffffff;
|
|
--color-secondary: #f8fafc;
|
|
--color-secondary-foreground: #0f172a;
|
|
--color-muted: #f1f5f9;
|
|
--color-muted-foreground: #64748b;
|
|
--color-accent: #dbeafe;
|
|
--color-accent-foreground: #1e40af;
|
|
--color-destructive: #ef4444;
|
|
--color-destructive-foreground: #ffffff;
|
|
--color-popover: #ffffff;
|
|
--color-popover-foreground: #0f172a;
|
|
|
|
/* Sidebar */
|
|
--color-sidebar: #f8fafc;
|
|
--color-sidebar-foreground: #0f172a;
|
|
--color-sidebar-border: #e2e8f0;
|
|
--color-sidebar-accent: #f1f5f9;
|
|
--color-sidebar-accent-foreground: #0f172a;
|
|
|
|
/* Card */
|
|
--color-card: #ffffff;
|
|
--color-card-foreground: #0f172a;
|
|
|
|
/* Semantic status colors */
|
|
--color-success: #22c55e;
|
|
--color-success-foreground: #ffffff;
|
|
--color-warning: #eab308;
|
|
--color-warning-foreground: #ffffff;
|
|
--color-info: #3b82f6;
|
|
--color-info-foreground: #ffffff;
|
|
|
|
/* Selection states */
|
|
--color-selection: #dbeafe;
|
|
--color-selection-foreground: #1e40af;
|
|
|
|
/* Unread indicator */
|
|
--color-unread: #3b82f6;
|
|
|
|
/* Chart colors */
|
|
--color-chart-1: #3b82f6;
|
|
--color-chart-2: #22c55e;
|
|
--color-chart-3: #f59e0b;
|
|
--color-chart-4: #ef4444;
|
|
--color-chart-5: #8b5cf6;
|
|
|
|
/* Settings variables */
|
|
--font-size-base: 16px;
|
|
--list-item-height: 48px;
|
|
--transition-duration: 0.2s;
|
|
|
|
/* Density spacing variables (defaults = regular) */
|
|
--density-item-py: 12px;
|
|
--density-item-gap: 12px;
|
|
--density-header-py: 12px;
|
|
--density-card-p: 16px;
|
|
--density-sidebar-py: 4px;
|
|
}
|
|
|
|
.dark {
|
|
--color-border: rgba(128, 128, 128, 0.3);
|
|
--color-input: #262626;
|
|
--color-ring: #d4d4d4;
|
|
--color-background: #0a0a0a;
|
|
--color-foreground: #fafafa;
|
|
--color-primary: #fafafa;
|
|
--color-primary-foreground: #171717;
|
|
--color-secondary: #262626;
|
|
--color-secondary-foreground: #fafafa;
|
|
--color-muted: #262626;
|
|
--color-muted-foreground: #a3a3a3;
|
|
--color-accent: #1e3a8a;
|
|
--color-accent-foreground: #dbeafe;
|
|
--color-destructive: #ef4444;
|
|
--color-destructive-foreground: #fafafa;
|
|
--color-popover: #1c1c1c;
|
|
--color-popover-foreground: #fafafa;
|
|
|
|
/* Sidebar */
|
|
--color-sidebar: #0a0a0a;
|
|
--color-sidebar-foreground: #fafafa;
|
|
--color-sidebar-border: #262626;
|
|
--color-sidebar-accent: #1a1a1a;
|
|
--color-sidebar-accent-foreground: #fafafa;
|
|
|
|
/* Card */
|
|
--color-card: #141414;
|
|
--color-card-foreground: #fafafa;
|
|
|
|
/* Semantic status colors */
|
|
--color-success: #16a34a;
|
|
--color-success-foreground: #ffffff;
|
|
--color-warning: #ca8a04;
|
|
--color-warning-foreground: #ffffff;
|
|
--color-info: #60a5fa;
|
|
--color-info-foreground: #ffffff;
|
|
|
|
/* Selection states */
|
|
--color-selection: rgba(59, 130, 246, 0.25);
|
|
--color-selection-foreground: #93c5fd;
|
|
|
|
/* Unread indicator */
|
|
--color-unread: #60a5fa;
|
|
|
|
/* Chart colors */
|
|
--color-chart-1: #60a5fa;
|
|
--color-chart-2: #4ade80;
|
|
--color-chart-3: #fbbf24;
|
|
--color-chart-4: #f87171;
|
|
--color-chart-5: #a78bfa;
|
|
}
|
|
|
|
@theme inline {
|
|
--color-border: var(--color-border);
|
|
--color-input: var(--color-input);
|
|
--color-ring: var(--color-ring);
|
|
--color-background: var(--color-background);
|
|
--color-foreground: var(--color-foreground);
|
|
--color-primary: var(--color-primary);
|
|
--color-primary-foreground: var(--color-primary-foreground);
|
|
--color-secondary: var(--color-secondary);
|
|
--color-secondary-foreground: var(--color-secondary-foreground);
|
|
--color-muted: var(--color-muted);
|
|
--color-muted-foreground: var(--color-muted-foreground);
|
|
--color-accent: var(--color-accent);
|
|
--color-accent-foreground: var(--color-accent-foreground);
|
|
--color-destructive: var(--color-destructive);
|
|
--color-destructive-foreground: var(--color-destructive-foreground);
|
|
--color-popover: var(--color-popover);
|
|
--color-popover-foreground: var(--color-popover-foreground);
|
|
--color-sidebar: var(--color-sidebar);
|
|
--color-sidebar-foreground: var(--color-sidebar-foreground);
|
|
--color-sidebar-border: var(--color-sidebar-border);
|
|
--color-sidebar-accent: var(--color-sidebar-accent);
|
|
--color-sidebar-accent-foreground: var(--color-sidebar-accent-foreground);
|
|
--color-card: var(--color-card);
|
|
--color-card-foreground: var(--color-card-foreground);
|
|
--color-success: var(--color-success);
|
|
--color-success-foreground: var(--color-success-foreground);
|
|
--color-warning: var(--color-warning);
|
|
--color-warning-foreground: var(--color-warning-foreground);
|
|
--color-info: var(--color-info);
|
|
--color-info-foreground: var(--color-info-foreground);
|
|
--color-selection: var(--color-selection);
|
|
--color-selection-foreground: var(--color-selection-foreground);
|
|
--color-unread: var(--color-unread);
|
|
--color-chart-1: var(--color-chart-1);
|
|
--color-chart-2: var(--color-chart-2);
|
|
--color-chart-3: var(--color-chart-3);
|
|
--color-chart-4: var(--color-chart-4);
|
|
--color-chart-5: var(--color-chart-5);
|
|
}
|
|
|
|
* {
|
|
border-color: var(--color-border);
|
|
}
|
|
|
|
body {
|
|
background-color: var(--color-background);
|
|
color: var(--color-foreground);
|
|
font-family:
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
"Segoe UI",
|
|
Roboto,
|
|
"Helvetica Neue",
|
|
Arial,
|
|
"Noto Sans Thai",
|
|
"Leelawadee UI",
|
|
Tahoma,
|
|
sans-serif;
|
|
font-feature-settings:
|
|
"rlig" 1,
|
|
"calt" 1;
|
|
}
|
|
|
|
/* Native <select> option lists are painted by the browser, not by the
|
|
Tailwind classes on the control, so they only honour color-scheme and an
|
|
explicit option colour. Themes whose --color-muted is translucent (Aurora
|
|
Glass dark: rgba(255,255,255,0.05)) composited to a light popup with
|
|
near-white text (#999). Pin options to the popover tokens, which every
|
|
theme defines as an opaque surface, and let dark themes ask the browser
|
|
for a dark popup where option colours are ignored (Safari). */
|
|
option {
|
|
background-color: var(--color-popover);
|
|
color: var(--color-popover-foreground);
|
|
}
|
|
|
|
/* The group labels (the From dropdown's accounts) are painted the same way.
|
|
Without colours of their own they kept the control's text colour on the
|
|
popup's base, which a transparent control leaves light: in dark themes,
|
|
light text on a white bar. */
|
|
optgroup {
|
|
background-color: var(--color-popover);
|
|
color: var(--color-popover-foreground);
|
|
}
|
|
|
|
.dark select {
|
|
color-scheme: dark;
|
|
}
|
|
|
|
/* Minimalist scrollbar */
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background-color: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background-color: var(--color-border);
|
|
border-radius: 9999px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background-color: var(--color-muted-foreground);
|
|
}
|
|
|
|
/* Enhanced Email Content Styling */
|
|
|
|
.email-content-wrapper {
|
|
min-width: 100%;
|
|
}
|
|
|
|
/* Forces light-theme CSS variables inside the email content area, so when
|
|
"Always Show Emails in Light Mode" is enabled in dark theme the surrounding
|
|
sender info / attachments / plain-text body don't end up with light text
|
|
on a white background. */
|
|
.email-content-light {
|
|
--color-background: #ffffff;
|
|
--color-foreground: #0f172a;
|
|
--color-muted: #f1f5f9;
|
|
--color-muted-foreground: #64748b;
|
|
--color-border: #e2e8f0;
|
|
--color-card: #ffffff;
|
|
--color-card-foreground: #0f172a;
|
|
--color-popover: #ffffff;
|
|
--color-popover-foreground: #0f172a;
|
|
--color-secondary: #f8fafc;
|
|
--color-secondary-foreground: #0f172a;
|
|
--color-accent: #dbeafe;
|
|
--color-accent-foreground: #1e40af;
|
|
--color-input: #e2e8f0;
|
|
}
|
|
|
|
.email-content-light .email-content-text a {
|
|
color: #2563eb;
|
|
}
|
|
|
|
.email-content-text {
|
|
padding: 1rem 1.25rem;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.email-content-text {
|
|
padding-left: 0.75rem;
|
|
padding-right: 0.75rem;
|
|
}
|
|
}
|
|
|
|
.email-content-text a {
|
|
color: #2563eb;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.dark .email-content-text a {
|
|
color: #60a5fa;
|
|
}
|
|
|
|
.email-content {
|
|
font-family:
|
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
|
Arial, sans-serif;
|
|
font-size: 0.9375rem;
|
|
line-height: 1.6;
|
|
color: var(--color-foreground);
|
|
max-width: none;
|
|
overflow-wrap: break-word;
|
|
word-wrap: break-word;
|
|
padding: 1rem 1.25rem;
|
|
}
|
|
|
|
/* Remove padding for rich HTML emails with table-based layouts */
|
|
.email-content:has(> table) {
|
|
padding: 0;
|
|
}
|
|
|
|
.email-content p {
|
|
margin: 0.75rem 0;
|
|
}
|
|
|
|
.email-content a {
|
|
color: var(--color-primary);
|
|
text-decoration: none;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.email-content a:hover {
|
|
text-decoration: underline;
|
|
color: var(--color-accent-foreground);
|
|
}
|
|
|
|
.email-content img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: 0.375rem;
|
|
margin: 1rem 0;
|
|
display: block;
|
|
}
|
|
|
|
.email-content blockquote {
|
|
border-left: 3px solid var(--color-border);
|
|
padding-left: 1rem;
|
|
margin: 1rem 0;
|
|
color: var(--color-muted-foreground);
|
|
font-style: italic;
|
|
}
|
|
|
|
.email-content pre {
|
|
background-color: var(--color-muted);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
font-family: "SF Mono", Monaco, Menlo, Consolas, monospace;
|
|
font-size: 0.875rem;
|
|
overflow-x: auto;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.email-content code {
|
|
background-color: var(--color-muted);
|
|
padding: 0.125rem 0.375rem;
|
|
border-radius: 0.25rem;
|
|
font-family: "SF Mono", Monaco, Menlo, Consolas, monospace;
|
|
font-size: 0.875rem;
|
|
color: var(--color-destructive);
|
|
}
|
|
|
|
.email-content h1,
|
|
.email-content h2,
|
|
.email-content h3,
|
|
.email-content h4 {
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.75rem;
|
|
font-weight: 600;
|
|
line-height: 1.25;
|
|
color: var(--color-foreground);
|
|
}
|
|
|
|
.email-content h1 {
|
|
font-size: 1.5rem;
|
|
}
|
|
.email-content h2 {
|
|
font-size: 1.25rem;
|
|
}
|
|
.email-content h3 {
|
|
font-size: 1.125rem;
|
|
}
|
|
.email-content h4 {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.email-content ul,
|
|
.email-content ol {
|
|
margin: 1rem 0;
|
|
padding-left: 1.75rem;
|
|
}
|
|
|
|
.email-content li {
|
|
margin: 0.375rem 0;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.email-content ul li {
|
|
list-style-type: disc;
|
|
}
|
|
|
|
.email-content ol li {
|
|
list-style-type: decimal;
|
|
}
|
|
|
|
/* Reply quoted HTML - preserves original inline styles/colors */
|
|
.email-reply-quote {
|
|
overflow-wrap: break-word;
|
|
word-wrap: break-word;
|
|
max-width: none;
|
|
}
|
|
|
|
.email-reply-quote p {
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.email-reply-quote img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.email-reply-quote a {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Only style tables that are actual data tables, not layout tables */
|
|
.email-content table.data-table,
|
|
.email-content table[border="1"] {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin: 1rem 0;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.email-content table.data-table th,
|
|
.email-content table.data-table td,
|
|
.email-content table[border="1"] th,
|
|
.email-content table[border="1"] td {
|
|
padding: 0.625rem;
|
|
border: 1px solid var(--color-border);
|
|
text-align: left;
|
|
}
|
|
|
|
.email-content table.data-table th,
|
|
.email-content table[border="1"] th {
|
|
background-color: var(--color-muted);
|
|
font-weight: 600;
|
|
color: var(--color-foreground);
|
|
}
|
|
|
|
/* Reset styles for layout tables (commonly used in HTML emails) */
|
|
.email-content table {
|
|
border-collapse: collapse;
|
|
border-spacing: 0;
|
|
}
|
|
|
|
/* Let HTML email's own styles take precedence */
|
|
.email-content table:not(.data-table):not([border="1"]) {
|
|
border: initial;
|
|
}
|
|
|
|
.email-content table:not(.data-table):not([border="1"]) td,
|
|
.email-content table:not(.data-table):not([border="1"]) th {
|
|
border: initial;
|
|
padding: initial;
|
|
}
|
|
|
|
.email-content hr {
|
|
border: none;
|
|
border-top: 1px solid var(--color-border);
|
|
margin: 1.5rem 0;
|
|
}
|
|
|
|
/* Email thread quoted text */
|
|
.email-content .quoted-text {
|
|
border-left: 3px solid var(--color-border);
|
|
padding-left: 1rem;
|
|
margin: 1rem 0;
|
|
color: var(--color-muted-foreground);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Toast animations */
|
|
@keyframes toast-enter {
|
|
from {
|
|
transform: translateX(calc(100% + 1.25rem));
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes toast-exit {
|
|
from {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translateX(calc(100% + 1.25rem));
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes toast-progress {
|
|
from {
|
|
width: 100%;
|
|
}
|
|
to {
|
|
width: 0%;
|
|
}
|
|
}
|
|
|
|
.toast-enter {
|
|
animation: toast-enter 0.32s cubic-bezier(0.21, 1.02, 0.73, 1) forwards;
|
|
}
|
|
|
|
.toast-exit {
|
|
animation: toast-exit 0.28s cubic-bezier(0.06, 0.71, 0.55, 1) forwards;
|
|
}
|
|
|
|
/* Mobile Responsive Utilities */
|
|
|
|
/* Safe area insets for notched devices (iPhone X+, etc.) */
|
|
.safe-area-inset-top {
|
|
padding-top: env(safe-area-inset-top);
|
|
}
|
|
|
|
.safe-area-inset-bottom {
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
.safe-area-inset-left {
|
|
padding-left: env(safe-area-inset-left);
|
|
}
|
|
|
|
.safe-area-inset-right {
|
|
padding-right: env(safe-area-inset-right);
|
|
}
|
|
|
|
/* Minimum touch targets (44x44px recommended by Apple HIG) */
|
|
.touch-target {
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
}
|
|
|
|
/* Prevent text selection on mobile for UI elements */
|
|
.no-select {
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
-webkit-touch-callout: none;
|
|
}
|
|
|
|
/* Smooth transitions for view switching */
|
|
.view-transition {
|
|
transition:
|
|
transform 0.3s ease-in-out,
|
|
opacity 0.3s ease-in-out;
|
|
}
|
|
|
|
/* Hide scrollbar on mobile while keeping functionality */
|
|
@media (max-width: 767px) {
|
|
.mobile-scroll-hidden::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-scroll-hidden {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
}
|
|
|
|
.scroll-hidden::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.scroll-hidden {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
/* Mobile backdrop blur support */
|
|
@supports (backdrop-filter: blur(8px)) {
|
|
.mobile-backdrop {
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
}
|
|
}
|
|
|
|
/* Prevent overscroll bounce on iOS */
|
|
.no-overscroll {
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
/* iOS Safari zooms the viewport whenever a focused field computes below 16px,
|
|
* and leaves the page zoomed and panned once the keyboard closes (#838). Every
|
|
* field here is text-sm (0.875rem), so no --font-size-base setting escapes it:
|
|
* small 12.25px, medium 14px, large 15.75px. The viewport meta deliberately
|
|
* omits maximum-scale so pinch zoom keeps working, which leaves CSS as the
|
|
* only place to put the floor.
|
|
*
|
|
* max() raises the small fields to 16px while letting the "Large" setting go
|
|
* past it (18px) rather than pinning every field to the threshold, and
|
|
* pointer: coarse keeps desktop typography untouched. Checkbox and radio are
|
|
* excluded because their box is font-relative in Safari; the :not() chain also
|
|
* lifts specificity above .text-sm, so no !important is needed.
|
|
*
|
|
* Quoted replies are exempt by construction: they render inside a shadow root
|
|
* (components/email/quoted-html.ts) so they match the sent mail 1:1, and page
|
|
* CSS cannot reach in. */
|
|
@media (pointer: coarse) {
|
|
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
|
textarea,
|
|
select,
|
|
[contenteditable="true"] {
|
|
font-size: max(16px, 1rem);
|
|
}
|
|
}
|
|
|
|
/* Shake animation (for rejected input) */
|
|
@keyframes shake {
|
|
0%,
|
|
100% {
|
|
transform: translateX(0);
|
|
}
|
|
20%,
|
|
60% {
|
|
transform: translateX(-4px);
|
|
}
|
|
40%,
|
|
80% {
|
|
transform: translateX(4px);
|
|
}
|
|
}
|
|
|
|
.animate-shake {
|
|
animation: shake 0.4s ease-in-out;
|
|
}
|
|
|
|
/* Fade in animation (for popovers) */
|
|
@keyframes fade-in {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.animate-fade-in {
|
|
animation: fade-in 0.2s ease-out;
|
|
}
|
|
|
|
/* Slide in from right animation (for mobile views) */
|
|
@keyframes slide-in-from-right {
|
|
from {
|
|
transform: translateX(100%);
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.animate-slide-in-from-right {
|
|
animation: slide-in-from-right 0.3s ease-out;
|
|
}
|
|
|
|
/* Slide in from left animation (for sidebar) */
|
|
@keyframes slide-in-from-left {
|
|
from {
|
|
transform: translateX(-100%);
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.animate-slide-in-from-left {
|
|
animation: slide-in-from-left 0.3s ease-out;
|
|
}
|
|
|
|
/* Calendar swipe animations (subtler slide for view transitions) */
|
|
@keyframes cal-slide-in-right {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(30%);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes cal-slide-in-left {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-30%);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.animate-slide-in-right {
|
|
animation: cal-slide-in-right 0.25s ease-out;
|
|
}
|
|
|
|
.animate-slide-in-left {
|
|
animation: cal-slide-in-left 0.25s ease-out;
|
|
}
|
|
|
|
/* Reduced motion: respect user OS preference */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|
|
|
|
/* Disable backdrop-filter on mobile for performance */
|
|
@media (max-width: 767px) {
|
|
.mobile-backdrop {
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
}
|
|
}
|
|
|
|
/* TipTap Rich Text Editor */
|
|
.tiptap {
|
|
outline: none;
|
|
}
|
|
|
|
.tiptap p {
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.tiptap h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.tiptap h2 {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.tiptap ul {
|
|
list-style-type: disc;
|
|
padding-inline-start: 1.5rem;
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.tiptap ol {
|
|
list-style-type: decimal;
|
|
padding-inline-start: 1.5rem;
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.tiptap li {
|
|
margin: 0.125rem 0;
|
|
}
|
|
|
|
.tiptap blockquote {
|
|
border-inline-start: 3px solid var(--color-border);
|
|
padding-inline-start: 1rem;
|
|
margin: 0.5rem 0;
|
|
color: var(--color-muted-foreground);
|
|
}
|
|
|
|
.tiptap pre {
|
|
background-color: var(--color-muted);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 0.375rem;
|
|
padding: 0.75rem;
|
|
font-family: monospace;
|
|
font-size: 0.875rem;
|
|
overflow-x: auto;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.tiptap code {
|
|
background-color: var(--color-muted);
|
|
padding: 0.125rem 0.25rem;
|
|
border-radius: 0.25rem;
|
|
font-family: monospace;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.tiptap a {
|
|
color: var(--color-primary);
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tiptap img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.tiptap hr {
|
|
border: none;
|
|
border-top: 1px solid var(--color-border);
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.tiptap p.is-editor-empty:first-child::before {
|
|
content: attr(data-placeholder);
|
|
float: inline-start;
|
|
color: var(--color-muted-foreground);
|
|
pointer-events: none;
|
|
height: 0;
|
|
}
|
|
|
|
.tiptap .ProseMirror-selectednode img {
|
|
outline: none;
|
|
}
|
|
|
|
.tiptap table {
|
|
border-collapse: collapse;
|
|
margin: 0.5rem 0;
|
|
table-layout: fixed;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tiptap table td,
|
|
.tiptap table th {
|
|
border: 1px solid var(--color-border);
|
|
padding: 0.375rem 0.5rem;
|
|
vertical-align: top;
|
|
position: relative;
|
|
min-width: 1em;
|
|
}
|
|
|
|
.tiptap table th {
|
|
background-color: var(--color-muted) !important;
|
|
color: var(--color-foreground) !important;
|
|
font-weight: 600;
|
|
text-align: left;
|
|
}
|
|
|
|
.tiptap table p {
|
|
margin: 0;
|
|
}
|
|
|
|
.tiptap table .selectedCell::after {
|
|
background: rgba(99, 102, 241, 0.15);
|
|
content: "";
|
|
inset: 0;
|
|
pointer-events: none;
|
|
position: absolute;
|
|
z-index: 2;
|
|
}
|
|
|
|
.tiptap table .column-resize-handle {
|
|
background-color: var(--color-primary);
|
|
bottom: -2px;
|
|
pointer-events: none;
|
|
position: absolute;
|
|
right: -2px;
|
|
top: 0;
|
|
width: 4px;
|
|
}
|
|
|
|
.tiptap.resize-cursor {
|
|
cursor: col-resize;
|
|
}
|
|
|
|
/* Smooth halo applied to a setting row when the user clicks a sub-result in
|
|
the settings search. The element bg + a 10px box-shadow fill the row plus
|
|
a 10px ring of the same tint, then a 2px outline sits exactly at the outer
|
|
edge. Uses the primary theme color so it adapts to light/dark. */
|
|
@keyframes settings-search-pulse {
|
|
0% {
|
|
background-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
|
|
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent);
|
|
outline-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
|
|
}
|
|
30% {
|
|
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
|
box-shadow: 0 0 0 10px color-mix(in srgb, var(--color-primary) 12%, transparent);
|
|
outline-color: color-mix(in srgb, var(--color-primary) 60%, transparent);
|
|
}
|
|
100% {
|
|
background-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
|
|
box-shadow: 0 0 0 10px color-mix(in srgb, var(--color-primary) 0%, transparent);
|
|
outline-color: color-mix(in srgb, var(--color-primary) 0%, transparent);
|
|
}
|
|
}
|
|
|
|
.settings-search-highlight {
|
|
outline: 2px solid transparent;
|
|
outline-offset: 10px;
|
|
border-radius: 8px;
|
|
animation: settings-search-pulse 1.6s ease-in-out forwards;
|
|
}
|
|
|
|
/* RTL: mirror directional icons (chevrons/arrows) so prev/next, back/forward,
|
|
and panel-collapse affordances point the correct way in right-to-left layouts.
|
|
Tabler emits a `tabler-icon-<name>` class per icon, so we target the
|
|
directional ones only — vertical chevrons (up/down) are intentionally left. */
|
|
[dir="rtl"] .tabler-icon-chevron-left,
|
|
[dir="rtl"] .tabler-icon-chevron-right,
|
|
[dir="rtl"] .tabler-icon-chevrons-left,
|
|
[dir="rtl"] .tabler-icon-chevrons-right,
|
|
[dir="rtl"] .tabler-icon-arrow-left,
|
|
[dir="rtl"] .tabler-icon-arrow-right,
|
|
[dir="rtl"] .tabler-icon-arrow-big-left,
|
|
[dir="rtl"] .tabler-icon-arrow-big-right,
|
|
[dir="rtl"] .tabler-icon-layout-sidebar-left,
|
|
[dir="rtl"] .tabler-icon-layout-sidebar-right {
|
|
transform: scaleX(-1);
|
|
}
|
|
|
|
/* ==========================================================================
|
|
Interface layouts
|
|
--------------------------------------------------------------------------
|
|
`data-interface-layout` is set on <html> by the settings store
|
|
(`applyInterfaceLayout`). The default layout declares no overrides, so
|
|
everything below is additive and switched off by a single attribute.
|
|
Mountain View sets its own geometry - bar heights, rail widths, pill shapes,
|
|
row rhythm - while colour, iconography and type keep coming from the active
|
|
theme.
|
|
========================================================================== */
|
|
|
|
[data-interface-layout="mountain-view"] {
|
|
--mv-topbar-h: 64px;
|
|
--mv-rail-w: 72px;
|
|
--mv-rail-peek-w: 256px;
|
|
--mv-nav-row-h: 32px;
|
|
--mv-nav-radius: 9999px;
|
|
--mv-compose-h: 56px;
|
|
--mv-search-h: 48px;
|
|
--mv-search-radius: 8px;
|
|
--mv-popover-w: 352px;
|
|
--mv-popover-radius: 28px;
|
|
}
|
|
|
|
/* Search: a filled 48px field that lifts onto a card while focused, rather
|
|
than the outlined input the default layout uses. */
|
|
[data-interface-layout="mountain-view"] .mv-topbar input[data-search-input] {
|
|
height: var(--mv-search-h);
|
|
border-radius: var(--mv-search-radius);
|
|
border-color: transparent;
|
|
background-color: var(--color-muted);
|
|
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);
|
|
}
|
|
|
|
[data-interface-layout="mountain-view"] .mv-topbar input[data-search-input]:focus-visible {
|
|
background-color: var(--color-background);
|
|
border-color: transparent;
|
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.16), 0 4px 8px 3px rgb(0 0 0 / 0.08);
|
|
}
|
|
|
|
/* The filter button sits inside the field's trailing end: keep typed text and
|
|
the clear button clear of it. */
|
|
[data-interface-layout="mountain-view"] .mv-topbar input[data-search-input] {
|
|
padding-inline-end: 5.5rem;
|
|
}
|
|
|
|
[data-interface-layout="mountain-view"] .mv-topbar-search > div > form > button {
|
|
inset-inline-end: 3.25rem;
|
|
}
|
|
|
|
/* The magnifier sits on the field's baseline grid, one 48px hit area in. */
|
|
[data-interface-layout="mountain-view"] .mv-topbar-search > div > form > svg {
|
|
inset-inline-start: 1rem;
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
}
|
|
|
|
/* Folder rows: 32px pills flush with the rail's leading edge and rounded only
|
|
on the trailing side, so the selected label reads as one continuous shape
|
|
with the rail. The default layout's leading accent stripe goes away with it. */
|
|
[data-interface-layout="mountain-view"] [data-mv-nav-row] {
|
|
min-height: var(--mv-nav-row-h);
|
|
margin-inline-end: 12px;
|
|
padding-inline-end: 16px;
|
|
border-inline-start-width: 0;
|
|
border-start-start-radius: 0;
|
|
border-end-start-radius: 0;
|
|
border-start-end-radius: var(--mv-nav-radius);
|
|
border-end-end-radius: var(--mv-nav-radius);
|
|
}
|
|
|
|
/* Compose: a 56px pill, which shrinks to a circle with the rail. */
|
|
[data-interface-layout="mountain-view"] [data-mv-compose] {
|
|
height: var(--mv-compose-h);
|
|
border-radius: var(--mv-nav-radius);
|
|
}
|
|
|
|
/* The account popover: a wide card with a very round corner, rather
|
|
than the narrow menu the rail and the sidebar switchers drop. */
|
|
[data-interface-layout="mountain-view"] [data-mv-account-popover] {
|
|
width: var(--mv-popover-w);
|
|
border-radius: var(--mv-popover-radius);
|
|
}
|
|
|
|
/* The advanced filters drop from the search field with a small gap and take
|
|
its corner, so the two read as one control. Position and width come from
|
|
the field's own box, measured by FilterPanelHost. */
|
|
[data-interface-layout="mountain-view"] [data-mv-filter-panel] {
|
|
border-radius: var(--mv-search-radius);
|
|
}
|
|
|
|
/* The collapsed rail's hover peek. Only on desktop: below that breakpoint the
|
|
sidebar is already a drawer, opened by a tap rather than by a pointer that
|
|
does not exist. The width beats Tailwind's own `lg:w-full` on specificity,
|
|
which is what keeps the floating panel from filling the 72px column. */
|
|
@media (min-width: 1024px) {
|
|
[data-interface-layout="mountain-view"] [data-mv-rail-peek] {
|
|
width: var(--mv-rail-peek-w);
|
|
}
|
|
}
|