@startsimpli/ui 0.4.6 → 0.4.7

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 (69) hide show
  1. package/package.json +2 -1
  2. package/src/components/ActivityTimeline.tsx +173 -0
  3. package/src/components/LogActivityDialog.tsx +303 -0
  4. package/src/components/QuickLogButtons.tsx +32 -0
  5. package/src/components/badge/StageBadge.tsx +31 -0
  6. package/src/components/badge/index.ts +3 -0
  7. package/src/components/command-palette/CommandPalette.tsx +344 -0
  8. package/src/components/command-palette/command-palette-context.tsx +51 -0
  9. package/src/components/command-palette/index.ts +3 -0
  10. package/src/components/compose/compose-header.tsx +72 -0
  11. package/src/components/compose/compose-loading.tsx +13 -0
  12. package/src/components/compose/index.ts +6 -0
  13. package/src/components/compose/save-status-indicator.tsx +57 -0
  14. package/src/components/compose/send-confirmation-dialog.tsx +87 -0
  15. package/src/components/compose/subject-input.tsx +25 -0
  16. package/src/components/compose/useAutoSave.ts +93 -0
  17. package/src/components/dashboard/DashboardGrid.tsx +32 -0
  18. package/src/components/dashboard/DashboardSection.tsx +32 -0
  19. package/src/components/dashboard/MetricCard.tsx +129 -0
  20. package/src/components/dashboard/PeriodSelector.tsx +55 -0
  21. package/src/components/dashboard/SparklineTrend.tsx +102 -0
  22. package/src/components/dashboard/index.ts +14 -0
  23. package/src/components/email-dialogs/index.ts +14 -0
  24. package/src/components/email-dialogs/merge-fields.tsx +196 -0
  25. package/src/components/email-dialogs/preview-dialog.tsx +194 -0
  26. package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
  27. package/src/components/email-dialogs/template-picker.tsx +225 -0
  28. package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
  29. package/src/components/email-editor/add-block-menu.tsx +151 -0
  30. package/src/components/email-editor/block-toolbar.tsx +73 -0
  31. package/src/components/email-editor/blocks/button-block.tsx +44 -0
  32. package/src/components/email-editor/blocks/divider-block.tsx +43 -0
  33. package/src/components/email-editor/blocks/footer-block.tsx +39 -0
  34. package/src/components/email-editor/blocks/header-block.tsx +39 -0
  35. package/src/components/email-editor/blocks/image-block.tsx +61 -0
  36. package/src/components/email-editor/blocks/index.ts +9 -0
  37. package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
  38. package/src/components/email-editor/blocks/social-block.tsx +75 -0
  39. package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
  40. package/src/components/email-editor/blocks/text-block.tsx +75 -0
  41. package/src/components/email-editor/editor-sidebar.tsx +791 -0
  42. package/src/components/email-editor/email-editor.tsx +886 -0
  43. package/src/components/email-editor/index.ts +50 -0
  44. package/src/components/email-editor/renderer/block-renderers.ts +209 -0
  45. package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
  46. package/src/components/email-editor/types.ts +413 -0
  47. package/src/components/email-editor/utils/defaults.ts +116 -0
  48. package/src/components/email-editor/utils/undo-redo.ts +59 -0
  49. package/src/components/enrichment/EnrichButton.tsx +33 -0
  50. package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
  51. package/src/components/enrichment/QualityBadge.tsx +43 -0
  52. package/src/components/enrichment/index.ts +8 -0
  53. package/src/components/gantt/GanttChart.tsx +25 -25
  54. package/src/components/gantt/types.ts +5 -5
  55. package/src/components/index.ts +46 -0
  56. package/src/components/integrations/ConnectionStatus.tsx +77 -0
  57. package/src/components/integrations/IntegrationCard.tsx +92 -0
  58. package/src/components/integrations/index.ts +5 -0
  59. package/src/components/kanban/KanbanBoard.tsx +103 -0
  60. package/src/components/kanban/index.ts +2 -0
  61. package/src/components/lists/CreateListDialog.tsx +158 -0
  62. package/src/components/lists/ListCard.tsx +77 -0
  63. package/src/components/lists/index.ts +5 -0
  64. package/src/components/pipeline/StageTransitionModal.tsx +146 -0
  65. package/src/components/pipeline/index.ts +2 -0
  66. package/src/components/settings/SettingsCard.tsx +33 -0
  67. package/src/components/settings/SettingsLayout.tsx +28 -0
  68. package/src/components/settings/SettingsNav.tsx +42 -0
  69. package/src/components/settings/index.ts +6 -0
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { DividerBlock as DividerBlockType } from '../types'
4
+
5
+ interface DividerBlockProps {
6
+ block: DividerBlockType
7
+ onChange: (block: DividerBlockType) => void
8
+ isEditing?: boolean
9
+ }
10
+
11
+ export function DividerBlockEditor({ block, onChange, isEditing = true }: DividerBlockProps) {
12
+ const color = block.color || '#d1d5db'
13
+ const thickness = block.thickness || 1
14
+ const width = block.width || 100
15
+
16
+ if (!isEditing) {
17
+ if (block.dividerStyle === 'space') {
18
+ return <div style={{ height: 32 }} />
19
+ }
20
+ return (
21
+ <div className="my-4" style={{ textAlign: 'center' }}>
22
+ <hr
23
+ style={{
24
+ display: 'inline-block',
25
+ width: `${width}%`,
26
+ border: 'none',
27
+ borderTop: `${thickness}px ${block.dividerStyle} ${color}`,
28
+ margin: 0,
29
+ }}
30
+ />
31
+ </div>
32
+ )
33
+ }
34
+
35
+ return (
36
+ <div className="p-4 space-y-3">
37
+ {/* Preview */}
38
+ <div className="py-2">
39
+ <DividerBlockEditor block={block} onChange={onChange} isEditing={false} />
40
+ </div>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { FooterBlock as FooterBlockType } from '../types'
4
+
5
+ interface FooterBlockProps {
6
+ block: FooterBlockType
7
+ onChange: (block: FooterBlockType) => void
8
+ isEditing?: boolean
9
+ }
10
+
11
+ export function FooterBlockEditor({ block, onChange, isEditing = true }: FooterBlockProps) {
12
+ const alignClass =
13
+ block.alignment === 'left'
14
+ ? 'text-left'
15
+ : block.alignment === 'right'
16
+ ? 'text-right'
17
+ : 'text-center'
18
+
19
+ return (
20
+ <div className={`py-4 ${alignClass}`}>
21
+ <div className="text-xs text-gray-500 space-y-1">
22
+ <p>{block.companyName}</p>
23
+ {block.address && <p>{block.address}</p>}
24
+ {block.showUnsubscribe && (
25
+ <p className="mt-2">
26
+ <a
27
+ href={block.unsubscribeUrl || '#'}
28
+ className="text-gray-500 underline"
29
+ onClick={isEditing ? (e) => e.preventDefault() : undefined}
30
+ >
31
+ Unsubscribe
32
+ </a>
33
+ {' '}from these emails
34
+ </p>
35
+ )}
36
+ </div>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { HeaderBlock as HeaderBlockType } from '../types'
4
+ import { Building2 } from 'lucide-react'
5
+
6
+ interface HeaderBlockProps {
7
+ block: HeaderBlockType
8
+ onChange: (block: HeaderBlockType) => void
9
+ isEditing?: boolean
10
+ }
11
+
12
+ export function HeaderBlockEditor({ block, onChange, isEditing = true }: HeaderBlockProps) {
13
+ const alignClass =
14
+ block.alignment === 'left'
15
+ ? 'text-left'
16
+ : block.alignment === 'right'
17
+ ? 'text-right'
18
+ : 'text-center'
19
+
20
+ return (
21
+ <div className={`py-4 ${alignClass}`}>
22
+ <div className="inline-flex items-center gap-3">
23
+ {block.logoUrl ? (
24
+ // eslint-disable-next-line @next/next/no-img-element
25
+ <img
26
+ src={block.logoUrl}
27
+ alt={block.companyName}
28
+ className="h-10 w-auto"
29
+ />
30
+ ) : (
31
+ <div className="h-10 w-10 rounded bg-gray-100 flex items-center justify-center">
32
+ <Building2 className="h-5 w-5 text-gray-400" />
33
+ </div>
34
+ )}
35
+ <span className="text-xl font-semibold">{block.companyName}</span>
36
+ </div>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ import { ImageBlock as ImageBlockType } from '../types'
4
+ import { ImageIcon } from 'lucide-react'
5
+
6
+ interface ImageBlockProps {
7
+ block: ImageBlockType
8
+ onChange: (block: ImageBlockType) => void
9
+ isEditing?: boolean
10
+ }
11
+
12
+ export function ImageBlockEditor({ block, onChange, isEditing = true }: ImageBlockProps) {
13
+ const alignClass =
14
+ block.alignment === 'left'
15
+ ? 'text-left'
16
+ : block.alignment === 'right'
17
+ ? 'text-right'
18
+ : 'text-center'
19
+
20
+ const widthPct = block.width || 100
21
+
22
+ if (!block.url) {
23
+ return (
24
+ <div className={`py-4 ${alignClass}`}>
25
+ <div
26
+ className="inline-flex flex-col items-center justify-center bg-gray-100 rounded-lg p-8"
27
+ style={{ width: `${widthPct}%`, maxWidth: '100%' }}
28
+ >
29
+ <ImageIcon className="h-12 w-12 text-gray-400" />
30
+ <p className="text-sm text-gray-500 mt-2">
31
+ {isEditing ? 'Click to set image URL' : 'No image set'}
32
+ </p>
33
+ </div>
34
+ </div>
35
+ )
36
+ }
37
+
38
+ const imgElement = (
39
+ // eslint-disable-next-line @next/next/no-img-element
40
+ <img
41
+ src={block.url}
42
+ alt={block.alt || 'Email image'}
43
+ style={{ width: `${widthPct}%`, maxWidth: '100%', borderRadius: 4 }}
44
+ />
45
+ )
46
+
47
+ return (
48
+ <div className={`py-2 ${alignClass}`}>
49
+ {block.linkUrl && !isEditing ? (
50
+ <a href={block.linkUrl} target="_blank" rel="noopener noreferrer">
51
+ {imgElement}
52
+ </a>
53
+ ) : (
54
+ imgElement
55
+ )}
56
+ {block.caption && (
57
+ <p className="text-sm text-gray-500 mt-1">{block.caption}</p>
58
+ )}
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,9 @@
1
+ export { TextBlockEditor } from './text-block'
2
+ export { MetricsBlockEditor } from './metrics-block'
3
+ export { DividerBlockEditor } from './divider-block'
4
+ export { ButtonBlockEditor } from './button-block'
5
+ export { ImageBlockEditor } from './image-block'
6
+ export { SpacerBlockEditor } from './spacer-block'
7
+ export { SocialBlockEditor } from './social-block'
8
+ export { HeaderBlockEditor } from './header-block'
9
+ export { FooterBlockEditor } from './footer-block'
@@ -0,0 +1,198 @@
1
+ 'use client'
2
+
3
+ import { MetricsBlock as MetricsBlockType, MetricItem } from '../types'
4
+ import { Button } from '../../ui/button'
5
+ import { Input } from '../../ui/input'
6
+ import { Label } from '../../ui/label'
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '../../ui/select'
14
+ import { Plus, Trash2, TrendingUp, TrendingDown, Minus } from 'lucide-react'
15
+ import { cn } from '../../../lib/utils'
16
+
17
+ interface MetricsBlockProps {
18
+ block: MetricsBlockType
19
+ onChange: (block: MetricsBlockType) => void
20
+ isEditing?: boolean
21
+ }
22
+
23
+ export function MetricsBlockEditor({ block, onChange, isEditing = true }: MetricsBlockProps) {
24
+ const updateMetric = (metricId: string, updates: Partial<MetricItem>) => {
25
+ onChange({
26
+ ...block,
27
+ metrics: block.metrics.map((m) =>
28
+ m.id === metricId ? { ...m, ...updates } : m
29
+ ),
30
+ })
31
+ }
32
+
33
+ const addMetric = () => {
34
+ const newMetric: MetricItem = {
35
+ id: `metric-${Date.now()}`,
36
+ label: 'New Metric',
37
+ value: '0',
38
+ change: '+0%',
39
+ changeType: 'neutral',
40
+ }
41
+ onChange({ ...block, metrics: [...block.metrics, newMetric] })
42
+ }
43
+
44
+ const removeMetric = (metricId: string) => {
45
+ onChange({ ...block, metrics: block.metrics.filter((m) => m.id !== metricId) })
46
+ }
47
+
48
+ const getChangeColor = (type?: 'positive' | 'negative' | 'neutral') => {
49
+ switch (type) {
50
+ case 'positive':
51
+ return 'text-green-600'
52
+ case 'negative':
53
+ return 'text-red-600'
54
+ default:
55
+ return 'text-gray-500'
56
+ }
57
+ }
58
+
59
+ const getChangeIcon = (type?: 'positive' | 'negative' | 'neutral') => {
60
+ switch (type) {
61
+ case 'positive':
62
+ return <TrendingUp className="h-3 w-3 text-green-600" />
63
+ case 'negative':
64
+ return <TrendingDown className="h-3 w-3 text-red-600" />
65
+ default:
66
+ return <Minus className="h-3 w-3 text-gray-400" />
67
+ }
68
+ }
69
+
70
+ if (!isEditing) {
71
+ return (
72
+ <div className="py-4">
73
+ {block.title && (
74
+ <h3 className="text-lg font-semibold mb-4 text-center">{block.title}</h3>
75
+ )}
76
+ <div
77
+ className={cn(
78
+ 'grid gap-4',
79
+ block.columns === 2 && 'grid-cols-2',
80
+ block.columns === 3 && 'grid-cols-3',
81
+ block.columns === 4 && 'grid-cols-4'
82
+ )}
83
+ >
84
+ {block.metrics.map((metric) => (
85
+ <div key={metric.id} className="text-center p-4 bg-gray-50 rounded-lg">
86
+ <div className="text-2xl font-bold">{metric.value}</div>
87
+ <div className="text-sm text-muted-foreground">{metric.label}</div>
88
+ {metric.change && (
89
+ <div
90
+ className={cn(
91
+ 'flex items-center justify-center gap-1 text-sm mt-1',
92
+ getChangeColor(metric.changeType)
93
+ )}
94
+ >
95
+ {getChangeIcon(metric.changeType)}
96
+ {metric.change}
97
+ </div>
98
+ )}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ return (
107
+ <div className="space-y-4 p-4">
108
+ <div className="grid grid-cols-2 gap-4">
109
+ <div className="space-y-2">
110
+ <Label className="text-xs">Section Title</Label>
111
+ <Input
112
+ value={block.title || ''}
113
+ onChange={(e) => onChange({ ...block, title: e.target.value })}
114
+ placeholder="Key Metrics"
115
+ />
116
+ </div>
117
+ <div className="space-y-2">
118
+ <Label className="text-xs">Columns</Label>
119
+ <Select
120
+ value={block.columns.toString()}
121
+ onValueChange={(v) =>
122
+ onChange({ ...block, columns: parseInt(v) as 2 | 3 | 4 })
123
+ }
124
+ >
125
+ <SelectTrigger>
126
+ <SelectValue />
127
+ </SelectTrigger>
128
+ <SelectContent>
129
+ <SelectItem value="2">2 columns</SelectItem>
130
+ <SelectItem value="3">3 columns</SelectItem>
131
+ <SelectItem value="4">4 columns</SelectItem>
132
+ </SelectContent>
133
+ </Select>
134
+ </div>
135
+ </div>
136
+
137
+ <div className="space-y-2">
138
+ {block.metrics.map((metric) => (
139
+ <div
140
+ key={metric.id}
141
+ className="flex items-start gap-2 p-2 border rounded bg-background"
142
+ >
143
+ <div className="flex-1 grid grid-cols-4 gap-2">
144
+ <Input
145
+ value={metric.label}
146
+ onChange={(e) => updateMetric(metric.id, { label: e.target.value })}
147
+ placeholder="Label"
148
+ className="text-xs"
149
+ />
150
+ <Input
151
+ value={metric.value}
152
+ onChange={(e) => updateMetric(metric.id, { value: e.target.value })}
153
+ placeholder="Value"
154
+ className="text-xs"
155
+ />
156
+ <Input
157
+ value={metric.change || ''}
158
+ onChange={(e) => updateMetric(metric.id, { change: e.target.value })}
159
+ placeholder="+15%"
160
+ className="text-xs"
161
+ />
162
+ <Select
163
+ value={metric.changeType || 'neutral'}
164
+ onValueChange={(v) =>
165
+ updateMetric(metric.id, {
166
+ changeType: v as 'positive' | 'negative' | 'neutral',
167
+ })
168
+ }
169
+ >
170
+ <SelectTrigger className="text-xs">
171
+ <SelectValue />
172
+ </SelectTrigger>
173
+ <SelectContent>
174
+ <SelectItem value="positive">Positive</SelectItem>
175
+ <SelectItem value="negative">Negative</SelectItem>
176
+ <SelectItem value="neutral">Neutral</SelectItem>
177
+ </SelectContent>
178
+ </Select>
179
+ </div>
180
+ <Button
181
+ variant="ghost"
182
+ size="icon"
183
+ className="h-8 w-8"
184
+ onClick={() => removeMetric(metric.id)}
185
+ disabled={block.metrics.length <= 1}
186
+ >
187
+ <Trash2 className="h-3 w-3 text-muted-foreground" />
188
+ </Button>
189
+ </div>
190
+ ))}
191
+ <Button variant="outline" size="sm" onClick={addMetric} className="w-full">
192
+ <Plus className="mr-2 h-3 w-3" />
193
+ Add Metric
194
+ </Button>
195
+ </div>
196
+ </div>
197
+ )
198
+ }
@@ -0,0 +1,75 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { SocialBlock as SocialBlockType, SocialLink } from '../types'
5
+ import { Linkedin, Twitter, Facebook, Instagram, Youtube, Github, Globe } from 'lucide-react'
6
+
7
+ interface SocialBlockProps {
8
+ block: SocialBlockType
9
+ onChange: (block: SocialBlockType) => void
10
+ isEditing?: boolean
11
+ }
12
+
13
+ const PLATFORM_ICONS: Record<SocialLink['platform'], React.ComponentType<{ className?: string; style?: React.CSSProperties }>> = {
14
+ linkedin: Linkedin,
15
+ twitter: Twitter,
16
+ facebook: Facebook,
17
+ instagram: Instagram,
18
+ youtube: Youtube,
19
+ github: Github,
20
+ website: Globe,
21
+ }
22
+
23
+ const PLATFORM_COLORS: Record<SocialLink['platform'], string> = {
24
+ linkedin: '#0A66C2',
25
+ twitter: '#1DA1F2',
26
+ facebook: '#1877F2',
27
+ instagram: '#E4405F',
28
+ youtube: '#FF0000',
29
+ github: '#333333',
30
+ website: '#6b7280',
31
+ }
32
+
33
+ export function SocialBlockEditor({ block, onChange, isEditing = true }: SocialBlockProps) {
34
+ const iconSize = block.iconSize || 24
35
+ const alignClass =
36
+ block.alignment === 'left'
37
+ ? 'justify-start'
38
+ : block.alignment === 'right'
39
+ ? 'justify-end'
40
+ : 'justify-center'
41
+
42
+ return (
43
+ <div className={`flex gap-3 py-3 ${alignClass}`}>
44
+ {block.links.map((link) => {
45
+ const Icon = PLATFORM_ICONS[link.platform]
46
+ const color = PLATFORM_COLORS[link.platform]
47
+ if (isEditing || !link.url) {
48
+ return (
49
+ <span
50
+ key={link.id}
51
+ className="inline-flex items-center justify-center rounded"
52
+ style={{ width: iconSize + 8, height: iconSize + 8 }}
53
+ >
54
+ <Icon style={{ width: iconSize, height: iconSize, color }} />
55
+ </span>
56
+ )
57
+ }
58
+ return (
59
+ <a
60
+ key={link.id}
61
+ href={link.url}
62
+ target="_blank"
63
+ rel="noopener noreferrer"
64
+ className="inline-flex items-center justify-center rounded"
65
+ style={{ width: iconSize + 8, height: iconSize + 8 }}
66
+ >
67
+ <Icon style={{ width: iconSize, height: iconSize, color }} />
68
+ </a>
69
+ )
70
+ })}
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export { PLATFORM_ICONS, PLATFORM_COLORS }
@@ -0,0 +1,26 @@
1
+ 'use client'
2
+
3
+ import { SpacerBlock as SpacerBlockType } from '../types'
4
+
5
+ interface SpacerBlockProps {
6
+ block: SpacerBlockType
7
+ onChange: (block: SpacerBlockType) => void
8
+ isEditing?: boolean
9
+ }
10
+
11
+ export function SpacerBlockEditor({ block, onChange, isEditing = true }: SpacerBlockProps) {
12
+ const height = block.height || 32
13
+
14
+ if (!isEditing) {
15
+ return <div style={{ height }} />
16
+ }
17
+
18
+ return (
19
+ <div
20
+ className="relative border border-dashed border-gray-300 rounded flex items-center justify-center"
21
+ style={{ height }}
22
+ >
23
+ <span className="text-xs text-gray-400">{height}px</span>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,75 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { TextBlock as TextBlockType } from '../types'
5
+
6
+ interface TextBlockProps {
7
+ block: TextBlockType
8
+ onChange: (block: TextBlockType) => void
9
+ isEditing?: boolean
10
+ /** Optional custom rich text editor component. Receives content, onChange, placeholder, and className. */
11
+ renderEditor?: (props: {
12
+ content: string
13
+ onChange: (html: string) => void
14
+ placeholder?: string
15
+ className?: string
16
+ }) => React.ReactNode
17
+ }
18
+
19
+ export function TextBlockEditor({ block, onChange, isEditing = true, renderEditor }: TextBlockProps) {
20
+ if (!isEditing) {
21
+ return (
22
+ <div
23
+ className="prose prose-sm max-w-none py-2"
24
+ style={{
25
+ fontSize: block.fontSize ? `${block.fontSize}px` : undefined,
26
+ fontFamily: block.fontFamily || undefined,
27
+ lineHeight: block.lineHeight || undefined,
28
+ color: block.textColor || undefined,
29
+ }}
30
+ dangerouslySetInnerHTML={{ __html: block.content || '<p></p>' }}
31
+ />
32
+ )
33
+ }
34
+
35
+ // If a custom editor is provided, use it
36
+ if (renderEditor) {
37
+ return (
38
+ <div
39
+ style={{
40
+ fontSize: block.fontSize ? `${block.fontSize}px` : undefined,
41
+ fontFamily: block.fontFamily || undefined,
42
+ lineHeight: block.lineHeight || undefined,
43
+ color: block.textColor || undefined,
44
+ }}
45
+ >
46
+ {renderEditor({
47
+ content: block.content,
48
+ onChange: (html) => onChange({ ...block, content: html }),
49
+ placeholder: 'Write your content...',
50
+ className: 'min-h-[60px] border-0',
51
+ })}
52
+ </div>
53
+ )
54
+ }
55
+
56
+ // Default fallback: simple contentEditable div
57
+ return (
58
+ <div
59
+ style={{
60
+ fontSize: block.fontSize ? `${block.fontSize}px` : undefined,
61
+ fontFamily: block.fontFamily || undefined,
62
+ lineHeight: block.lineHeight || undefined,
63
+ color: block.textColor || undefined,
64
+ }}
65
+ >
66
+ <div
67
+ contentEditable
68
+ suppressContentEditableWarning
69
+ className="min-h-[60px] outline-none prose prose-sm max-w-none py-2"
70
+ dangerouslySetInnerHTML={{ __html: block.content || '' }}
71
+ onBlur={(e) => onChange({ ...block, content: e.currentTarget.innerHTML })}
72
+ />
73
+ </div>
74
+ )
75
+ }