@startsimpli/ui 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +2 -1
  2. package/src/__mocks__/next/link.js +11 -0
  3. package/src/components/ActivityTimeline.tsx +173 -0
  4. package/src/components/LogActivityDialog.tsx +303 -0
  5. package/src/components/QuickLogButtons.tsx +32 -0
  6. package/src/components/account/__tests__/account.test.tsx +315 -0
  7. package/src/components/badge/StageBadge.tsx +31 -0
  8. package/src/components/badge/index.ts +3 -0
  9. package/src/components/command-palette/CommandGroup.tsx +23 -0
  10. package/src/components/command-palette/CommandPalette.tsx +327 -0
  11. package/src/components/command-palette/CommandResultItem.tsx +59 -0
  12. package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
  13. package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
  14. package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
  15. package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
  16. package/src/components/command-palette/command-palette-context.tsx +51 -0
  17. package/src/components/command-palette/index.ts +9 -0
  18. package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
  19. package/src/components/compose/__tests__/compose.test.tsx +656 -0
  20. package/src/components/compose/compose-header.tsx +72 -0
  21. package/src/components/compose/compose-loading.tsx +13 -0
  22. package/src/components/compose/index.ts +6 -0
  23. package/src/components/compose/save-status-indicator.tsx +57 -0
  24. package/src/components/compose/send-confirmation-dialog.tsx +87 -0
  25. package/src/components/compose/subject-input.tsx +25 -0
  26. package/src/components/compose/useAutoSave.ts +93 -0
  27. package/src/components/dashboard/DashboardGrid.tsx +32 -0
  28. package/src/components/dashboard/DashboardSection.tsx +32 -0
  29. package/src/components/dashboard/MetricCard.tsx +129 -0
  30. package/src/components/dashboard/PeriodSelector.tsx +55 -0
  31. package/src/components/dashboard/PipelineFunnel.tsx +126 -0
  32. package/src/components/dashboard/SparklineTrend.tsx +102 -0
  33. package/src/components/dashboard/TopCampaigns.tsx +132 -0
  34. package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
  35. package/src/components/dashboard/index.ts +20 -0
  36. package/src/components/dialog/ConfirmDialog.tsx +72 -0
  37. package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
  38. package/src/components/dialog/index.ts +3 -0
  39. package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
  40. package/src/components/email-dialogs/index.ts +14 -0
  41. package/src/components/email-dialogs/merge-fields.tsx +196 -0
  42. package/src/components/email-dialogs/preview-dialog.tsx +194 -0
  43. package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
  44. package/src/components/email-dialogs/template-picker.tsx +225 -0
  45. package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
  46. package/src/components/email-editor/BlockRenderer.tsx +120 -0
  47. package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
  48. package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
  49. package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
  50. package/src/components/email-editor/add-block-menu.tsx +151 -0
  51. package/src/components/email-editor/block-toolbar.tsx +73 -0
  52. package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
  53. package/src/components/email-editor/blocks/button-block.tsx +44 -0
  54. package/src/components/email-editor/blocks/divider-block.tsx +43 -0
  55. package/src/components/email-editor/blocks/footer-block.tsx +39 -0
  56. package/src/components/email-editor/blocks/header-block.tsx +39 -0
  57. package/src/components/email-editor/blocks/image-block.tsx +61 -0
  58. package/src/components/email-editor/blocks/index.ts +9 -0
  59. package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
  60. package/src/components/email-editor/blocks/social-block.tsx +75 -0
  61. package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
  62. package/src/components/email-editor/blocks/text-block.tsx +75 -0
  63. package/src/components/email-editor/editor-sidebar.tsx +66 -0
  64. package/src/components/email-editor/email-editor.tsx +497 -0
  65. package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
  66. package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
  67. package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
  68. package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
  69. package/src/components/email-editor/index.ts +51 -0
  70. package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
  71. package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
  72. package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
  73. package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
  74. package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
  75. package/src/components/email-editor/panels/index.ts +3 -0
  76. package/src/components/email-editor/renderer/block-renderers.ts +209 -0
  77. package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
  78. package/src/components/email-editor/types.ts +413 -0
  79. package/src/components/email-editor/utils/defaults.ts +116 -0
  80. package/src/components/email-editor/utils/undo-redo.ts +59 -0
  81. package/src/components/enrichment/EnrichButton.tsx +33 -0
  82. package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
  83. package/src/components/enrichment/QualityBadge.tsx +43 -0
  84. package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
  85. package/src/components/enrichment/index.ts +8 -0
  86. package/src/components/gantt/GanttBoardView.tsx +71 -0
  87. package/src/components/gantt/GanttChart.tsx +140 -887
  88. package/src/components/gantt/GanttFilterBar.tsx +100 -0
  89. package/src/components/gantt/GanttListView.tsx +63 -0
  90. package/src/components/gantt/GanttTimelineView.tsx +215 -0
  91. package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
  92. package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
  93. package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
  94. package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
  95. package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
  96. package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
  97. package/src/components/gantt/hooks/useGanttState.ts +644 -0
  98. package/src/components/gantt/index.ts +10 -0
  99. package/src/components/gantt/types.ts +5 -5
  100. package/src/components/index.ts +46 -0
  101. package/src/components/integrations/ConnectionStatus.tsx +77 -0
  102. package/src/components/integrations/IntegrationCard.tsx +92 -0
  103. package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
  104. package/src/components/integrations/index.ts +5 -0
  105. package/src/components/kanban/KanbanBoard.tsx +103 -0
  106. package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
  107. package/src/components/kanban/index.ts +2 -0
  108. package/src/components/lists/CreateListDialog.tsx +158 -0
  109. package/src/components/lists/ListCard.tsx +77 -0
  110. package/src/components/lists/__tests__/lists.test.tsx +263 -0
  111. package/src/components/lists/index.ts +5 -0
  112. package/src/components/loading/__tests__/loading.test.tsx +114 -0
  113. package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
  114. package/src/components/pipeline/StageTransitionModal.tsx +146 -0
  115. package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
  116. package/src/components/pipeline/index.ts +2 -0
  117. package/src/components/settings/SettingsCard.tsx +33 -0
  118. package/src/components/settings/SettingsLayout.tsx +28 -0
  119. package/src/components/settings/SettingsNav.tsx +42 -0
  120. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  121. package/src/components/settings/index.ts +6 -0
  122. package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
@@ -0,0 +1,14 @@
1
+ export { ScheduleDialog } from './schedule-dialog'
2
+ export { TestSendDialog } from './test-send-dialog'
3
+ export { PreviewDialog } from './preview-dialog'
4
+ export type { PreviewRecipient } from './preview-dialog'
5
+ export { TemplatePicker } from './template-picker'
6
+ export type { EmailTemplate } from './template-picker'
7
+ export {
8
+ MergeFieldsMenu,
9
+ MergeFieldPreview,
10
+ replaceMergeFields,
11
+ DEFAULT_MERGE_FIELDS,
12
+ DEFAULT_CATEGORIES,
13
+ } from './merge-fields'
14
+ export type { MergeField, MergeFieldCategory } from './merge-fields'
@@ -0,0 +1,196 @@
1
+ 'use client'
2
+
3
+ import { Button } from '../ui/button'
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuLabel,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuTrigger,
11
+ } from '../ui/dropdown-menu'
12
+ import { Braces } from 'lucide-react'
13
+
14
+ export interface MergeField {
15
+ key: string
16
+ label: string
17
+ example: string
18
+ category: string
19
+ }
20
+
21
+ export interface MergeFieldCategory {
22
+ name: string
23
+ label: string
24
+ }
25
+
26
+ /** Default merge field categories for email composition */
27
+ export const DEFAULT_CATEGORIES: MergeFieldCategory[] = [
28
+ { name: 'recipient', label: 'Recipient' },
29
+ { name: 'organization', label: 'Organization' },
30
+ { name: 'sender', label: 'Sender' },
31
+ { name: 'date', label: 'Date' },
32
+ ]
33
+
34
+ /** Default merge fields for email composition */
35
+ export const DEFAULT_MERGE_FIELDS: MergeField[] = [
36
+ // Recipient fields
37
+ { key: '{{recipient.firstName}}', label: 'First Name', example: 'John', category: 'recipient' },
38
+ { key: '{{recipient.lastName}}', label: 'Last Name', example: 'Smith', category: 'recipient' },
39
+ { key: '{{recipient.fullName}}', label: 'Full Name', example: 'John Smith', category: 'recipient' },
40
+ { key: '{{recipient.email}}', label: 'Email', example: 'john@example.com', category: 'recipient' },
41
+ { key: '{{recipient.title}}', label: 'Title', example: 'Partner', category: 'recipient' },
42
+
43
+ // Organization fields
44
+ { key: '{{organization.name}}', label: 'Organization', example: 'Acme Corp', category: 'organization' },
45
+ { key: '{{organization.type}}', label: 'Type', example: 'Enterprise', category: 'organization' },
46
+
47
+ // Sender fields
48
+ { key: '{{sender.name}}', label: 'Your Company', example: 'My Company', category: 'sender' },
49
+ { key: '{{sender.contactName}}', label: 'Your Name', example: 'Jane Doe', category: 'sender' },
50
+
51
+ // Date fields
52
+ { key: '{{date.today}}', label: 'Today', example: 'December 8, 2024', category: 'date' },
53
+ { key: '{{date.month}}', label: 'Current Month', example: 'December', category: 'date' },
54
+ { key: '{{date.quarter}}', label: 'Current Quarter', example: 'Q4 2024', category: 'date' },
55
+ { key: '{{date.year}}', label: 'Current Year', example: '2024', category: 'date' },
56
+ ]
57
+
58
+ interface MergeFieldsMenuProps {
59
+ onInsert: (field: string) => void
60
+ variant?: 'default' | 'compact'
61
+ /** Custom merge fields to display. Defaults to DEFAULT_MERGE_FIELDS. */
62
+ fields?: MergeField[]
63
+ /** Custom categories for grouping. Defaults to DEFAULT_CATEGORIES. */
64
+ categories?: MergeFieldCategory[]
65
+ }
66
+
67
+ export function MergeFieldsMenu({
68
+ onInsert,
69
+ variant = 'default',
70
+ fields = DEFAULT_MERGE_FIELDS,
71
+ categories = DEFAULT_CATEGORIES,
72
+ }: MergeFieldsMenuProps) {
73
+ const fieldsByCategory = categories.map((cat) => ({
74
+ ...cat,
75
+ fields: fields.filter((f) => f.category === cat.name),
76
+ }))
77
+
78
+ return (
79
+ <DropdownMenu>
80
+ <DropdownMenuTrigger asChild>
81
+ {variant === 'compact' ? (
82
+ <Button variant="ghost" size="sm" className="h-8 gap-1">
83
+ <Braces className="h-4 w-4" />
84
+ <span className="text-xs">Merge</span>
85
+ </Button>
86
+ ) : (
87
+ <Button variant="outline" size="sm">
88
+ <Braces className="mr-2 h-4 w-4" />
89
+ Insert Merge Field
90
+ </Button>
91
+ )}
92
+ </DropdownMenuTrigger>
93
+ <DropdownMenuContent align="start" className="w-64">
94
+ {fieldsByCategory.map((cat, idx) => (
95
+ <div key={cat.name}>
96
+ {idx > 0 && <DropdownMenuSeparator />}
97
+ <DropdownMenuLabel>{cat.label}</DropdownMenuLabel>
98
+ {cat.fields.map((field) => (
99
+ <MergeFieldItem key={field.key} field={field} onInsert={onInsert} />
100
+ ))}
101
+ </div>
102
+ ))}
103
+ </DropdownMenuContent>
104
+ </DropdownMenu>
105
+ )
106
+ }
107
+
108
+ function MergeFieldItem({ field, onInsert }: { field: MergeField; onInsert: (field: string) => void }) {
109
+ return (
110
+ <DropdownMenuItem onClick={() => onInsert(field.key)} className="flex justify-between">
111
+ <span>{field.label}</span>
112
+ <span className="text-xs text-muted-foreground">{field.example}</span>
113
+ </DropdownMenuItem>
114
+ )
115
+ }
116
+
117
+ // Preview component to show merge fields as visual tokens
118
+ interface MergeFieldPreviewProps {
119
+ content: string
120
+ sampleData?: Record<string, string>
121
+ }
122
+
123
+ /** Replace merge field tokens in content with sample data for previewing */
124
+ export function MergeFieldPreview({ content, sampleData }: MergeFieldPreviewProps) {
125
+ const defaultSampleData: Record<string, string> = {
126
+ '{{recipient.firstName}}': 'John',
127
+ '{{recipient.lastName}}': 'Smith',
128
+ '{{recipient.fullName}}': 'John Smith',
129
+ '{{recipient.email}}': 'john@example.com',
130
+ '{{recipient.title}}': 'Partner',
131
+ '{{organization.name}}': 'Acme Corp',
132
+ '{{organization.type}}': 'Enterprise',
133
+ '{{sender.name}}': 'My Company',
134
+ '{{sender.contactName}}': 'Jane Doe',
135
+ '{{date.today}}': new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }),
136
+ '{{date.month}}': new Date().toLocaleDateString('en-US', { month: 'long' }),
137
+ '{{date.quarter}}': `Q${Math.ceil((new Date().getMonth() + 1) / 3)} ${new Date().getFullYear()}`,
138
+ '{{date.year}}': new Date().getFullYear().toString(),
139
+ }
140
+
141
+ const data = { ...defaultSampleData, ...sampleData }
142
+
143
+ let preview = content
144
+ for (const [key, value] of Object.entries(data)) {
145
+ preview = preview.replace(new RegExp(key.replace(/[{}]/g, '\\$&'), 'g'), value)
146
+ }
147
+
148
+ return preview
149
+ }
150
+
151
+ /** Replace merge fields in content with actual recipient data for sending */
152
+ export function replaceMergeFields(
153
+ content: string,
154
+ recipient: {
155
+ firstName?: string | null
156
+ lastName?: string | null
157
+ name?: string | null
158
+ email?: string | null
159
+ title?: string | null
160
+ },
161
+ organization?: {
162
+ name?: string | null
163
+ type?: string | null
164
+ },
165
+ sender?: {
166
+ name?: string | null
167
+ contactName?: string | null
168
+ }
169
+ ): string {
170
+ const firstName = recipient.firstName || recipient.name?.split(' ')[0] || ''
171
+ const lastName = recipient.lastName || recipient.name?.split(' ').slice(1).join(' ') || ''
172
+ const fullName = recipient.name || `${firstName} ${lastName}`.trim()
173
+
174
+ const replacements: Record<string, string> = {
175
+ '{{recipient.firstName}}': firstName,
176
+ '{{recipient.lastName}}': lastName,
177
+ '{{recipient.fullName}}': fullName,
178
+ '{{recipient.email}}': recipient.email || '',
179
+ '{{recipient.title}}': recipient.title || '',
180
+ '{{organization.name}}': organization?.name || '',
181
+ '{{organization.type}}': organization?.type || '',
182
+ '{{sender.name}}': sender?.name || '',
183
+ '{{sender.contactName}}': sender?.contactName || '',
184
+ '{{date.today}}': new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }),
185
+ '{{date.month}}': new Date().toLocaleDateString('en-US', { month: 'long' }),
186
+ '{{date.quarter}}': `Q${Math.ceil((new Date().getMonth() + 1) / 3)} ${new Date().getFullYear()}`,
187
+ '{{date.year}}': new Date().getFullYear().toString(),
188
+ }
189
+
190
+ let result = content
191
+ for (const [key, value] of Object.entries(replacements)) {
192
+ result = result.replace(new RegExp(key.replace(/[{}]/g, '\\$&'), 'g'), value)
193
+ }
194
+
195
+ return result
196
+ }
@@ -0,0 +1,194 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from '../ui/dialog'
10
+ import { Button } from '../ui/button'
11
+ import { Label } from '../ui/label'
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '../ui/select'
19
+ import { Tabs, TabsList, TabsTrigger } from '../ui/tabs'
20
+ import { Monitor, Smartphone, Code } from 'lucide-react'
21
+ import { MergeFieldPreview } from './merge-fields'
22
+ import { cn } from '../../utils/cn'
23
+
24
+ export interface PreviewRecipient {
25
+ id: string
26
+ name: string
27
+ label?: string
28
+ /** Sample data for merge field substitution, keyed by merge field token (e.g. "{{recipient.firstName}}") */
29
+ mergeData?: Record<string, string>
30
+ }
31
+
32
+ interface PreviewDialogProps {
33
+ open: boolean
34
+ onOpenChange: (open: boolean) => void
35
+ subject: string
36
+ /**
37
+ * Pre-rendered email body HTML. The component will wrap this in an email
38
+ * shell with proper styling for preview.
39
+ */
40
+ bodyHtml: string
41
+ /** Recipients available for merge field preview */
42
+ recipients?: PreviewRecipient[]
43
+ /** Additional merge field sample data applied on top of recipient data */
44
+ sampleData?: Record<string, string>
45
+ /** Unsubscribe link text. Set to null to hide. */
46
+ unsubscribeText?: string | null
47
+ }
48
+
49
+ // Default sample recipient for preview
50
+ const DEFAULT_RECIPIENT: PreviewRecipient = {
51
+ id: 'sample',
52
+ name: 'John Smith',
53
+ mergeData: {
54
+ '{{recipient.firstName}}': 'John',
55
+ '{{recipient.lastName}}': 'Smith',
56
+ '{{recipient.fullName}}': 'John Smith',
57
+ '{{recipient.email}}': 'john@example.com',
58
+ },
59
+ }
60
+
61
+ export function PreviewDialog({
62
+ open,
63
+ onOpenChange,
64
+ subject,
65
+ bodyHtml,
66
+ recipients = [],
67
+ sampleData,
68
+ unsubscribeText = 'Unsubscribe from these updates',
69
+ }: PreviewDialogProps) {
70
+ const [viewMode, setViewMode] = useState<'desktop' | 'mobile' | 'html'>('desktop')
71
+ const [selectedRecipientId, setSelectedRecipientId] = useState<string>('sample')
72
+
73
+ const previewRecipients = recipients.length > 0 ? recipients : [DEFAULT_RECIPIENT]
74
+ const selectedRecipient = previewRecipients.find(r => r.id === selectedRecipientId) || previewRecipients[0]
75
+
76
+ // Build merge data from selected recipient + any extra sample data
77
+ const mergeDataForPreview: Record<string, string> = {
78
+ ...selectedRecipient.mergeData,
79
+ '{{date.today}}': new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }),
80
+ '{{date.month}}': new Date().toLocaleDateString('en-US', { month: 'long' }),
81
+ '{{date.quarter}}': `Q${Math.ceil((new Date().getMonth() + 1) / 3)} ${new Date().getFullYear()}`,
82
+ '{{date.year}}': new Date().getFullYear().toString(),
83
+ ...sampleData,
84
+ }
85
+
86
+ const previewSubject = MergeFieldPreview({ content: subject, sampleData: mergeDataForPreview })
87
+ const previewBody = MergeFieldPreview({ content: bodyHtml, sampleData: mergeDataForPreview })
88
+
89
+ const fullEmailHtml = `
90
+ <!DOCTYPE html>
91
+ <html>
92
+ <head>
93
+ <meta charset="utf-8">
94
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
95
+ <title>${previewSubject}</title>
96
+ </head>
97
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f3f4f6;">
98
+ <div style="max-width: 600px; margin: 0 auto; padding: 24px;">
99
+ <div style="background: white; border-radius: 8px; padding: 32px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
100
+ ${previewBody}
101
+ </div>
102
+ ${unsubscribeText ? `
103
+ <div style="text-align: center; padding: 24px; font-size: 12px; color: #6b7280;">
104
+ <a href="#" style="color: #6b7280;">${unsubscribeText}</a>
105
+ </div>
106
+ ` : ''}
107
+ </div>
108
+ </body>
109
+ </html>
110
+ `.trim()
111
+
112
+ return (
113
+ <Dialog open={open} onOpenChange={onOpenChange}>
114
+ <DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
115
+ <DialogHeader>
116
+ <DialogTitle>Preview Email</DialogTitle>
117
+ </DialogHeader>
118
+
119
+ <div className="flex items-center justify-between gap-4 pb-4 border-b">
120
+ <div className="flex items-center gap-4">
121
+ <div className="space-y-1">
122
+ <Label className="text-xs text-muted-foreground">Preview as</Label>
123
+ <Select value={selectedRecipientId} onValueChange={setSelectedRecipientId}>
124
+ <SelectTrigger className="w-[200px]">
125
+ <SelectValue />
126
+ </SelectTrigger>
127
+ <SelectContent>
128
+ {previewRecipients.map(r => (
129
+ <SelectItem key={r.id} value={r.id}>
130
+ {r.label || r.name}
131
+ </SelectItem>
132
+ ))}
133
+ </SelectContent>
134
+ </Select>
135
+ </div>
136
+ </div>
137
+
138
+ <Tabs value={viewMode} onValueChange={(v) => setViewMode(v as typeof viewMode)}>
139
+ <TabsList>
140
+ <TabsTrigger value="desktop" className="gap-2">
141
+ <Monitor className="h-4 w-4" />
142
+ Desktop
143
+ </TabsTrigger>
144
+ <TabsTrigger value="mobile" className="gap-2">
145
+ <Smartphone className="h-4 w-4" />
146
+ Mobile
147
+ </TabsTrigger>
148
+ <TabsTrigger value="html" className="gap-2">
149
+ <Code className="h-4 w-4" />
150
+ HTML
151
+ </TabsTrigger>
152
+ </TabsList>
153
+ </Tabs>
154
+ </div>
155
+
156
+ <div className="flex-1 overflow-auto">
157
+ {/* Subject line preview */}
158
+ <div className="p-4 bg-muted/50 rounded-lg mb-4">
159
+ <Label className="text-xs text-muted-foreground">Subject</Label>
160
+ <p className="font-medium mt-1">{previewSubject}</p>
161
+ </div>
162
+
163
+ {viewMode === 'html' ? (
164
+ <div className="bg-muted rounded-lg p-4 overflow-auto">
165
+ <pre className="text-xs whitespace-pre-wrap font-mono">{fullEmailHtml}</pre>
166
+ </div>
167
+ ) : (
168
+ <div
169
+ className={cn(
170
+ 'bg-gray-100 rounded-lg p-4 mx-auto transition-all',
171
+ viewMode === 'mobile' ? 'max-w-[375px]' : 'max-w-full'
172
+ )}
173
+ >
174
+ <div className="bg-white rounded-lg shadow-sm overflow-hidden">
175
+ <iframe
176
+ srcDoc={fullEmailHtml}
177
+ className="w-full border-0"
178
+ style={{ height: viewMode === 'mobile' ? '500px' : '400px' }}
179
+ title="Email preview"
180
+ />
181
+ </div>
182
+ </div>
183
+ )}
184
+ </div>
185
+
186
+ <div className="flex justify-end gap-2 pt-4 border-t">
187
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
188
+ Close
189
+ </Button>
190
+ </div>
191
+ </DialogContent>
192
+ </Dialog>
193
+ )
194
+ }