mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-11 08:12:38 +02:00
feat: Gmail-style email block with inbox container layout (#531)
* feat: restyle email block with Gmail-style layout and avatar * style: apply Google Sans/Roboto font to email block * feat: add Gmail inbox-style multi-email block with accordion rows * style: fix sender name casing, weight, and email display in expanded view * feat: emails inbox block with container layout, two-line rows, Gmail title style
This commit is contained in:
parent
3630032d21
commit
0e3d058c29
7 changed files with 899 additions and 363 deletions
|
|
@ -20,7 +20,7 @@ import { IframeBlockExtension } from '@/extensions/iframe-block'
|
|||
import { ChartBlockExtension } from '@/extensions/chart-block'
|
||||
import { TableBlockExtension } from '@/extensions/table-block'
|
||||
import { CalendarBlockExtension } from '@/extensions/calendar-block'
|
||||
import { EmailBlockExtension } from '@/extensions/email-block'
|
||||
import { EmailBlockExtension, EmailsBlockExtension } from '@/extensions/email-block'
|
||||
import { TranscriptBlockExtension } from '@/extensions/transcript-block'
|
||||
import { MermaidBlockExtension } from '@/extensions/mermaid-block'
|
||||
import { Markdown } from 'tiptap-markdown'
|
||||
|
|
@ -707,6 +707,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
|||
ChartBlockExtension,
|
||||
TableBlockExtension,
|
||||
CalendarBlockExtension,
|
||||
EmailsBlockExtension,
|
||||
EmailBlockExtension,
|
||||
TranscriptBlockExtension,
|
||||
MermaidBlockExtension,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mergeAttributes, Node } from '@tiptap/react'
|
||||
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
|
||||
import { X, Mail, ChevronDown, ExternalLink, Copy, Check, MessageSquare } from 'lucide-react'
|
||||
import { X, ExternalLink, Copy, Check, MessageSquare, ChevronDown } from 'lucide-react'
|
||||
import { blocks } from '@x/shared'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useTheme } from '@/contexts/theme-context'
|
||||
|
|
@ -11,17 +11,47 @@ function formatEmailDate(dateStr: string): string {
|
|||
try {
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return dateStr
|
||||
return d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) +
|
||||
' ' + d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||
const now = new Date()
|
||||
const isToday = d.getDate() === now.getDate() && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear()
|
||||
if (isToday) return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract just the name part from "Name <email>" format */
|
||||
function senderFirstName(from: string): string {
|
||||
const name = from.replace(/<.*>/, '').trim()
|
||||
return name.split(/\s+/)[0] || name
|
||||
function formatFullDate(dateStr: string): string {
|
||||
try {
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return dateStr
|
||||
return d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }) +
|
||||
', ' + d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
function extractName(from: string): string {
|
||||
const match = from.match(/^([^<]+)</)
|
||||
if (match) return match[1].trim()
|
||||
const username = from.replace(/@.*/, '').replace(/[._+]/g, ' ').trim()
|
||||
return username.replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
|
||||
function getInitial(from: string): string {
|
||||
const name = extractName(from)
|
||||
return (name[0] || '?').toUpperCase()
|
||||
}
|
||||
|
||||
const GMAIL_AVATAR_COLORS = [
|
||||
'#1a73e8', '#e8453c', '#34a853', '#8430ce', '#f29900',
|
||||
'#00796b', '#c62828', '#1565c0', '#6a1b9a', '#2e7d32',
|
||||
]
|
||||
|
||||
function avatarColor(from: string): string {
|
||||
let hash = 0
|
||||
for (let i = 0; i < from.length; i++) hash = (hash * 31 + from.charCodeAt(i)) >>> 0
|
||||
return GMAIL_AVATAR_COLORS[hash % GMAIL_AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
@ -30,7 +60,308 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Email Block ---
|
||||
// --- Shared: expanded email body used by both block types ---
|
||||
|
||||
function EmailExpandedBody({
|
||||
config,
|
||||
resolvedTheme,
|
||||
}: {
|
||||
config: blocks.EmailBlock
|
||||
resolvedTheme: string
|
||||
}) {
|
||||
const [draftBody, setDraftBody] = useState(config.draft_response || '')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const bodyRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setDraftBody(config.draft_response || '')
|
||||
}, [config.draft_response])
|
||||
|
||||
useEffect(() => {
|
||||
if (bodyRef.current) {
|
||||
bodyRef.current.style.height = 'auto'
|
||||
bodyRef.current.style.height = bodyRef.current.scrollHeight + 'px'
|
||||
}
|
||||
}, [draftBody])
|
||||
|
||||
const draftWithAssistant = useCallback(() => {
|
||||
let prompt = draftBody
|
||||
? `Help me refine this draft response to an email`
|
||||
: `Help me draft a response to this email`
|
||||
if (config.threadId) {
|
||||
prompt += `. Read the full thread at gmail_sync/${config.threadId}.md for context`
|
||||
}
|
||||
prompt += `.\n\n**From:** ${config.from || 'Unknown'}\n**Subject:** ${config.subject || 'No subject'}\n`
|
||||
if (draftBody) prompt += `\n**Current draft:**\n${draftBody}\n`
|
||||
window.__pendingEmailDraft = { prompt }
|
||||
window.dispatchEvent(new Event('email-block:draft-with-assistant'))
|
||||
}, [config, draftBody])
|
||||
|
||||
const copyDraft = useCallback(() => {
|
||||
navigator.clipboard.writeText(draftBody).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}).catch(() => {
|
||||
const el = document.createElement('textarea')
|
||||
el.value = draftBody
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}, [draftBody])
|
||||
|
||||
const gmailUrl = config.threadId
|
||||
? `https://mail.google.com/mail/u/0/#all/${config.threadId}`
|
||||
: null
|
||||
|
||||
const senderName = config.from ? extractName(config.from) : 'Unknown'
|
||||
const initial = config.from ? getInitial(config.from) : '?'
|
||||
const color = config.from ? avatarColor(config.from) : '#5f6368'
|
||||
const hasDraft = !!config.draft_response
|
||||
|
||||
return (
|
||||
<div className="email-gmail-expanded">
|
||||
{config.subject && (
|
||||
<div className="email-gmail-exp-subject">{config.subject}</div>
|
||||
)}
|
||||
|
||||
<div className="email-gmail-exp-meta">
|
||||
<div className="email-gmail-exp-avatar" style={{ backgroundColor: color }}>{initial}</div>
|
||||
<div className="email-gmail-exp-meta-right">
|
||||
<div className="email-gmail-exp-sender">{config.from || 'Unknown'}</div>
|
||||
<div className="email-gmail-exp-to-date">
|
||||
{config.to && <span>to {config.to}</span>}
|
||||
{config.date && <span className="email-gmail-exp-fulldate">{formatFullDate(config.date)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="email-gmail-exp-body">{config.latest_email}</div>
|
||||
|
||||
{config.past_summary && (
|
||||
<div className="email-gmail-exp-history">
|
||||
<div className="email-gmail-exp-history-label">Earlier conversation</div>
|
||||
<div className="email-gmail-exp-history-body">{config.past_summary}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasDraft && (
|
||||
<div className="email-gmail-reply-row">
|
||||
{gmailUrl && (
|
||||
<button
|
||||
className="email-gmail-btn"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); window.open(gmailUrl, '_blank') }}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
Open in Gmail
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="email-gmail-btn email-gmail-btn-primary email-gmail-reply-row-end"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); draftWithAssistant() }}
|
||||
>
|
||||
<MessageSquare size={13} />
|
||||
Draft with Rowboat
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasDraft && (
|
||||
<div className="email-gmail-compose">
|
||||
<div className="email-gmail-compose-to">
|
||||
<span className="email-gmail-compose-to-label">Reply</span>
|
||||
{config.from && <span className="email-gmail-compose-to-addr">{config.from}</span>}
|
||||
</div>
|
||||
<textarea
|
||||
key={resolvedTheme}
|
||||
ref={bodyRef}
|
||||
className="email-gmail-compose-body"
|
||||
value={draftBody}
|
||||
onChange={(e) => setDraftBody(e.target.value)}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder="Write your reply..."
|
||||
rows={3}
|
||||
/>
|
||||
<div className="email-gmail-compose-footer">
|
||||
<button
|
||||
className="email-gmail-btn email-gmail-btn-primary"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); draftWithAssistant() }}
|
||||
>
|
||||
<MessageSquare size={13} />
|
||||
{hasDraft ? 'Refine with Rowboat' : 'Draft with Rowboat'}
|
||||
</button>
|
||||
<button
|
||||
className="email-gmail-btn"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); copyDraft() }}
|
||||
>
|
||||
{copied ? <Check size={13} /> : <Copy size={13} />}
|
||||
{copied ? 'Copied!' : 'Copy draft'}
|
||||
</button>
|
||||
{gmailUrl && (
|
||||
<button
|
||||
className="email-gmail-btn"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); window.open(gmailUrl, '_blank') }}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
Open in Gmail
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Multi-email inbox block (language-emails) ---
|
||||
|
||||
function EmailsBlockView({ node, deleteNode }: {
|
||||
node: { attrs: Record<string, unknown> }
|
||||
deleteNode: () => void
|
||||
}) {
|
||||
const raw = node.attrs.data as string
|
||||
let config: blocks.EmailsBlock | null = null
|
||||
|
||||
try {
|
||||
config = blocks.EmailsBlockSchema.parse(JSON.parse(raw))
|
||||
} catch { /* fallback below */ }
|
||||
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null)
|
||||
|
||||
if (!config || config.emails.length === 0) {
|
||||
return (
|
||||
<NodeViewWrapper className="email-block-wrapper" data-type="emails-block">
|
||||
<div className="email-block-card email-block-error"><span>Invalid emails block</span></div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="email-block-wrapper" data-type="emails-block">
|
||||
<div className="email-block-card email-inbox-card" onMouseDown={(e) => e.stopPropagation()}>
|
||||
<button className="email-block-delete" onClick={deleteNode} aria-label="Remove block"><X size={14} /></button>
|
||||
|
||||
{config.title && (
|
||||
<div className="email-inbox-title">{config.title}</div>
|
||||
)}
|
||||
|
||||
<div className="email-inbox-list">
|
||||
{config.emails.map((email, i) => {
|
||||
const isExpanded = expandedIndex === i
|
||||
const senderName = email.from ? extractName(email.from) : 'Unknown'
|
||||
const initial = email.from ? getInitial(email.from) : '?'
|
||||
const color = email.from ? avatarColor(email.from) : '#5f6368'
|
||||
const snippet = email.summary
|
||||
|| (email.latest_email ? email.latest_email.slice(0, 100).replace(/\s+/g, ' ').trim() : '')
|
||||
|
||||
return (
|
||||
<div key={i} className={`email-inbox-row${isExpanded ? ' email-inbox-row-expanded' : ''}`}>
|
||||
{/* Collapsed row */}
|
||||
<div
|
||||
className="email-inbox-row-header"
|
||||
onClick={(e) => { e.stopPropagation(); setExpandedIndex(isExpanded ? null : i) }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="email-inbox-avatar" style={{ backgroundColor: color }}>{initial}</div>
|
||||
|
||||
<div className="email-inbox-content">
|
||||
<div className="email-inbox-top-row">
|
||||
<span className="email-inbox-sender">{senderName}</span>
|
||||
{email.date && <span className="email-inbox-date">{formatEmailDate(email.date)}</span>}
|
||||
</div>
|
||||
<div className="email-inbox-bottom-row">
|
||||
{email.subject && <span className="email-inbox-subject">{email.subject}</span>}
|
||||
{snippet && (
|
||||
<span className="email-inbox-snippet">
|
||||
{email.subject ? ` — ${snippet}` : snippet}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={`email-inbox-chevron${isExpanded ? ' email-inbox-chevron-open' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isExpanded && (
|
||||
<div className="email-inbox-expanded-wrap">
|
||||
<EmailExpandedBody
|
||||
config={email}
|
||||
resolvedTheme={resolvedTheme}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const EmailsBlockExtension = Node.create({
|
||||
name: 'emailsBlock',
|
||||
group: 'block',
|
||||
atom: true,
|
||||
selectable: true,
|
||||
draggable: false,
|
||||
|
||||
addAttributes() {
|
||||
return { data: { default: '{}' } }
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{
|
||||
tag: 'pre',
|
||||
priority: 61,
|
||||
getAttrs(element) {
|
||||
const code = element.querySelector('code')
|
||||
if (!code) return false
|
||||
if ((code.className || '').includes('language-emails')) {
|
||||
return { data: code.textContent || '{}' }
|
||||
}
|
||||
return false
|
||||
},
|
||||
}]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }: { HTMLAttributes: Record<string, unknown> }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'emails-block' })]
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(EmailsBlockView)
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
markdown: {
|
||||
serialize(state: { write: (text: string) => void; closeBlock: (node: unknown) => void }, node: { attrs: { data: string } }) {
|
||||
state.write('```emails\n' + node.attrs.data + '\n```')
|
||||
state.closeBlock(node)
|
||||
},
|
||||
parse: {},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// --- Single email block (language-email, backward compat) ---
|
||||
|
||||
function EmailBlockView({ node, deleteNode, updateAttributes }: {
|
||||
node: { attrs: Record<string, unknown> }
|
||||
|
|
@ -42,194 +373,57 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: {
|
|||
|
||||
try {
|
||||
config = blocks.EmailBlockSchema.parse(JSON.parse(raw))
|
||||
} catch {
|
||||
// fallback below
|
||||
}
|
||||
|
||||
const hasDraft = !!config?.draft_response
|
||||
const hasPastSummary = !!config?.past_summary
|
||||
} catch { /* fallback below */ }
|
||||
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
// Local draft state for editing
|
||||
const [draftBody, setDraftBody] = useState(config?.draft_response || '')
|
||||
const [emailExpanded, setEmailExpanded] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const bodyRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Sync draft from external changes
|
||||
useEffect(() => {
|
||||
try {
|
||||
const parsed = blocks.EmailBlockSchema.parse(JSON.parse(raw))
|
||||
setDraftBody(parsed.draft_response || '')
|
||||
} catch { /* ignore */ }
|
||||
}, [raw])
|
||||
|
||||
// Auto-resize textarea
|
||||
useEffect(() => {
|
||||
if (bodyRef.current) {
|
||||
bodyRef.current.style.height = 'auto'
|
||||
bodyRef.current.style.height = bodyRef.current.scrollHeight + 'px'
|
||||
}
|
||||
}, [draftBody])
|
||||
|
||||
const commitDraft = useCallback((newBody: string) => {
|
||||
try {
|
||||
const current = JSON.parse(raw) as Record<string, unknown>
|
||||
updateAttributes({ data: JSON.stringify({ ...current, draft_response: newBody }) })
|
||||
} catch { /* ignore */ }
|
||||
}, [raw, updateAttributes])
|
||||
|
||||
const draftWithAssistant = useCallback(() => {
|
||||
if (!config) return
|
||||
let prompt = draftBody
|
||||
? `Help me refine this draft response to an email`
|
||||
: `Help me draft a response to this email`
|
||||
if (config.threadId) {
|
||||
prompt += `. Read the full thread at gmail_sync/${config.threadId}.md for context`
|
||||
}
|
||||
prompt += `.\n\n`
|
||||
prompt += `**From:** ${config.from || 'Unknown'}\n`
|
||||
prompt += `**Subject:** ${config.subject || 'No subject'}\n`
|
||||
if (draftBody) {
|
||||
prompt += `\n**Current draft:**\n${draftBody}\n`
|
||||
}
|
||||
window.__pendingEmailDraft = { prompt }
|
||||
window.dispatchEvent(new Event('email-block:draft-with-assistant'))
|
||||
}, [config, draftBody])
|
||||
void updateAttributes // available for future per-email draft persistence
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<NodeViewWrapper className="email-block-wrapper" data-type="email-block">
|
||||
<div className="email-block-card email-block-error">
|
||||
<Mail size={16} />
|
||||
<span>Invalid email block</span>
|
||||
</div>
|
||||
<div className="email-block-card email-block-error"><span>Invalid email block</span></div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const gmailUrl = config.threadId
|
||||
? `https://mail.google.com/mail/u/0/#all/${config.threadId}`
|
||||
: null
|
||||
|
||||
// Build summary: use explicit summary, or auto-generate from sender + subject
|
||||
const summary = config.summary
|
||||
|| (config.from && config.subject
|
||||
? `${senderFirstName(config.from)} reached out about ${config.subject}`
|
||||
: config.subject || 'New email')
|
||||
const senderName = config.from ? extractName(config.from) : 'Unknown'
|
||||
const initial = config.from ? getInitial(config.from) : '?'
|
||||
const color = config.from ? avatarColor(config.from) : '#5f6368'
|
||||
const snippet = config.summary
|
||||
|| (config.latest_email ? config.latest_email.slice(0, 120).replace(/\s+/g, ' ').trim() : '')
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="email-block-wrapper" data-type="email-block">
|
||||
<div className="email-block-card email-block-card-gmail" onMouseDown={(e) => e.stopPropagation()}>
|
||||
<button className="email-block-delete" onClick={deleteNode} aria-label="Delete email block">
|
||||
<X size={14} />
|
||||
</button>
|
||||
<button className="email-block-delete" onClick={deleteNode} aria-label="Delete email block"><X size={14} /></button>
|
||||
|
||||
{/* Header: Email badge */}
|
||||
<div className="email-block-badge">
|
||||
<Mail size={13} />
|
||||
Email
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="email-block-summary">{summary}</div>
|
||||
|
||||
{/* Expandable email details */}
|
||||
<button
|
||||
className="email-block-expand-btn"
|
||||
onClick={(e) => { e.stopPropagation(); setEmailExpanded(!emailExpanded) }}
|
||||
<div
|
||||
className={`email-gmail-row${expanded ? ' email-gmail-row-expanded' : ''}`}
|
||||
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded) }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ChevronDown size={13} className={`email-block-toggle-chevron ${emailExpanded ? 'email-block-toggle-chevron-open' : ''}`} />
|
||||
{emailExpanded ? 'Hide email' : 'Show email'}
|
||||
{config.from && <span className="email-block-expand-meta">· From {senderFirstName(config.from)}</span>}
|
||||
{config.date && <span className="email-block-expand-meta">· {formatEmailDate(config.date)}</span>}
|
||||
</button>
|
||||
|
||||
{emailExpanded && (
|
||||
<div className="email-block-email-details">
|
||||
<div className="email-block-message">
|
||||
<div className="email-block-message-header">
|
||||
<div className="email-block-sender-info">
|
||||
<div className="email-block-sender-row">
|
||||
<div className="email-block-sender-name">{config.from || 'Unknown'}</div>
|
||||
{config.date && <div className="email-block-sender-date">{formatEmailDate(config.date)}</div>}
|
||||
</div>
|
||||
{config.subject && <div className="email-block-subject-line">Subject: {config.subject}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="email-block-message-body">{config.latest_email}</div>
|
||||
<div className="email-gmail-avatar" style={{ backgroundColor: color }} aria-hidden="true">{initial}</div>
|
||||
<div className="email-gmail-content">
|
||||
<div className="email-gmail-top-row">
|
||||
<span className="email-gmail-sender">{senderName}</span>
|
||||
{config.date && <span className="email-gmail-date">{formatEmailDate(config.date)}</span>}
|
||||
</div>
|
||||
<div className="email-gmail-bottom-row">
|
||||
{config.subject && <span className="email-gmail-subject">{config.subject}</span>}
|
||||
{snippet && <span className="email-gmail-snippet">{config.subject ? ` — ${snippet}` : snippet}</span>}
|
||||
</div>
|
||||
{hasPastSummary && (
|
||||
<div className="email-block-context-section">
|
||||
<div className="email-block-context-label">Earlier conversation</div>
|
||||
<div className="email-block-context-summary">{config.past_summary}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Draft section */}
|
||||
{hasDraft && (
|
||||
<div className="email-block-draft-section">
|
||||
<div className="email-block-draft-label">Draft reply</div>
|
||||
<textarea
|
||||
key={resolvedTheme}
|
||||
ref={bodyRef}
|
||||
className="email-draft-block-body-input"
|
||||
value={draftBody}
|
||||
onChange={(e) => setDraftBody(e.target.value)}
|
||||
onBlur={() => commitDraft(draftBody)}
|
||||
placeholder="Write your reply..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="email-block-actions">
|
||||
<button
|
||||
className="email-block-gmail-btn email-block-gmail-btn-primary"
|
||||
onClick={draftWithAssistant}
|
||||
>
|
||||
<MessageSquare size={13} />
|
||||
{hasDraft ? 'Refine with Rowboat' : 'Draft with Rowboat'}
|
||||
</button>
|
||||
{hasDraft && (
|
||||
<button
|
||||
className="email-block-gmail-btn email-block-gmail-btn-primary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(draftBody).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}).catch(() => {
|
||||
// Fallback for Electron contexts where clipboard API may fail
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = draftBody
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{copied ? <Check size={13} /> : <Copy size={13} />}
|
||||
{copied ? 'Copied!' : 'Copy draft'}
|
||||
</button>
|
||||
)}
|
||||
{gmailUrl && (
|
||||
<button
|
||||
className="email-block-gmail-btn"
|
||||
onClick={() => window.open(gmailUrl, '_blank')}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
Open in Gmail
|
||||
</button>
|
||||
)}
|
||||
<ChevronDown size={15} className={`email-gmail-chevron${expanded ? ' email-gmail-chevron-open' : ''}`} />
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<EmailExpandedBody
|
||||
config={config}
|
||||
resolvedTheme={resolvedTheme}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
|
|
@ -243,9 +437,7 @@ export const EmailBlockExtension = Node.create({
|
|||
draggable: false,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
data: { default: '{}' },
|
||||
}
|
||||
return { data: { default: '{}' } }
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
|
|
@ -256,7 +448,7 @@ export const EmailBlockExtension = Node.create({
|
|||
const code = element.querySelector('code')
|
||||
if (!code) return false
|
||||
const cls = code.className || ''
|
||||
if (cls.includes('language-email') && !cls.includes('language-emailDraft')) {
|
||||
if (cls.includes('language-email') && !cls.includes('language-emailDraft') && !cls.includes('language-emails')) {
|
||||
return { data: code.textContent || '{}' }
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
|
||||
|
||||
/* Tiptap Editor Styles */
|
||||
|
||||
.tiptap-editor {
|
||||
|
|
@ -820,6 +822,49 @@
|
|||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* Consecutive email blocks — zero gap, shared outer border */
|
||||
|
||||
/* Kill margins between adjacent email wrappers */
|
||||
.tiptap-editor .ProseMirror .email-block-wrapper:has(+ .email-block-wrapper) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.tiptap-editor .ProseMirror .email-block-wrapper + .email-block-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Strip card border/radius from every card inside a sequence */
|
||||
.tiptap-editor .ProseMirror .email-block-wrapper:has(+ .email-block-wrapper) .email-block-card,
|
||||
.tiptap-editor .ProseMirror .email-block-wrapper + .email-block-wrapper .email-block-card {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
}
|
||||
|
||||
/* First in group: restore top border + top radius */
|
||||
.tiptap-editor .ProseMirror .email-block-wrapper:not(.email-block-wrapper + .email-block-wrapper):has(+ .email-block-wrapper) .email-block-card {
|
||||
border-top: 1px solid var(--border);
|
||||
border-left: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
/* Last in group: restore bottom border + bottom radius, remove hairline */
|
||||
.tiptap-editor .ProseMirror .email-block-wrapper + .email-block-wrapper:not(:has(+ .email-block-wrapper)) .email-block-card {
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-left: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
/* Middle cards: just left + right borders */
|
||||
.tiptap-editor .ProseMirror .email-block-wrapper + .email-block-wrapper:has(+ .email-block-wrapper) .email-block-card {
|
||||
border-left: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
|
||||
.tiptap-editor .ProseMirror .image-block-card,
|
||||
.tiptap-editor .ProseMirror .embed-block-card,
|
||||
.tiptap-editor .ProseMirror .iframe-block-card,
|
||||
|
|
@ -1422,141 +1467,209 @@
|
|||
|
||||
/* Email block – Gmail style */
|
||||
.tiptap-editor .ProseMirror .email-block-card-gmail {
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 1px rgba(0, 0, 0, 0.06);
|
||||
font-family: 'Google Sans', Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-card-gmail:hover {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
/* Email badge */
|
||||
.tiptap-editor .ProseMirror .email-block-badge {
|
||||
display: inline-flex;
|
||||
/* Gmail-style two-column row */
|
||||
.tiptap-editor .ProseMirror .email-gmail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: color-mix(in srgb, var(--foreground) 45%, transparent);
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
padding: 2px 0;
|
||||
border-radius: 4px;
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Summary */
|
||||
.tiptap-editor .ProseMirror .email-block-summary {
|
||||
.tiptap-editor .ProseMirror .email-gmail-row:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 4%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-row.email-gmail-row-expanded {
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Sender avatar circle */
|
||||
.tiptap-editor .ProseMirror .email-gmail-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--foreground);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Expand button */
|
||||
.tiptap-editor .ProseMirror .email-block-expand-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: color-mix(in srgb, var(--foreground) 50%, transparent);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.12s ease;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-expand-btn:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-expand-meta {
|
||||
color: color-mix(in srgb, var(--foreground) 35%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-toggle-chevron {
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-toggle-chevron-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Email details (expanded) */
|
||||
.tiptap-editor .ProseMirror .email-block-email-details {
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background: color-mix(in srgb, var(--foreground) 4%, transparent);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-message {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-message-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-sender-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
/* Content column */
|
||||
.tiptap-editor .ProseMirror .email-gmail-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-sender-row {
|
||||
.tiptap-editor .ProseMirror .email-gmail-top-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-sender-name {
|
||||
.tiptap-editor .ProseMirror .email-gmail-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;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-sender-date {
|
||||
.tiptap-editor .ProseMirror .email-gmail-date {
|
||||
font-size: 12px;
|
||||
color: color-mix(in srgb, var(--foreground) 50%, transparent);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-subject-line {
|
||||
font-size: 12px;
|
||||
.tiptap-editor .ProseMirror .email-gmail-bottom-row {
|
||||
font-size: 13px;
|
||||
color: color-mix(in srgb, var(--foreground) 55%, transparent);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-subject {
|
||||
color: color-mix(in srgb, var(--foreground) 80%, transparent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-snippet {
|
||||
color: color-mix(in srgb, var(--foreground) 45%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-message-body {
|
||||
/* Chevron */
|
||||
.tiptap-editor .ProseMirror .email-gmail-chevron {
|
||||
flex-shrink: 0;
|
||||
color: color-mix(in srgb, var(--foreground) 40%, transparent);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-chevron.email-gmail-chevron-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Expanded area */
|
||||
.tiptap-editor .ProseMirror .email-gmail-expanded {
|
||||
padding-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-subject {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: var(--foreground);
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Metadata strip (avatar + from/to/date + open button) */
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-meta {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-meta-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-sender {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-to-date {
|
||||
font-size: 12px;
|
||||
color: color-mix(in srgb, var(--foreground) 50%, transparent);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-fulldate {
|
||||
color: color-mix(in srgb, var(--foreground) 40%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-open-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: none;
|
||||
color: color-mix(in srgb, var(--foreground) 50%, transparent);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-open-btn:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Email body */
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-body {
|
||||
font-size: 14px;
|
||||
color: color-mix(in srgb, var(--foreground) 80%, transparent);
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.58;
|
||||
line-height: 1.6;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-context-section {
|
||||
/* Earlier conversation */
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-history {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-context-label {
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-history-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
|
|
@ -1564,68 +1677,88 @@
|
|||
color: color-mix(in srgb, var(--foreground) 40%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-context-summary {
|
||||
font-size: 14px;
|
||||
color: color-mix(in srgb, var(--foreground) 65%, transparent);
|
||||
line-height: 1.58;
|
||||
.tiptap-editor .ProseMirror .email-gmail-exp-history-body {
|
||||
font-size: 13px;
|
||||
color: color-mix(in srgb, var(--foreground) 55%, transparent);
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid color-mix(in srgb, var(--foreground) 12%, transparent);
|
||||
}
|
||||
|
||||
/* Draft section */
|
||||
.tiptap-editor .ProseMirror .email-block-draft-section {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
|
||||
border-radius: 6px;
|
||||
/* Compose / draft box */
|
||||
.tiptap-editor .ProseMirror .email-gmail-compose {
|
||||
margin-top: 4px;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 15%, transparent);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-draft-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: color-mix(in srgb, var(--foreground) 40%, transparent);
|
||||
margin-bottom: 4px;
|
||||
.tiptap-editor .ProseMirror .email-gmail-compose-to {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px 6px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-draft-block-body-input {
|
||||
.tiptap-editor .ProseMirror .email-gmail-compose-to-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: color-mix(in srgb, var(--foreground) 45%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-compose-to-addr {
|
||||
font-size: 13px;
|
||||
color: color-mix(in srgb, var(--foreground) 70%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-compose-body {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: var(--foreground);
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 4px 0;
|
||||
padding: 10px 12px;
|
||||
font-family: inherit;
|
||||
line-height: 1.58;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-draft-block-body-input::placeholder {
|
||||
.tiptap-editor .ProseMirror .email-gmail-compose-body::placeholder {
|
||||
color: color-mix(in srgb, var(--foreground) 35%, transparent);
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.tiptap-editor .ProseMirror .email-block-actions {
|
||||
.tiptap-editor .ProseMirror .email-gmail-compose-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-gmail-btn {
|
||||
/* Action buttons */
|
||||
.tiptap-editor .ProseMirror .email-gmail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 16px;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: color-mix(in srgb, var(--foreground) 60%, transparent);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 20%, transparent);
|
||||
border-radius: 18px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
|
@ -1633,24 +1766,19 @@
|
|||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-gmail-btn:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 1px rgba(0, 0, 0, 0.06);
|
||||
.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);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-gmail-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-gmail-btn-primary {
|
||||
.tiptap-editor .ProseMirror .email-gmail-btn-primary {
|
||||
color: #fff;
|
||||
background: #1a73e8;
|
||||
border-color: #1a73e8;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-block-gmail-btn-primary:hover:not(:disabled) {
|
||||
.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;
|
||||
|
|
@ -1665,6 +1793,167 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Reply / Forward pill buttons (in expanded view) */
|
||||
.tiptap-editor .ProseMirror .email-gmail-reply-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-reply-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: color-mix(in srgb, var(--foreground) 65%, transparent);
|
||||
background: transparent;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 22%, transparent);
|
||||
border-radius: 18px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease, box-shadow 0.12s ease;
|
||||
font-family: 'Google Sans', Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-reply-btn:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 6%, transparent);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-gmail-reply-row-end {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ---- Emails inbox block (language-emails) ---- */
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-card {
|
||||
font-family: 'Google Sans', Roboto, RobotoDraft, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: color-mix(in srgb, var(--foreground) 70%, transparent);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Each email row — hairline separator only, no card */
|
||||
.tiptap-editor .ProseMirror .email-inbox-row {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 4px 7px 0;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-row-header:hover {
|
||||
background: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-row.email-inbox-row-expanded .email-inbox-row-header {
|
||||
background: color-mix(in srgb, var(--foreground) 3%, transparent);
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
.tiptap-editor .ProseMirror .email-inbox-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Content column — two-line layout */
|
||||
.tiptap-editor .ProseMirror .email-inbox-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-top-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-date {
|
||||
font-size: 12px;
|
||||
color: color-mix(in srgb, var(--foreground) 50%, transparent);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-bottom-row {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-subject {
|
||||
font-weight: 500;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-snippet {
|
||||
color: color-mix(in srgb, var(--foreground) 45%, transparent);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Expand chevron */
|
||||
.tiptap-editor .ProseMirror .email-inbox-chevron {
|
||||
flex-shrink: 0;
|
||||
color: color-mix(in srgb, var(--foreground) 35%, transparent);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.tiptap-editor .ProseMirror .email-inbox-chevron.email-inbox-chevron-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Expanded content padding */
|
||||
.tiptap-editor .ProseMirror .email-inbox-expanded-wrap {
|
||||
padding: 8px 0 12px 0;
|
||||
border-top: 1px solid color-mix(in srgb, var(--foreground) 6%, transparent);
|
||||
}
|
||||
|
||||
/* Transcript block */
|
||||
.tiptap-editor .ProseMirror .transcript-block-toggle {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue