From 824610f84fe304319a34607ab1c36ec780c56ae8 Mon Sep 17 00:00:00 2001 From: hrsvrn Date: Fri, 26 Jun 2026 03:19:47 +0530 Subject: [PATCH] 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. --- apps/x/apps/renderer/src/App.css | 768 ++---------------- .../renderer/src/components/email-view.tsx | 407 ++++++---- apps/x/apps/renderer/src/styles/editor.css | 48 +- 3 files changed, 343 insertions(+), 880 deletions(-) diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css index 2cb6ac33..bdb1430f 100644 --- a/apps/x/apps/renderer/src/App.css +++ b/apps/x/apps/renderer/src/App.css @@ -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; diff --git a/apps/x/apps/renderer/src/components/email-view.tsx b/apps/x/apps/renderer/src/components/email-view.tsx index 5d62898a..266ae93c 100644 --- a/apps/x/apps/renderer/src/components/email-view.tsx +++ b/apps/x/apps/renderer/src/components/email-view.tsx @@ -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 ( - + ) } function ComposeToolbar({ editor, onOpenLink }: { editor: Editor; onOpenLink: () => void }) { return ( -
- - + - + + + editor.chain().focus().toggleBold().run()} isActive={editor.isActive('bold')} label="Bold" > - + - + - + - + editor.chain().focus().toggleBulletList().run()} isActive={editor.isActive('bulletList')} label="Bulleted list" > - + - + - + - - + +
) } @@ -678,7 +689,7 @@ function HighlightedText({ text, query }: { text: string; query: string }) { return ( <> {text.slice(0, idx)} - {text.slice(idx, idx + q.length)} + {text.slice(idx, idx + q.length)} {text.slice(idx + q.length)} ) @@ -812,26 +823,30 @@ function RecipientField({ const showSuggestions = isFocused && suggestions.length > 0 return ( -
- {label} -
+
+ {label} +
{value.map((token, index) => ( - - {recipientLabel(token)} + + {recipientLabel(token)} ))} setDraft(event.target.value)} onKeyDown={onKeyDown} @@ -854,7 +869,11 @@ function RecipientField({ }} /> {showSuggestions && ( -
    +
      {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)} > - - + + {c.name && ( - + )} @@ -893,7 +915,7 @@ function RecipientField({
    )}
- {trailing &&
{trailing}
} + {trailing &&
{trailing}
}
) } @@ -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('') 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 = ( -
event.stopPropagation() : undefined} - > -
- {modeLabel} - -
+ const inner = ( + <> - {!showCc && } - {!showBcc && } +
+ {!showCc && ( + + )} + {!showBcc && ( + + )}
} /> {showCc && } {showBcc && } - {isNew && ( - <> -
- 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() - } - }} - /> - -
-
- - {TONE_PRESETS.map((preset) => ( - - ))} -
- - )} +
+ 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() + } + }} + /> + +
+
+ + {TONE_PRESETS.map((preset) => ( + + ))} +
{(isNew || mode === 'forward') && ( -
- Subject +
+ Subject setSubject(event.target.value)} />
)} - + {attachments.length > 0 && ( -
+
{attachments.map((att) => ( -
- - {att.filename} - {formatAttachmentSize(att.size)} +
+ + {att.filename} + {formatAttachmentSize(att.size)} + >
))}
)} {linkOpen && ( -
event.preventDefault()}> - event.preventDefault()} + > + setLinkUrl(event.target.value)} placeholder="https://example.com" @@ -1423,58 +1485,99 @@ const ComposeBox = memo(function ComposeBox({ } }} /> - - + +
)} -
-
- - + {thread && ( - + )}
{editor && } - +
-
+ ) if (isNew) { return ( -
- {card} -
+ { if (!open) onClose() }}> + +
+ {modeLabel} + +
+ {inner} +
+
) } - return card + + return ( +
+
+ {modeLabel} + +
+ {inner} +
+ ) }) 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]) diff --git a/apps/x/apps/renderer/src/styles/editor.css b/apps/x/apps/renderer/src/styles/editor.css index dbf9f067..9822a4ab 100644 --- a/apps/x/apps/renderer/src/styles/editor.css +++ b/apps/x/apps/renderer/src/styles/editor.css @@ -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;