feat(email): restyle inbox, composer & reply to the Rowboat design system

- Inbox (gmail-shell): map the bespoke --gm-* palette onto the app design
  tokens (background/foreground/border/accent/primary), drop the unloaded
  "Inter" font override so it inherits the app font, and remove the redundant
  ".light .gmail-shell" block so theming follows :root/.dark.
- Composer: rebuilt on the shadcn Button/Input/Dialog primitives with app
  tokens; bring the AI describe-and-write bar and tone presets into reply mode.
- Auto-generated reply block (editor.css): retokenize the Gmail-blue pill
  buttons and compose surface onto app tokens; inherit the editor font.
- Add a Gmail-style "n" shortcut to open the composer from the inbox.
This commit is contained in:
hrsvrn 2026-06-26 03:19:47 +05:30 committed by arkml
parent 40d684684c
commit 824610f84f
3 changed files with 343 additions and 880 deletions

View file

@ -82,64 +82,40 @@
background-image: radial-gradient(circle, oklch(0.7 0 0 / 0.06) 1px, transparent 1px);
}
/* Inbox surface mapped onto the app's design tokens so it follows the same
palette, accent, and theme switching as the rest of Rowboat. Light/dark is
handled by the app tokens (:root / .dark), so no separate .light override. */
.gmail-shell {
--gm-bg: #0f0f12;
--gm-bg-card: #131317;
--gm-bg-input: #1c1c20;
--gm-bg-row-hover: #16161a;
--gm-bg-row-selected: #1a1620;
--gm-bg-row-selected-hover: #1d1825;
--gm-bg-iframe: #fafafa;
--gm-bg-pill: #1c1c20;
--gm-bg-pill-hover: #232328;
--gm-text: #e4e4e7;
--gm-text-strong: #fafafa;
--gm-text-muted: #71717a;
--gm-text-faint: #52525b;
--gm-text-body: #d4d4d8;
--gm-border: #1f1f24;
--gm-border-strong: #2e2e35;
--gm-accent: #a78bfa;
--gm-accent-hover: #b9a6ff;
--gm-accent-glow: rgba(167, 139, 250, 0.45);
--gm-accent-fg: #18181b;
--gm-icon-hover-bg: #1f1f24;
--gm-placeholder: #52525b;
--gm-bg: var(--background);
--gm-bg-card: var(--card-surface);
--gm-bg-input: var(--muted);
--gm-bg-row-hover: var(--accent);
--gm-bg-row-selected: var(--rowboat-wash);
--gm-bg-row-selected-hover: color-mix(in oklab, var(--primary) 18%, var(--background));
--gm-bg-iframe: #ffffff;
--gm-bg-pill: var(--background);
--gm-bg-pill-hover: var(--accent);
--gm-text: var(--foreground);
--gm-text-strong: var(--foreground);
--gm-text-muted: var(--muted-foreground);
--gm-text-faint: color-mix(in oklab, var(--muted-foreground) 70%, var(--background));
--gm-text-body: color-mix(in oklab, var(--foreground) 88%, var(--background));
--gm-border: var(--border);
--gm-border-strong: var(--rowboat-hairline);
--gm-accent: var(--primary);
--gm-accent-hover: color-mix(in oklab, var(--primary) 90%, transparent);
--gm-accent-glow: color-mix(in oklab, var(--primary) 40%, transparent);
--gm-accent-fg: var(--primary-foreground);
--gm-icon-hover-bg: var(--accent);
--gm-placeholder: var(--muted-foreground);
display: flex;
height: 100%;
min-height: 0;
width: 100%;
overflow: hidden;
background: var(--gm-bg);
color: var(--gm-text);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-feature-settings: "ss01", "cv11";
}
.light .gmail-shell {
--gm-bg: #ffffff;
--gm-bg-card: #ffffff;
--gm-bg-input: #f4f4f7;
--gm-bg-row-hover: #f7f7f9;
--gm-bg-row-selected: #f5f0ff;
--gm-bg-row-selected-hover: #ece4ff;
--gm-bg-iframe: #ffffff;
--gm-bg-pill: #ffffff;
--gm-bg-pill-hover: #f4f4f7;
--gm-text: #27272a;
--gm-text-strong: #09090b;
--gm-text-muted: #71717a;
--gm-text-faint: #a1a1aa;
--gm-text-body: #3f3f46;
--gm-border: #e4e4e7;
--gm-border-strong: #d4d4d8;
--gm-accent: #7c3aed;
--gm-accent-hover: #6d28d9;
--gm-accent-glow: rgba(124, 58, 237, 0.3);
--gm-accent-fg: #ffffff;
--gm-icon-hover-bg: #f4f4f7;
--gm-placeholder: #a1a1aa;
background: var(--background);
color: var(--foreground);
}
.gmail-main {
@ -326,7 +302,7 @@
}
.gmail-row-action-danger:hover {
color: #e8453c;
color: var(--destructive);
}
.gmail-row:hover {
@ -714,714 +690,78 @@
border-color: var(--gm-border-strong);
}
/* Standalone "new email" composer — centered modal popup */
.gmail-compose-overlay {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
background: rgba(0, 0, 0, 0.32);
}
/* Dark defaults (mirrors .gmail-shell); .light overrides below. */
.gmail-compose-modal,
.gmail-compose-card {
--gm-bg-card: #131317;
--gm-bg-input: #1c1c20;
--gm-bg-elevated: #1c1c20;
--gm-bg-pill: #1c1c20;
--gm-bg-pill-hover: #232328;
--gm-text: #e4e4e7;
--gm-text-strong: #fafafa;
--gm-text-muted: #71717a;
--gm-text-body: #d4d4d8;
--gm-border: #1f1f24;
--gm-border-strong: #2e2e35;
--gm-accent: #a78bfa;
--gm-accent-hover: #b9a6ff;
--gm-accent-fg: #18181b;
--gm-icon-hover-bg: #1f1f24;
--gm-placeholder: #52525b;
}
.light .gmail-compose-modal,
.light .gmail-compose-card {
--gm-bg-card: #ffffff;
--gm-bg-input: #f4f4f7;
--gm-bg-elevated: #ffffff;
--gm-bg-pill: #ffffff;
--gm-bg-pill-hover: #f4f4f7;
--gm-text: #27272a;
--gm-text-strong: #09090b;
--gm-text-muted: #71717a;
--gm-text-body: #3f3f46;
--gm-border: #e4e4e7;
--gm-border-strong: #d4d4d8;
--gm-accent: #7c3aed;
--gm-accent-hover: #6d28d9;
--gm-accent-fg: #ffffff;
--gm-icon-hover-bg: #f4f4f7;
--gm-placeholder: #a1a1aa;
}
.gmail-compose-modal {
display: flex;
flex-direction: column;
width: min(840px, 100%);
height: min(720px, calc(100vh - 64px));
max-height: calc(100vh - 64px);
border: 1px solid var(--gm-border-strong);
border-radius: 10px;
overflow: hidden;
background: var(--gm-bg-card);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35);
}
.gmail-compose-modal-header {
display: flex;
align-items: center;
gap: 10px;
height: 40px;
padding: 0 8px 0 14px;
background: var(--gm-bg-input);
color: var(--gm-text-body);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.01em;
text-transform: uppercase;
}
.gmail-compose-modal-header > span {
flex: 1;
}
.gmail-compose-modal .gmail-compose-editor {
flex: 1;
min-height: 160px;
max-height: none;
padding: 0 14px;
}
.gmail-compose-ai-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--gm-border);
}
.gmail-compose-ai-input {
flex: 1;
min-width: 0;
height: 30px;
padding: 0 10px;
border: 1px solid var(--gm-border-strong);
border-radius: 6px;
outline: none;
background: var(--gm-bg-input);
color: var(--gm-text);
font: inherit;
font-size: 12px;
}
.gmail-compose-ai-presets {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 0 12px 10px;
border-bottom: 1px solid var(--gm-border);
}
.gmail-compose-ai-presets button {
height: 24px;
padding: 0 10px;
border: 1px solid var(--gm-border-strong);
border-radius: 999px;
background: var(--gm-bg-pill);
color: var(--gm-text-muted);
font: inherit;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.gmail-compose-ai-presets button:hover:not(:disabled) {
background: var(--gm-bg-pill-hover);
border-color: var(--gm-accent);
color: var(--gm-accent);
}
.gmail-compose-ai-presets button:disabled,
.gmail-compose-ai-input:disabled {
opacity: 0.5;
cursor: default;
}
.gmail-compose-card {
max-width: 720px;
margin-left: 40px;
border: 1px solid var(--gm-border-strong);
border-radius: 8px;
overflow: hidden;
background: var(--gm-bg-card);
}
.gmail-compose-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 12px;
background: var(--gm-bg-input);
color: var(--gm-text-body);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.01em;
text-transform: uppercase;
}
.gmail-compose-header button,
.gmail-compose-link {
border: none;
background: transparent;
color: var(--gm-text-muted);
cursor: pointer;
}
.gmail-compose-line {
display: flex;
align-items: center;
gap: 8px;
min-height: 32px;
padding: 0 12px;
border-bottom: 1px solid var(--gm-border);
color: var(--gm-text-muted);
font-size: 12px;
}
.gmail-compose-line input {
min-width: 0;
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--gm-text);
font: inherit;
}
.gmail-compose-label {
flex: none;
min-width: 28px;
color: var(--gm-text-muted);
}
.gmail-compose-subject-input {
min-width: 0;
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--gm-text);
font: inherit;
}
/* Recipient (To / Cc / Bcc) rows with editable chips */
.gmail-recipient-row {
display: flex;
align-items: flex-start;
gap: 8px;
min-height: 34px;
padding: 5px 12px;
border-bottom: 1px solid var(--gm-border);
font-size: 13px;
}
.gmail-recipient-label {
flex: none;
min-width: 28px;
padding-top: 5px;
color: var(--gm-text-muted);
}
.gmail-recipient-field {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
position: relative;
}
.gmail-recipient-suggestions {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 30;
margin: 0;
padding: 6px;
list-style: none;
width: max-content;
min-width: 280px;
max-width: min(440px, 100%);
background: var(--gm-bg-elevated, #ffffff);
border: 1px solid var(--gm-border);
border-radius: 10px;
box-shadow:
0 1px 2px rgba(24, 24, 27, 0.08),
0 12px 32px rgba(24, 24, 27, 0.18);
max-height: 296px;
overflow-y: auto;
overscroll-behavior: contain;
transform-origin: top left;
animation: gmail-recipient-suggestions-in 110ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
@keyframes gmail-recipient-suggestions-in {
from {
opacity: 0;
transform: translateY(-2px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.gmail-recipient-suggestion {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
color: var(--gm-text);
cursor: pointer;
transition: background-color 80ms linear;
}
.gmail-recipient-suggestion:hover {
background: var(--gm-bg-pill-hover);
}
.gmail-recipient-suggestion.is-active {
background: rgba(99, 142, 255, 0.18);
}
.gmail-recipient-suggestion-avatar {
flex: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
color: #fff;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.2px;
text-transform: uppercase;
}
.gmail-recipient-suggestion-text {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-width: 0;
line-height: 1.25;
}
.gmail-recipient-suggestion-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.gmail-recipient-suggestion-email {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11.5px;
color: var(--gm-text-muted);
margin-top: 1px;
}
.gmail-recipient-suggestion-match {
background: transparent;
color: inherit;
font-weight: 700;
padding: 0;
}
.gmail-recipient-chip {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
height: 24px;
padding: 0 4px 0 10px;
border-radius: 12px;
background: var(--gm-bg-pill);
color: var(--gm-text);
font-size: 12px;
line-height: 1;
}
.gmail-recipient-chip-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 240px;
}
.gmail-recipient-chip-remove {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--gm-text-muted);
font-size: 14px;
line-height: 1;
cursor: pointer;
}
.gmail-recipient-chip-remove:hover {
background: var(--gm-bg-pill-hover);
color: var(--gm-text);
}
.gmail-recipient-input {
flex: 1 1 80px;
min-width: 80px;
height: 24px;
border: none;
outline: none;
background: transparent;
color: var(--gm-text);
font: inherit;
font-size: 13px;
}
.gmail-recipient-trailing {
flex: none;
padding-top: 5px;
}
.gmail-recipient-toggles {
display: flex;
gap: 10px;
}
.gmail-recipient-toggles button {
border: none;
background: transparent;
color: var(--gm-text-muted);
font: inherit;
font-size: 12px;
cursor: pointer;
}
.gmail-recipient-toggles button:hover {
color: var(--gm-text);
text-decoration: underline;
}
.gmail-compose-toolbar {
display: flex;
align-items: center;
gap: 2px;
flex: 1;
min-width: 0;
justify-content: flex-start;
padding-left: 10px;
margin-left: 2px;
border-left: 1px solid var(--gm-border-strong);
}
.gmail-compose-link-popover {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--gm-border);
background: var(--gm-bg-input);
}
.gmail-compose-link-popover input {
flex: 1;
min-width: 0;
height: 28px;
padding: 0 8px;
border: 1px solid var(--gm-border-strong);
border-radius: 4px;
background: var(--gm-bg-card);
color: var(--gm-text);
font: inherit;
font-size: 12px;
outline: none;
}
.gmail-compose-link-popover input:focus {
border-color: var(--gm-accent);
}
.gmail-compose-link-popover button {
height: 26px;
padding: 0 10px;
border: 1px solid var(--gm-border-strong);
border-radius: 4px;
background: var(--gm-bg-pill);
color: var(--gm-text);
font: inherit;
font-size: 12px;
cursor: pointer;
}
.gmail-compose-link-popover button:hover {
background: var(--gm-bg-pill-hover);
}
.gmail-compose-link-popover-apply {
background: var(--gm-accent) !important;
border-color: var(--gm-accent) !important;
color: var(--gm-accent-fg) !important;
font-weight: 600;
}
.gmail-compose-link-popover-apply:hover {
background: var(--gm-accent-hover) !important;
border-color: var(--gm-accent-hover) !important;
}
.gmail-compose-tool {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--gm-text-muted);
cursor: pointer;
transition: background 120ms ease, color 120ms ease;
}
.gmail-compose-tool:hover:not(:disabled) {
background: var(--gm-bg-pill-hover);
color: var(--gm-text);
}
.gmail-compose-tool:disabled {
opacity: 0.4;
cursor: default;
}
.gmail-compose-tool.is-active {
background: var(--gm-bg-pill-hover);
color: var(--gm-accent);
}
.gmail-compose-tool-sep {
display: inline-block;
width: 1px;
height: 18px;
margin: 0 6px;
background: var(--gm-border-strong);
}
.gmail-compose-editor {
display: block;
width: 100%;
max-height: 360px;
overflow-y: auto;
}
.gmail-compose-content {
/* Email composer body — Tiptap content, styled to the app's design system. */
.compose-content {
outline: none;
min-height: 120px;
padding: 12px;
background: transparent;
color: var(--gm-text);
font: 13px/1.55 inherit;
color: var(--foreground);
font-size: 0.875rem;
line-height: 1.6;
}
.gmail-compose-content p {
.compose-content p {
margin: 0;
}
.gmail-compose-content p + p,
.gmail-compose-content p + ul,
.gmail-compose-content p + ol,
.gmail-compose-content p + blockquote {
.compose-content p + p,
.compose-content p + ul,
.compose-content p + ol,
.compose-content p + blockquote {
margin-top: 8px;
}
.gmail-compose-content ul,
.gmail-compose-content ol {
.compose-content ul,
.compose-content ol {
margin: 0;
padding-left: 22px;
}
.gmail-compose-content ul {
.compose-content ul {
list-style: disc;
}
.gmail-compose-content ol {
.compose-content ol {
list-style: decimal;
}
.gmail-compose-content li {
.compose-content li {
margin: 2px 0;
}
.gmail-compose-content li > p {
.compose-content li > p {
margin: 0;
}
.gmail-compose-content blockquote {
.compose-content blockquote {
margin: 4px 0;
padding-left: 12px;
border-left: 2px solid var(--gm-border-strong);
color: var(--gm-text-muted);
border-left: 2px solid var(--border);
color: var(--muted-foreground);
}
.gmail-compose-content a {
color: var(--gm-accent);
.compose-content a {
color: var(--primary);
text-decoration: underline;
}
.gmail-compose-content code {
.compose-content code {
padding: 1px 4px;
border-radius: 3px;
background: var(--gm-bg-pill-hover);
background: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
font-size: 0.8125rem;
}
.gmail-compose-content p.is-editor-empty:first-child::before {
.compose-content p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--gm-placeholder);
color: var(--muted-foreground);
float: left;
height: 0;
pointer-events: none;
}
.gmail-compose-attachments {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 12px 0;
}
.gmail-compose-attachment {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 240px;
padding: 4px 8px;
border: 1px solid var(--gm-border);
border-radius: 6px;
background: var(--gm-bg-pill);
font-size: 12px;
color: var(--gm-text);
}
.gmail-compose-attachment-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gmail-compose-attachment-size {
color: var(--gm-text-muted);
flex-shrink: 0;
}
.gmail-compose-attachment-remove {
border: none;
background: transparent;
color: var(--gm-text-muted);
cursor: pointer;
font-size: 15px;
line-height: 1;
padding: 0 0 0 2px;
flex-shrink: 0;
}
.gmail-compose-attachment-remove:hover {
color: var(--gm-text);
}
.gmail-compose-actions {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-top: 1px solid var(--gm-border);
}
.gmail-compose-actions-primary {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.gmail-refine-button {
display: inline-flex;
align-items: center;
gap: 8px;
box-sizing: border-box;
height: 30px;
padding: 0 14px;
border: 1px solid var(--gm-border-strong);
border-radius: 6px;
background: var(--gm-bg-pill);
color: var(--gm-text);
font: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.gmail-refine-button:hover {
background: var(--gm-bg-pill-hover);
border-color: var(--gm-accent);
color: var(--gm-accent);
}
.gmail-send-button {
display: inline-flex;
align-items: center;
gap: 8px;
box-sizing: border-box;
height: 30px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 6px;
background: var(--gm-accent);
color: var(--gm-accent-fg);
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.gmail-send-button:hover {
background: var(--gm-accent-hover);
}
.gmail-empty-state {
display: flex;
align-items: center;

View file

@ -1,5 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Archive, Bold, CheckCheck, Forward, Italic, Link as LinkIcon, List, ListOrdered, LoaderIcon, Mail, Paperclip, Quote, Redo2, RefreshCw, Reply, ReplyAll, Search, Send, Sparkles, SquarePen, Strikethrough, Trash2, Undo2 } from 'lucide-react'
import { Archive, Bold, CheckCheck, Forward, Italic, Link as LinkIcon, List, ListOrdered, LoaderIcon, Mail, Paperclip, Quote, Redo2, RefreshCw, Reply, ReplyAll, Search, Send, Sparkles, SquarePen, Strikethrough, Trash2, Undo2, X } from 'lucide-react'
import { useEditor, EditorContent, type Editor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
@ -9,6 +9,9 @@ import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
import { useTheme } from '@/contexts/theme-context'
import { SettingsDialog } from '@/components/settings-dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
type GmailThread = blocks.GmailThread
type GmailThreadMessage = blocks.GmailThreadMessage
@ -539,9 +542,11 @@ function ComposeToolbarButton({
children: React.ReactNode
}) {
return (
<button
<Button
type="button"
className={cn('gmail-compose-tool', isActive && 'is-active')}
variant="ghost"
size="icon-sm"
className={cn('size-7 text-muted-foreground', isActive && 'bg-accent text-foreground')}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
command()
@ -552,43 +557,47 @@ function ComposeToolbarButton({
title={label}
>
{children}
</button>
</Button>
)
}
function ComposeToolbar({ editor, onOpenLink }: { editor: Editor; onOpenLink: () => void }) {
return (
<div className="gmail-compose-toolbar">
<button
<div className="flex min-w-0 flex-1 items-center gap-0.5 border-l border-border pl-2.5">
<Button
type="button"
className="gmail-compose-tool"
variant="ghost"
size="icon-sm"
className="size-7 text-muted-foreground"
onMouseDown={(event) => event.preventDefault()}
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
aria-label="Undo"
title="Undo"
>
<Undo2 size={14} />
</button>
<button
<Undo2 className="size-3.5" />
</Button>
<Button
type="button"
className="gmail-compose-tool"
variant="ghost"
size="icon-sm"
className="size-7 text-muted-foreground"
onMouseDown={(event) => event.preventDefault()}
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
aria-label="Redo"
title="Redo"
>
<Redo2 size={14} />
</button>
<span className="gmail-compose-tool-sep" />
<Redo2 className="size-3.5" />
</Button>
<span className="mx-1.5 h-4 w-px bg-border" />
<ComposeToolbarButton
editor={editor}
command={() => editor.chain().focus().toggleBold().run()}
isActive={editor.isActive('bold')}
label="Bold"
>
<Bold size={14} />
<Bold className="size-3.5" />
</ComposeToolbarButton>
<ComposeToolbarButton
editor={editor}
@ -596,7 +605,7 @@ function ComposeToolbar({ editor, onOpenLink }: { editor: Editor; onOpenLink: ()
isActive={editor.isActive('italic')}
label="Italic"
>
<Italic size={14} />
<Italic className="size-3.5" />
</ComposeToolbarButton>
<ComposeToolbarButton
editor={editor}
@ -604,16 +613,16 @@ function ComposeToolbar({ editor, onOpenLink }: { editor: Editor; onOpenLink: ()
isActive={editor.isActive('strike')}
label="Strikethrough"
>
<Strikethrough size={14} />
<Strikethrough className="size-3.5" />
</ComposeToolbarButton>
<span className="gmail-compose-tool-sep" />
<span className="mx-1.5 h-4 w-px bg-border" />
<ComposeToolbarButton
editor={editor}
command={() => editor.chain().focus().toggleBulletList().run()}
isActive={editor.isActive('bulletList')}
label="Bulleted list"
>
<List size={14} />
<List className="size-3.5" />
</ComposeToolbarButton>
<ComposeToolbarButton
editor={editor}
@ -621,7 +630,7 @@ function ComposeToolbar({ editor, onOpenLink }: { editor: Editor; onOpenLink: ()
isActive={editor.isActive('orderedList')}
label="Numbered list"
>
<ListOrdered size={14} />
<ListOrdered className="size-3.5" />
</ComposeToolbarButton>
<ComposeToolbarButton
editor={editor}
@ -629,20 +638,22 @@ function ComposeToolbar({ editor, onOpenLink }: { editor: Editor; onOpenLink: ()
isActive={editor.isActive('blockquote')}
label="Quote"
>
<Quote size={14} />
<Quote className="size-3.5" />
</ComposeToolbarButton>
<span className="gmail-compose-tool-sep" />
<button
<span className="mx-1.5 h-4 w-px bg-border" />
<Button
type="button"
className={cn('gmail-compose-tool', editor.isActive('link') && 'is-active')}
variant="ghost"
size="icon-sm"
className={cn('size-7 text-muted-foreground', editor.isActive('link') && 'bg-accent text-foreground')}
onMouseDown={(event) => event.preventDefault()}
onClick={onOpenLink}
aria-label="Link"
aria-pressed={editor.isActive('link')}
title="Link"
>
<LinkIcon size={14} />
</button>
<LinkIcon className="size-3.5" />
</Button>
</div>
)
}
@ -678,7 +689,7 @@ function HighlightedText({ text, query }: { text: string; query: string }) {
return (
<>
{text.slice(0, idx)}
<mark className="gmail-recipient-suggestion-match">{text.slice(idx, idx + q.length)}</mark>
<mark className="bg-transparent p-0 font-bold text-inherit">{text.slice(idx, idx + q.length)}</mark>
{text.slice(idx + q.length)}
</>
)
@ -812,26 +823,30 @@ function RecipientField({
const showSuggestions = isFocused && suggestions.length > 0
return (
<div className="gmail-recipient-row">
<span className="gmail-recipient-label">{label}</span>
<div className="gmail-recipient-field" ref={fieldRef}>
<div className="flex items-start gap-2 border-b border-border px-3 py-1.5 text-sm">
<span className="min-w-7 pt-1.5 text-muted-foreground">{label}</span>
<div className="relative flex min-w-0 flex-1 flex-wrap items-center gap-1" ref={fieldRef}>
{value.map((token, index) => (
<span key={`${token}-${index}`} className="gmail-recipient-chip" title={extractAddress(token)}>
<span className="gmail-recipient-chip-label">{recipientLabel(token)}</span>
<span
key={`${token}-${index}`}
className="inline-flex h-6 max-w-full items-center gap-1 rounded-md bg-muted pl-2 pr-1 text-xs text-foreground"
title={extractAddress(token)}
>
<span className="max-w-[240px] truncate">{recipientLabel(token)}</span>
<button
type="button"
className="gmail-recipient-chip-remove"
className="inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label={`Remove ${extractAddress(token)}`}
onMouseDown={(event) => event.preventDefault()}
onClick={() => onChange(value.filter((_, idx) => idx !== index))}
>
×
<X className="size-3" />
</button>
</span>
))}
<input
ref={inputRef}
className="gmail-recipient-input"
className="h-6 min-w-[80px] flex-1 border-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
value={draft}
onChange={(event) => setDraft(event.target.value)}
onKeyDown={onKeyDown}
@ -854,7 +869,11 @@ function RecipientField({
}}
/>
{showSuggestions && (
<ul className="gmail-recipient-suggestions" role="listbox" ref={listRef}>
<ul
className="absolute left-0 top-[calc(100%+6px)] z-30 m-0 max-h-[296px] w-max min-w-[280px] max-w-[min(440px,100%)] list-none overflow-y-auto overscroll-contain rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
role="listbox"
ref={listRef}
>
{suggestions.map((c, idx) => {
const hue = contactHue(c.email)
return (
@ -862,7 +881,10 @@ function RecipientField({
key={c.email}
role="option"
aria-selected={idx === activeIndex}
className={cn('gmail-recipient-suggestion', idx === activeIndex && 'is-active')}
className={cn(
'flex cursor-pointer items-center gap-2.5 rounded-sm px-2.5 py-1.5 text-sm transition-colors',
idx === activeIndex ? 'bg-accent text-accent-foreground' : 'hover:bg-accent/60'
)}
onMouseDown={(event) => {
// Prevent input blur before click fires.
event.preventDefault()
@ -871,18 +893,18 @@ function RecipientField({
onMouseEnter={() => setActiveIndex(idx)}
>
<span
className="gmail-recipient-suggestion-avatar"
className="inline-flex size-6 flex-none items-center justify-center rounded-full text-xs font-semibold uppercase text-white"
style={{ background: `hsl(${hue}, 60%, 42%)` }}
aria-hidden="true"
>
{contactInitial(c)}
</span>
<span className="gmail-recipient-suggestion-text">
<span className="gmail-recipient-suggestion-name">
<span className="flex min-w-0 flex-1 flex-col leading-tight">
<span className="truncate font-medium">
<HighlightedText text={c.name || c.email} query={queryShown} />
</span>
{c.name && (
<span className="gmail-recipient-suggestion-email">
<span className="mt-0.5 truncate text-xs text-muted-foreground">
<HighlightedText text={c.email} query={queryShown} />
</span>
)}
@ -893,7 +915,7 @@ function RecipientField({
</ul>
)}
</div>
{trailing && <div className="gmail-recipient-trailing">{trailing}</div>}
{trailing && <div className="flex-none pt-1.5">{trailing}</div>}
</div>
)
}
@ -1005,7 +1027,7 @@ const ComposeBox = memo(function ComposeBox({
}),
],
editorProps: {
attributes: { class: 'gmail-compose-content' },
attributes: { class: 'compose-content' },
},
content: initialContent,
})
@ -1054,16 +1076,17 @@ const ComposeBox = memo(function ComposeBox({
}
// The signed-in account's display name, used to sign off AI-generated emails.
// Loaded in every mode (new, reply, forward) since the AI writer is available
// throughout and needs a name to sign off with.
const [selfName, setSelfName] = useState<string>('')
const selfFirstName = useMemo(() => firstNameFromDisplayName(selfName), [selfName])
useEffect(() => {
if (!isNew) return
let cancelled = false
window.ipc.invoke('gmail:getAccountName', {})
.then((res) => { if (!cancelled && res?.name) setSelfName(res.name) })
.catch(() => {})
return () => { cancelled = true }
}, [isNew])
}, [])
const [aiPrompt, setAiPrompt] = useState('')
const [generating, setGenerating] = useState(false)
@ -1101,6 +1124,18 @@ const ComposeBox = memo(function ComposeBox({
if (!instruction.trim()) { toast('Describe what to write.', 'error'); return }
system = AI_GENERATE_SYSTEM
const ctx: string[] = []
// When replying or forwarding, give the model the thread it's responding
// to (oldest first) so the draft is on-topic and references the right
// points. New emails have no thread and skip this.
if (thread) {
const threadText = thread.messages
.map((message, index) => {
const header = message.from ? `From: ${message.from}\n` : ''
return `--- Message ${index + 1} ---\n${header}${(message.body || '').trim()}`
})
.join('\n\n')
ctx.push(`This is a ${modeLabel.toLowerCase()} to the following email thread (oldest first):\n${threadText}`)
}
// Use the recipients' names (from the contacts picker) so the AI can
// address them naturally; fall back to the address when there's no name.
const recipientNames = toList
@ -1111,7 +1146,7 @@ const ComposeBox = memo(function ComposeBox({
.filter(Boolean)
if (recipientNames.length) ctx.push(`Recipient(s): ${recipientNames.join(', ')}`)
if (selfFirstName) ctx.push(`Sender's first name (sign off as this): ${selfFirstName}`)
if (subject.trim()) ctx.push(`Desired subject hint: ${subject.trim()}`)
if (isNew && subject.trim()) ctx.push(`Desired subject hint: ${subject.trim()}`)
if (current) ctx.push(`Existing draft (revise or build on it):\n${current}`)
prompt = `${ctx.length ? ctx.join('\n') + '\n\n' : ''}Instruction: ${instruction.trim()}`
} else {
@ -1135,15 +1170,18 @@ const ComposeBox = memo(function ComposeBox({
// draft lands in the editor's undo history and the toolbar's Undo reverts it.
if (aiMode === 'generate') {
const { subject: generatedSubject, body } = parseGeneratedEmail(res.text)
if (generatedSubject) setSubject(generatedSubject)
// Only new emails take the AI's subject; replies/forwards keep their
// derived "Re:"/"Fwd:" subject (and don't expose a subject field).
if (generatedSubject && isNew) setSubject(generatedSubject)
// Always sign off with the account first name, even if the model omitted it.
const signed = ensureSignature(body, selfFirstName)
editor.chain().focus().selectAll().insertContent(plainTextToHtml(signed)).run()
setHasGenerated(true)
} else {
// Rewrites also regenerate the subject so it stays in sync with the body.
// Rewrites also regenerate the subject so it stays in sync with the body —
// but only for new emails, to preserve a reply/forward's threaded subject.
const { subject: rewrittenSubject, body } = parseGeneratedEmail(res.text)
if (rewrittenSubject) setSubject(rewrittenSubject)
if (rewrittenSubject && isNew) setSubject(rewrittenSubject)
editor.chain().focus().selectAll().insertContent(plainTextToHtml(body)).run()
}
} catch (err) {
@ -1301,84 +1339,100 @@ const ComposeBox = memo(function ComposeBox({
window.dispatchEvent(new Event('email-block:draft-with-assistant'))
}
const card = (
<div
className={isNew ? 'gmail-compose-modal' : 'gmail-compose-card'}
onClick={isNew ? (event) => event.stopPropagation() : undefined}
>
<div className={isNew ? 'gmail-compose-modal-header' : 'gmail-compose-header'}>
<span>{modeLabel}</span>
<button
type="button"
className={isNew ? 'gmail-icon-button' : undefined}
onClick={onClose}
aria-label="Close compose"
>×</button>
</div>
const inner = (
<>
<RecipientField
label="To"
value={toList}
onChange={setToList}
autoFocus={isNew || mode === 'forward'}
trailing={
<div className="gmail-recipient-toggles">
{!showCc && <button type="button" onClick={() => setShowCc(true)}>Cc</button>}
{!showBcc && <button type="button" onClick={() => setShowBcc(true)}>Bcc</button>}
<div className="flex gap-2.5">
{!showCc && (
<button
type="button"
className="text-xs text-muted-foreground hover:text-foreground hover:underline"
onClick={() => setShowCc(true)}
>Cc</button>
)}
{!showBcc && (
<button
type="button"
className="text-xs text-muted-foreground hover:text-foreground hover:underline"
onClick={() => setShowBcc(true)}
>Bcc</button>
)}
</div>
}
/>
{showCc && <RecipientField label="Cc" value={ccList} onChange={setCcList} />}
{showBcc && <RecipientField label="Bcc" value={bccList} onChange={setBccList} />}
{isNew && (
<>
<div className="gmail-compose-ai-bar">
<input
className="gmail-compose-ai-input"
value={aiPrompt}
onChange={(event) => setAiPrompt(event.target.value)}
placeholder={hasGenerated
? 'Edit the draft (e.g. add a line about…, remove the last paragraph)…'
: 'Describe the email and let AI write it…'}
disabled={generating}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault()
void runAiBar()
}
}}
/>
<button
type="button"
className="gmail-refine-button"
onClick={() => { void runAiBar() }}
disabled={generating}
title={hasGenerated ? 'Apply this edit to the draft' : 'Write a draft with AI'}
>
{generating ? <LoaderIcon size={15} className="animate-spin" /> : <Sparkles size={15} />}
{generating
? (hasGenerated ? 'Editing…' : 'Writing…')
: (hasGenerated ? 'Edit' : 'Write')}
</button>
</div>
<div className="gmail-compose-ai-presets">
<button type="button" onClick={() => { void runAi('Improve the clarity, grammar, and flow of this email while preserving its meaning.', 'rewrite') }} disabled={generating}>Improve</button>
{TONE_PRESETS.map((preset) => (
<button key={preset.key} type="button" onClick={() => { void runAi(preset.instruction, 'rewrite') }} disabled={generating}>{preset.label}</button>
))}
</div>
</>
)}
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
<Input
className="h-8"
value={aiPrompt}
onChange={(event) => setAiPrompt(event.target.value)}
placeholder={hasGenerated
? 'Edit the draft (e.g. add a line about…, remove the last paragraph)…'
: isNew
? 'Describe the email and let AI write it…'
: 'Describe your reply and let AI write it…'}
disabled={generating}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault()
void runAiBar()
}
}}
/>
<Button
type="button"
size="sm"
onClick={() => { void runAiBar() }}
disabled={generating}
title={hasGenerated ? 'Apply this edit to the draft' : 'Write a draft with AI'}
>
{generating ? <LoaderIcon className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
{generating
? (hasGenerated ? 'Editing…' : 'Writing…')
: (hasGenerated ? 'Edit' : 'Write')}
</Button>
</div>
<div className="flex flex-wrap gap-1.5 border-b border-border px-3 pb-2.5">
<Button
type="button"
variant="outline"
size="xs"
className="rounded-full"
onClick={() => { void runAi('Improve the clarity, grammar, and flow of this email while preserving its meaning.', 'rewrite') }}
disabled={generating}
>Improve</Button>
{TONE_PRESETS.map((preset) => (
<Button
key={preset.key}
type="button"
variant="outline"
size="xs"
className="rounded-full"
onClick={() => { void runAi(preset.instruction, 'rewrite') }}
disabled={generating}
>{preset.label}</Button>
))}
</div>
{(isNew || mode === 'forward') && (
<div className="gmail-compose-line">
<span className="gmail-compose-label">Subject</span>
<div className="flex min-h-8 items-center gap-2 border-b border-border px-3 text-sm">
<span className="text-muted-foreground">Subject</span>
<input
className="gmail-compose-subject-input"
className="min-w-0 flex-1 border-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
value={subject}
onChange={(event) => setSubject(event.target.value)}
/>
</div>
)}
<EditorContent editor={editor} className="gmail-compose-editor" />
<EditorContent
editor={editor}
className={cn('w-full overflow-y-auto', isNew ? 'min-h-0 flex-1' : 'max-h-[360px]')}
/>
<input
ref={fileInputRef}
type="file"
@ -1390,26 +1444,34 @@ const ComposeBox = memo(function ComposeBox({
}}
/>
{attachments.length > 0 && (
<div className="gmail-compose-attachments">
<div className="flex flex-wrap gap-1.5 px-3 pt-2">
{attachments.map((att) => (
<div key={att.id} className="gmail-compose-attachment" title={att.filename}>
<Paperclip size={13} />
<span className="gmail-compose-attachment-name">{att.filename}</span>
<span className="gmail-compose-attachment-size">{formatAttachmentSize(att.size)}</span>
<div
key={att.id}
className="inline-flex max-w-60 items-center gap-1.5 rounded-md border border-border bg-muted/50 px-2 py-1 text-xs text-foreground"
title={att.filename}
>
<Paperclip className="size-3 shrink-0" />
<span className="truncate">{att.filename}</span>
<span className="shrink-0 text-muted-foreground">{formatAttachmentSize(att.size)}</span>
<button
type="button"
className="gmail-compose-attachment-remove"
className="shrink-0 rounded-sm text-muted-foreground transition-colors hover:text-foreground"
onClick={() => removeAttachment(att.id)}
aria-label={`Remove ${att.filename}`}
>×</button>
><X className="size-3" /></button>
</div>
))}
</div>
)}
{linkOpen && (
<div className="gmail-compose-link-popover" onMouseDown={(event) => event.preventDefault()}>
<input
<div
className="flex items-center gap-1.5 border-t border-border bg-muted/30 px-3 py-2"
onMouseDown={(event) => event.preventDefault()}
>
<Input
ref={linkInputRef}
className="h-7 flex-1 text-xs"
value={linkUrl}
onChange={(event) => setLinkUrl(event.target.value)}
placeholder="https://example.com"
@ -1423,58 +1485,99 @@ const ComposeBox = memo(function ComposeBox({
}
}}
/>
<button type="button" className="gmail-compose-link-popover-apply" onClick={applyLink}>Apply</button>
<button type="button" className="gmail-compose-link-popover-cancel" onClick={cancelLink}>Cancel</button>
<Button type="button" size="xs" onClick={applyLink}>Apply</Button>
<Button type="button" variant="outline" size="xs" onClick={cancelLink}>Cancel</Button>
</div>
)}
<div className="gmail-compose-actions">
<div className="gmail-compose-actions-primary">
<button
<div className="flex items-center gap-3 border-t border-border px-3 py-2.5">
<div className="flex shrink-0 items-center gap-2">
<Button
type="button"
className="gmail-send-button"
size="sm"
onClick={() => { void sendInGmail() }}
disabled={sending}
title={isNew ? 'Send this email via Gmail' : 'Send this reply via Gmail'}
>
{sending ? <LoaderIcon size={15} className="animate-spin" /> : <Send size={15} />}
{sending ? <LoaderIcon className="size-4 animate-spin" /> : <Send className="size-4" />}
{sending ? 'Sending…' : 'Send'}
</button>
<button
</Button>
<Button
type="button"
className="gmail-refine-button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={sending}
title="Attach files"
>
<Paperclip size={15} />
<Paperclip className="size-4" />
Attach
</button>
</Button>
{thread && (
<button
<Button
type="button"
className="gmail-refine-button"
variant="outline"
size="sm"
onClick={refineWithCopilot}
title="Refine this draft with Copilot"
>
<Sparkles size={15} />
<Sparkles className="size-4" />
Refine
</button>
</Button>
)}
</div>
{editor && <ComposeToolbar editor={editor} onOpenLink={openLink} />}
<button type="button" className="gmail-compose-link" onClick={onClose}>Discard</button>
<Button type="button" variant="ghost" size="sm" className="text-muted-foreground" onClick={onClose}>
Discard
</Button>
</div>
</div>
</>
)
if (isNew) {
return (
<div className="gmail-compose-overlay" onClick={onClose}>
{card}
</div>
<Dialog open onOpenChange={(open) => { if (!open) onClose() }}>
<DialogContent
showCloseButton={false}
aria-describedby={undefined}
className="flex h-[min(720px,calc(100vh-4rem))] flex-col gap-0 overflow-hidden p-0 font-sans sm:max-w-[840px]"
>
<div className="flex h-10 shrink-0 items-center gap-2 border-b border-border px-3">
<DialogTitle className="flex-1 text-sm font-medium text-foreground">{modeLabel}</DialogTitle>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="size-7 text-muted-foreground"
onClick={onClose}
aria-label="Close compose"
>
<X className="size-4" />
</Button>
</div>
{inner}
</DialogContent>
</Dialog>
)
}
return card
return (
<div className="ml-10 max-w-[720px] overflow-hidden rounded-lg border border-border bg-background font-sans">
<div className="flex h-8 items-center justify-between border-b border-border px-3">
<span className="text-xs font-medium text-muted-foreground">{modeLabel}</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-muted-foreground"
onClick={onClose}
aria-label="Close compose"
>
<X className="size-3.5" />
</Button>
</div>
{inner}
</div>
)
})
function ThreadDetail({
@ -1691,6 +1794,30 @@ export function EmailView({ initialThreadId, threadIdVersion }: EmailViewProps =
}
}, [])
// Gmail-style "n" to start a new message. EmailView only mounts while the
// inbox is open, so this is naturally scoped to that view. Ignored while
// typing in any field or when a dialog (compose/settings) is already up.
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'n' || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.isComposing) return
if (composeOpen || settingsOpen) return
const target = e.target as HTMLElement | null
if (
target &&
(target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable)
) {
return
}
e.preventDefault()
setComposeOpen(true)
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [composeOpen, settingsOpen])
useEffect(() => { persistedImportant = important }, [important])
useEffect(() => { persistedOther = other }, [other])

View file

@ -1551,9 +1551,9 @@
/* Compose / draft box */
.tiptap-editor .ProseMirror .email-gmail-compose {
margin-top: 4px;
border: 1px solid color-mix(in srgb, var(--foreground) 15%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
background: var(--background);
overflow: hidden;
}
@ -1562,13 +1562,13 @@
align-items: center;
gap: 6px;
padding: 8px 12px 6px;
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
border-bottom: 1px solid var(--border);
}
.tiptap-editor .ProseMirror .email-gmail-compose-to-label {
font-size: 12px;
font-weight: 500;
color: color-mix(in srgb, var(--foreground) 45%, transparent);
color: var(--muted-foreground);
}
.tiptap-editor .ProseMirror .email-gmail-compose-to-addr {
@ -1592,7 +1592,7 @@
}
.tiptap-editor .ProseMirror .email-gmail-compose-body::placeholder {
color: color-mix(in srgb, var(--foreground) 35%, transparent);
color: var(--muted-foreground);
}
.tiptap-editor .ProseMirror .email-gmail-compose-footer {
@ -1600,7 +1600,7 @@
align-items: center;
gap: 8px;
padding: 8px 12px;
border-top: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
border-top: 1px solid var(--border);
}
/* Action buttons */
@ -1615,35 +1615,33 @@
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 16px;
font-size: 13px;
padding: 7px 12px;
font-size: 0.8125rem;
font-weight: 500;
color: color-mix(in srgb, var(--foreground) 60%, transparent);
color: var(--foreground);
background: transparent;
border: 1px solid color-mix(in srgb, var(--foreground) 20%, transparent);
border-radius: 18px;
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
transition: background-color 0.15s ease, box-shadow 0.15s ease;
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
width: fit-content;
letter-spacing: 0.01em;
}
.tiptap-editor .ProseMirror .email-gmail-btn:hover {
background: color-mix(in srgb, var(--foreground) 6%, transparent);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
color: var(--foreground);
background: var(--accent);
color: var(--accent-foreground);
}
.tiptap-editor .ProseMirror .email-gmail-btn-primary {
color: #fff;
background: #1a73e8;
border-color: #1a73e8;
color: var(--primary-foreground);
background: var(--primary);
border-color: var(--primary);
}
.tiptap-editor .ProseMirror .email-gmail-btn-primary:hover {
background: #1765cc;
box-shadow: 0 1px 2px 0 rgba(26, 115, 232, 0.45), 0 1px 3px 1px rgba(26, 115, 232, 0.3);
color: #fff;
background: color-mix(in oklab, var(--primary) 90%, transparent);
border-color: color-mix(in oklab, var(--primary) 90%, transparent);
color: var(--primary-foreground);
}
.tiptap-editor .ProseMirror .email-block-error,
@ -1690,9 +1688,8 @@
/* ---- Emails inbox block (language-emails) ---- */
.tiptap-editor .ProseMirror .email-inbox-card {
font-family: 'Google Sans', Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
}
/* No font-family override inherit the editor's app font (ui-sans-serif /
system-ui) so the inbox block matches the rest of the Rowboat UI. */
.tiptap-editor .ProseMirror .email-inbox-title {
font-size: 14px;
@ -1768,7 +1765,6 @@
.tiptap-editor .ProseMirror .email-inbox-sender {
font-size: 14px;
font-weight: 500;
font-family: 'Google Sans', Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
color: var(--foreground);
white-space: nowrap;
overflow: hidden;