diff --git a/site/package.json b/site/package.json index ccf8aefaf..064cbee32 100644 --- a/site/package.json +++ b/site/package.json @@ -14,6 +14,7 @@ "@astrojs/rss": "^4.0.18", "@astrojs/sitemap": "^3.7.3", "@fontsource/inter": "^5.2.6", + "@paper-design/shaders": "^0.0.80", "@tailwindcss/vite": "^4.1.10", "astro": "^6.4.8", "motion": "^12.34.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 3d9885244..ad0280995 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@fontsource/inter': specifier: ^5.2.6 version: 5.2.8 + '@paper-design/shaders': + specifier: ^0.0.80 + version: 0.0.80 '@tailwindcss/vite': specifier: ^4.1.10 version: 4.3.0(vite@7.3.6(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) @@ -601,6 +604,9 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@paper-design/shaders@0.0.80': + resolution: {integrity: sha512-pcabvt5xDlFoEhpjUj4b1tGJMfqb0i5mXifWMNQf6z7FoOJYxYDo7F8R6k0JAh5D/7ouxjX5+N0cIq5WURyQ1Q==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -2500,6 +2506,8 @@ snapshots: '@oslojs/encoding@1.1.0': {} + '@paper-design/shaders@0.0.80': {} + '@rollup/pluginutils@5.3.0(rollup@4.62.2)': dependencies: '@types/estree': 1.0.9 diff --git a/site/src/components/AIAssistantMock.astro b/site/src/components/AIAssistantMock.astro deleted file mode 100644 index df0c7f5ea..000000000 --- a/site/src/components/AIAssistantMock.astro +++ /dev/null @@ -1,314 +0,0 @@ ---- -/** - * Pixel-faithful mock of the Warmbly "Write with AI" assistant, shown IN - * CONTEXT inside the campaign Step email composer. - * - * Maps 1:1 to the real components: - * - web/src/components/app/campaigns/sequences/SequenceView.tsx the Step composer panel - * - web/src/components/app/campaigns/sequences/WriteWithAI.tsx the AI popover (the star) - * - web/src/components/app/campaigns/sequences/RichTextEditor.tsx the body toolbar + editor - * - web/src/lib/api/models/app/generation/Write.ts WRITE_TONES presets - * - web/src/components/ui/field.tsx TextInput / Label - * - web/src/components/ui/popover-menu.tsx SelectButton / PopoverMenuContent - * - * Desktop-locked, authored at the 900px design width (FeatureSection scales it - * to the column). No responsive variants; always the desktop layout. - */ - -// Tone presets, copied verbatim from WRITE_TONES (web/.../generation/Write.ts). -// "Default" (empty value) is the selected tone. -const tones = [ - { label: "Default", selected: true }, - { label: "Friendly", selected: false }, - { label: "Professional", selected: false }, - { label: "Casual", selected: false }, - { label: "Concise", selected: false }, - { label: "Persuasive", selected: false }, -]; - -// The freshly generated cold-email draft that fills the body after Generate. -// Matches SequenceView.onInsert -> setBody: each line is a

in the tiptap body. -const draftLines = [ - "Hi Alex,", - "Noticed Acme has been scaling fast on AWS. Teams your size are usually overpaying 30 to 40 percent on compute they spun up once and never tuned.", - "I help engineering teams find that spend without re-architecting anything. Worth a quick 15-minute call to walk through the three line items I'd check first?", - "Open to next Tuesday or Wednesday afternoon?", - "Best,", - "Jordan", -]; ---- - -

- - -
- Sequence -
-
- - 1 - Step 1 - - - 2 - Follow-up - +3d - - - - Add step - -
-
- - - - - Draft saved -
-
- - -
- - -
- - -
-
-
Step 1
-

Compose the email this step sends.

-
-
- - - - - - - - - -
- - - - - - -
-
- - -
- - -
- -
Cold intro
-

Internal label only. Recipients never see it.

-
- - -
-
- - -
-
- Quick question about Acme's AWS spend, {`{{.FirstName}}`} -
-
- - -
-
- -
- - - Edit - - - - Preview - -
-
- - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-

Hi {`{{.FirstName}}`}, ...

- - -
-
- {draftLines.map((line) => ( -

{line}

- ))} -
-
-
-
-
-
- - -
-
- -
- -
- A short intro asking to book a 15-min call about cutting their AWS bill. -
-
- - -
- -
- {tones.map((t) => ( - {t.label} - ))} -
-
- - -
- - 3 credits remaining - - - Generated. 2 credits remaining - - - -
-
-
- -
-
- - - - - - - -
diff --git a/site/src/components/AnalyticsMock.astro b/site/src/components/AnalyticsMock.astro index e0fff9772..b47acc59a 100644 --- a/site/src/components/AnalyticsMock.astro +++ b/site/src/components/AnalyticsMock.astro @@ -1,20 +1,116 @@ --- import Logo from '../components/Logo.astro'; +import DemoCursor from './DemoCursor.astro'; /** - * Pixel-faithful mock of the Warmbly /app/analytics screen. + * The /app/analytics screen, 1:1 with web/src/app/app/analytics/page.tsx: + * PageTopbar with the RangeTabs and Share image button, the four-cell + * StatStrip, the MultiTrend chart (every metric on one graph, dithered area + * per series, three gridlines with compact labels, legend pills that toggle + * a series), the Breakdown and Account health aside, Top campaigns and + * Recent activity. Rendered as the content panel only; the shell wraps it. * - * Maps 1:1 to the real content panel: - * - web/src/app/app/analytics/page.tsx - PageTopbar / StatStrip / SectionBar / charts / rows - * - web/src/components/ui/charts.tsx - DailyBars (rounded-sm bars, baseline, font-mono ticks) - * - web/src/components/app/analytics/StatsShareCard.tsx - the branded share card - * - web/src/components/app/analytics/AnalyticsShareButton.tsx - the preview modal - * - * Renders only the in-app screen content + its immediate chrome (no global - * sidebar / app header). The marketing page wraps this in its own browser frame. + * The share modal is AnalyticsShareButton's preview: aspect presets with 1:1 + * active by default, the 1080x1080 StatsShareCard drawn at true size and + * scaled into its 460px preview box, Copy and Download PNG. */ -// Exact StatsShareCard gradient/wash/haze (copied from the real component). +// play: the cursor opens and dismisses the share modal on a loop. +// off: the plain screen. open: the modal held open, no cursor. +interface Props { demo?: 'play' | 'off' | 'open' } +const { demo = 'play' } = Astro.props; +const demoClass = demo === 'off' ? 'an-off' : demo === 'open' ? 'an-open' : ''; + +// daily_trend, Dec 2 to Dec 8. Every series shares one y scale, as MultiTrend does. +const days = ['Dec 2', 'Dec 3', 'Dec 4', 'Dec 5', 'Dec 6', 'Dec 7', 'Dec 8']; +const series = [ + { key: 'sent', label: 'Sent', dot: 'bg-sky-500', rgb: '14,165,233', values: [380, 420, 350, 480, 440, 410, 367] }, + { key: 'opens', label: 'Opens', dot: 'bg-emerald-500', rgb: '16,185,129', values: [118, 132, 108, 150, 138, 128, 117] }, + { key: 'clicks', label: 'Clicks', dot: 'bg-violet-500', rgb: '139,92,246', values: [30, 35, 28, 40, 36, 33, 32] }, + { key: 'replies', label: 'Replies', dot: 'bg-amber-500', rgb: '245,158,11', values: [7, 9, 6, 10, 9, 8, 7] }, +]; + +// MultiTrend geometry: height 280, 18px tick row, 5px top pad, 1px bottom pad. +// The chart column is 1280 minus the 256 sidebar minus the 300 aside minus +// the 20px padding each side, so the SVG is authored at that width. +const CH_W = 684; +const CH_H = 280; +const TICK_H = 18; +const CHART_H = CH_H - TICK_H - 4; +const PAD_TOP = 5; +const PAD_BOTTOM = 1; +const INNER_H = CHART_H - PAD_TOP - PAD_BOTTOM; +const max = Math.max(...series.flatMap((s) => s.values)); +const yOf = (v: number) => PAD_TOP + (1 - v / max) * INNER_H; +const xOf = (i: number) => (i / (days.length - 1)) * CH_W; + +// Monotone cubic through the points, the same shape the canvas painter draws. +function monotonePath(vals: number[]) { + const n = vals.length; + const xs = vals.map((_, i) => xOf(i)); + const ys = vals.map(yOf); + const d: number[] = []; + for (let i = 0; i < n - 1; i++) d.push((ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i])); + const m: number[] = new Array(n).fill(0); + m[0] = d[0]; + m[n - 1] = d[n - 2]; + for (let i = 1; i < n - 1; i++) m[i] = d[i - 1] * d[i] <= 0 ? 0 : (d[i - 1] + d[i]) / 2; + let path = `M ${xs[0].toFixed(1)} ${ys[0].toFixed(1)}`; + for (let i = 0; i < n - 1; i++) { + const dx = (xs[i + 1] - xs[i]) / 3; + path += ` C ${(xs[i] + dx).toFixed(1)} ${(ys[i] + m[i] * dx).toFixed(1)}, ${(xs[i + 1] - dx).toFixed(1)} ${(ys[i + 1] - m[i + 1] * dx).toFixed(1)}, ${xs[i + 1].toFixed(1)} ${ys[i + 1].toFixed(1)}`; + } + return path; +} +const curves = series.map((s) => { + const line = monotonePath(s.values); + return { ...s, line, area: `${line} L ${CH_W} ${CHART_H} L 0 ${CHART_H} Z` }; +}); +const compact = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n)}`); +const grid = [0.25, 0.5, 0.75].map((f) => ({ top: PAD_TOP + (1 - f) * INNER_H, label: compact(max * f) })); + +const statStrip = [ + { label: 'Total sent', value: '2,847', sub: 'in range', accent: true, last: false }, + { label: 'Open rate', value: '31.3%', sub: 'after delivery', accent: false, last: false }, + { label: 'Reply rate', value: '2.0%', sub: 'incl. positive', accent: false, last: false }, + { label: 'Bounce rate', value: '0.5%', sub: 'hard + soft', accent: false, last: true }, +]; + +const breakdown = [ + { label: 'Sent', value: '2,847', dot: 'bg-slate-400' }, + { label: 'Opens', value: '891', dot: 'bg-emerald-500', note: '142 auto' }, + { label: 'Clicks', value: '234', dot: 'bg-violet-500' }, + { label: 'Replies', value: '56', dot: 'bg-amber-500' }, + { label: 'Bounces', value: '14', dot: 'bg-rose-500' }, +]; + +const health = [ + { n: '6', label: 'Healthy', tone: 'text-emerald-600' }, + { n: '1', label: 'At risk', tone: 'text-amber-600' }, + { n: '1', label: 'Issues', tone: 'text-rose-600' }, +]; + +const campaigns = [ + { name: 'Q4 Outreach - Tech Leaders', status: 'active', sent: '850', open: '38.5%', reply: '2.8%' }, + { name: 'Win-back: Former Customers', status: 'active', sent: '620', open: '26.3%', reply: '1.6%' }, + { name: 'Product Launch Beta Access', status: 'paused', sent: '480', open: '42.1%', reply: '3.1%' }, +]; +const statusDot = (s: string) => (s === 'active' ? 'bg-emerald-500' : s === 'paused' ? 'bg-amber-500' : 'bg-slate-300'); + +// lucide "activity" +const activityPath = ''; +const activity = [ + { tone: 'text-emerald-600', verb: 'Opened by', email: 'sarah.chen@techcorp.com', campaign: 'Q4 Outreach - Tech Leaders', ts: 'Dec 8, 02:32 PM' }, + { tone: 'text-violet-600', verb: 'Clicked by', email: 'mark.ross@northwind.dev', campaign: 'Win-back: Former Customers', ts: 'Dec 8, 02:18 PM' }, + { tone: 'text-amber-600', verb: 'Reply from', email: 'jess@helio.gen', campaign: 'Product Launch Beta Access', ts: 'Dec 8, 01:54 PM' }, + { tone: 'text-slate-500', verb: 'Sent to', email: 'omar@brightco.io', campaign: 'Q4 Outreach - Tech Leaders', ts: 'Dec 8, 01:40 PM' }, + { tone: 'text-rose-600', verb: 'Bounced', email: 'lena@oldmail.example', campaign: 'Win-back: Former Customers', ts: 'Dec 8, 01:12 PM' }, +]; + +// StatsShareCard, 1:1 preset: 1080x1080, drawn at true size and scaled into +// the 460px preview box the modal gives it. +const CARD = 1080; +const PREVIEW = 460; const SKY_BASE = 'radial-gradient(ellipse 125% 105% at 50% -12%,' + ' #9fe0fb 0%, #74cef7 30%, #4ec1f6 58%, #2fb6f2 82%, #18abed 100%)'; @@ -23,103 +119,44 @@ const SKY_BREATHE = ' rgba(224,242,254,0.85) 0%, rgba(125,211,252,0.38) 24%, rgba(56,189,248,0.14) 46%, transparent 64%)'; const HAZE_BOTTOM = 'linear-gradient(to top, rgba(186,230,253,0.40) 0%, rgba(125,211,252,0.16) 42%, transparent 100%)'; - -// The card's "Sends over time" area chart, computed from the same trend the -// dashboard shows (ShareAreaChart geometry: viewBox 0 0 1000 320, baseline y=314). -const shareLine = 'M 4.0 76.5 L 169.3 51.5 L 334.7 95.2 L 500.0 14.0 L 665.3 39.0 L 830.7 57.8 L 996.0 84.6'; -const shareArea = `${shareLine} L 996.0 314 L 4 314 Z`; - -// The 4-metric divided row inside the share card (matches the StatStrip values). const shareMetrics = [ - { label: 'Sent', value: '2,847' }, + { label: 'Sent', value: '2,847', sub: 'emails' }, { label: 'Open rate', value: '31.3%' }, - { label: 'Reply rate', value: '2.0%' }, - { label: 'Bounce rate', value: '0.5%' }, -]; - -// daily_trend "sent" series - Dec 2 … Dec 8 -const trend = [ - { date: 'Dec 2', sent: 380 }, - { date: 'Dec 3', sent: 420 }, - { date: 'Dec 4', sent: 350 }, - { date: 'Dec 5', sent: 480 }, - { date: 'Dec 6', sent: 440 }, - { date: 'Dec 7', sent: 410 }, - { date: 'Dec 8', sent: 367 }, -]; -const maxSent = Math.max(...trend.map((p) => p.sent)); - -const statStrip = [ - { label: 'Total sent', value: '2,847', sub: 'in range', accent: false, last: false }, - { label: 'Open rate', value: '31.3%', sub: 'after delivery', accent: true, last: false }, - { label: 'Reply rate', value: '2.0%', sub: 'incl. positive', accent: false, last: false }, - { label: 'Bounce rate', value: '0.5%', sub: 'hard + soft', accent: false, last: true }, -]; - -const metricTabs = [ - { label: 'Sent', active: true }, - { label: 'Opens', active: false }, - { label: 'Clicks', active: false }, - { label: 'Replies', active: false }, -]; - -const breakdown = [ - { label: 'Sent', value: '2,847', dot: 'bg-slate-400' }, - { label: 'Opens', value: '891', dot: 'bg-emerald-500' }, - { label: 'Clicks', value: '234', dot: 'bg-violet-500' }, - { label: 'Replies', value: '56', dot: 'bg-amber-500' }, - { label: 'Bounces', value: '14', dot: 'bg-rose-500' }, -]; - -const health = [ - { n: '6', label: 'Healthy', tone: 'text-emerald-600' }, - { n: '1', label: 'At risk', tone: 'text-amber-600' }, - { n: '1', label: 'Issues', tone: 'text-rose-600' }, -]; - -const campaigns = [ - { name: 'Q4 Outreach - Tech Leaders', status: 'active', sent: '850', open: '38.5%', reply: '2.8%' }, - { name: 'Win-back: Former Customers', status: 'active', sent: '620', open: '26.3%', reply: '1.6%' }, - { name: 'Product Launch Beta Access', status: 'paused', sent: '480', open: '42.1%', reply: '3.1%' }, -]; -const statusDot = (s: string) => - s === 'active' ? 'bg-emerald-500' : s === 'paused' ? 'bg-amber-500' : 'bg-slate-300'; - -// activity icon path (lucide "activity") -const activityPath = ''; -const activity = [ - { tone: 'text-emerald-600', verb: 'Opened by', email: 'sarah.chen@techcorp.com', campaign: 'Q4 Outreach', ts: 'Dec 8, 02:32 PM' }, - { tone: 'text-violet-600', verb: 'Clicked by', email: 'mark.ross@northwind.dev', campaign: 'Win-back', ts: 'Dec 8, 02:18 PM' }, - { tone: 'text-amber-600', verb: 'Reply from', email: 'jess@helio.gen', campaign: 'Product Launch', ts: 'Dec 8, 01:54 PM' }, + { label: 'Reply rate', value: '2.0%' }, + { label: 'Bounce rate', value: '0.5%' }, ]; +// ShareAreaChart: viewBox 1000x320, padX 4, padTop 14, baseline 314. +const sent = series[0].values; +const sMax = Math.max(...sent); +const sPts = sent.map((v, i) => [4 + (i * 992) / (sent.length - 1), 314 - (v / sMax) * 300] as const); +const shareLine = sPts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`).join(' '); +const shareArea = `${shareLine} L ${sPts[sPts.length - 1][0].toFixed(1)} 314 L 4 314 Z`; --- -
- -
+
+ +
Analytics
Deliverability across the workspace -
+
-
- - - +
+ 7d + 30d + 90d
- -
- -
+ +
{statStrip.map((s) => ( -
+
{s.label} {s.accent && } @@ -130,42 +167,57 @@ const activity = [ ))}
- -
- -
-
+ +
+
+
Email performance -
- {metricTabs.map((m) => ( - +
+ {series.map((m) => ( + + + {m.label} + ))} -
+
- -
-
-
- {trend.map((p) => ( -
-
-
+
+ +
+ {grid.map((g) => ( +
+
+ {g.label} +
+ ))} +
- -
- -
- {trend[0].date} - {trend[trend.length - 1].date} + +
+
+ {days[0]} + {days[days.length - 1]}
- -
@@ -180,16 +182,22 @@ const cursorXPct = (cursorX / ROOT_W) * 100;
-
Start date
-
-
End date
- @@ -201,9 +209,10 @@ const cursorXPct = (cursorX / ROOT_W) * 100; -
- - + +
+ +
@@ -216,55 +225,19 @@ const cursorXPct = (cursorX / ROOT_W) * 100; 9:00 - 17:00
- +
+ + diff --git a/site/src/components/TemplatingShowcase.astro b/site/src/components/TemplatingShowcase.astro index c309dd0c0..60a83c744 100644 --- a/site/src/components/TemplatingShowcase.astro +++ b/site/src/components/TemplatingShowcase.astro @@ -98,7 +98,7 @@ const features = [ /> -
diff --git a/site/src/components/UniboxMock.astro b/site/src/components/UniboxMock.astro index f27e37357..c0d284697 100644 --- a/site/src/components/UniboxMock.astro +++ b/site/src/components/UniboxMock.astro @@ -1,4 +1,5 @@ --- +import DemoCursor from './DemoCursor.astro'; /** * Pixel-faithful mock of the Warmbly /app/unibox screen (unified inbox). * @@ -29,15 +30,26 @@ const stats = [ // ── Scope rail (left) ────────────────────────────────────────────── const inboxScopes = [ - { icon: 'inbox', label: 'All', count: '342', active: false, tone: 'muted' }, - { icon: 'sparkle', label: 'Unread', count: '7', active: true, tone: 'accent' }, + { icon: 'inbox', label: 'All', count: '342', active: true, tone: 'muted' }, + { icon: 'sparkle', label: 'Unread', count: '7', active: false, tone: 'accent' }, { icon: 'clock', label: 'Today', count: '24', active: false, tone: 'muted' }, { icon: 'calendar', label: 'This week', count: '89', active: false, tone: 'muted' }, { icon: 'reply', label: 'Awaiting reply', count: '3', active: false, tone: 'accent' }, + { icon: 'sparkles', label: 'Agent drafts', count: '2', active: false, tone: 'accent' }, { icon: 'moon', label: 'Snoozed', count: '2', active: false, tone: 'muted' }, { icon: 'send', label: 'Scheduled', count: '1', active: false, tone: 'accent' }, ]; +// Folders (ScopeRail FolderItem), the inbox selected. +const folders = [ + { label: 'Inbox', active: true, icon: '' }, + { label: 'Drafts', icon: '' }, + { label: 'Sent', icon: '' }, + { label: 'Archive', icon: '' }, + { label: 'Spam', icon: '' }, + { label: 'Trash', icon: '' }, +]; + const mailboxScopes = [ { email: 'hello@company.com', count: '2' }, { email: 'support@company.com', count: '5' }, @@ -55,7 +67,7 @@ const conversations = [ initials: 'SC', hue: 14, sender: 'Sarah Chen', subject: 'Follow up on Q1 proposal', preview: "Thanks for sending over the docs. I've reviewed them and have a few…", - time: '5m', unread: true, selected: true, + time: '5m', unread: true, selected: true, count: 3, mailbox: 'hello@company.com', tags: [{ color: '#ef4444', label: 'Urgent' }, { color: '#f59e0b', label: 'Clients' }], }, @@ -71,7 +83,7 @@ const conversations = [ initials: 'AM', hue: 265, sender: 'Alex Morgan', subject: 'New partnership opportunity', preview: "Hi there, I'm reaching out about a potential partnership between…", - time: '3h', unread: false, selected: false, muted: true, + time: '3h', unread: false, selected: false, mailbox: 'sales@company.com', tags: [{ color: '#3b82f6', label: 'Leads' }], }, @@ -79,7 +91,7 @@ const conversations = [ initials: 'CR', hue: 150, sender: 'Casey Reynolds', subject: '(no subject)', preview: '(no preview)', - time: 'Jan 15', unread: false, selected: false, muted: true, + time: 'Jan 15', unread: false, selected: false, mailbox: 'hello@company.com', tags: [], }, @@ -89,17 +101,17 @@ const conversations = [ const messages = [ { initials: 'SC', name: 'Sarah Chen', addr: 'sarah@acme.io', - to: 'hello@company.com', time: 'Jan 18, 10:32', you: false, + to: 'hello@company.com', time: 'Jan 18, 10:32 AM', collapsed: true, body: "Thanks for sending over the docs. I've reviewed them and have a few questions. Can we sync up tomorrow morning?", }, { - initials: 'YO', name: 'You', addr: 'hello@company.com', - to: 'sarah@acme.io', time: '14:15', you: true, + initials: 'CO', name: 'Company', addr: 'hello@company.com', + to: 'sarah@acme.io', time: 'Jan 18, 02:15 PM', collapsed: true, body: 'Absolutely! How does 10am EST work for you?', }, { initials: 'SC', name: 'Sarah Chen', addr: 'sarah@acme.io', - to: 'hello@company.com', time: '14:28', you: false, + to: 'hello@company.com', time: 'Jan 18, 02:28 PM', collapsed: false, body: 'Perfect. See you then!', }, ]; @@ -110,11 +122,11 @@ const railToneClass = (tone: string, active: boolean) => { return 'text-slate-400'; }; --- -
+
-
+
Inbox
@@ -141,22 +153,44 @@ const railToneClass = (tone: string, active: boolean) => { -
+
- Search inbox… (/) + Search all… (/) + 4 @@ -228,14 +263,14 @@ const railToneClass = (tone: string, active: boolean) => {
-
+
Today 4
{conversations.map((c) => ( -
+
{c.unread && ( )} @@ -247,28 +282,34 @@ const railToneClass = (tone: string, active: boolean) => { >{c.initials}
- {c.sender} + {c.sender} + {'count' in c && {c.count}} {c.time}
-
{c.subject}
+
{c.subject}
{c.preview}
- {c.mailbox} {c.tags.map((t) => ( - {t.label} + {t.label} ))} + {c.mailbox}
))}
+
@@ -279,20 +320,27 @@ const railToneClass = (tone: string, active: boolean) => {
Follow up on Q1 proposal hello@company.com +
- - + + + + + Snooze - + - + - +
@@ -306,9 +354,9 @@ const railToneClass = (tone: string, active: boolean) => {
{messages.map((m) => ( -
-
-
{m.initials}
+
+
+
{m.initials}
{m.name} @@ -318,16 +366,24 @@ const railToneClass = (tone: string, active: boolean) => { to {m.to}
- {m.time} +
+
+ + +
+ {m.time} +
-
{m.body}
+ {m.collapsed + ?
{m.body}
+ :
{m.body}
}
))}
- + Reply @@ -341,87 +397,73 @@ const railToneClass = (tone: string, active: boolean) => {
- -