@startsimpli/ui 0.4.7 → 0.4.9

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 (62) hide show
  1. package/package.json +21 -23
  2. package/src/__mocks__/next/link.js +11 -0
  3. package/src/components/account/__tests__/account.test.tsx +315 -0
  4. package/src/components/command-palette/CommandGroup.tsx +23 -0
  5. package/src/components/command-palette/CommandPalette.tsx +183 -200
  6. package/src/components/command-palette/CommandResultItem.tsx +59 -0
  7. package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
  8. package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
  9. package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
  10. package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
  11. package/src/components/command-palette/index.ts +6 -0
  12. package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
  13. package/src/components/compose/__tests__/compose.test.tsx +656 -0
  14. package/src/components/dashboard/PipelineFunnel.tsx +126 -0
  15. package/src/components/dashboard/TopCampaigns.tsx +132 -0
  16. package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
  17. package/src/components/dashboard/index.ts +6 -0
  18. package/src/components/dialog/ConfirmDialog.tsx +72 -0
  19. package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
  20. package/src/components/dialog/index.ts +3 -0
  21. package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
  22. package/src/components/email-editor/BlockRenderer.tsx +120 -0
  23. package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
  24. package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
  25. package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
  26. package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
  27. package/src/components/email-editor/editor-sidebar.tsx +6 -731
  28. package/src/components/email-editor/email-editor.tsx +78 -467
  29. package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
  30. package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
  31. package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
  32. package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
  33. package/src/components/email-editor/index.ts +1 -0
  34. package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
  35. package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
  36. package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
  37. package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
  38. package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
  39. package/src/components/email-editor/panels/index.ts +3 -0
  40. package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
  41. package/src/components/gantt/GanttBoardView.tsx +71 -0
  42. package/src/components/gantt/GanttChart.tsx +134 -881
  43. package/src/components/gantt/GanttFilterBar.tsx +100 -0
  44. package/src/components/gantt/GanttListView.tsx +63 -0
  45. package/src/components/gantt/GanttTimelineView.tsx +215 -0
  46. package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
  47. package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
  48. package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
  49. package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
  50. package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
  51. package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
  52. package/src/components/gantt/hooks/useGanttState.ts +644 -0
  53. package/src/components/gantt/index.ts +10 -0
  54. package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
  55. package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
  56. package/src/components/lists/__tests__/lists.test.tsx +263 -0
  57. package/src/components/loading/__tests__/loading.test.tsx +114 -0
  58. package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
  59. package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
  60. package/src/components/safe-html.tsx +9 -8
  61. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  62. package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
@@ -0,0 +1,376 @@
1
+ import { renderToEmailHtml, renderToPreviewHtml } from '../renderer/email-html-renderer'
2
+ import type { Section, GlobalStyles } from '../types'
3
+ import { DEFAULT_GLOBAL_STYLES, createSection, createRow } from '../types'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Fixtures
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function makeSingleSection(overrides: Partial<Section> = {}): Section[] {
10
+ const section: Section = {
11
+ id: 'section-1',
12
+ rows: [
13
+ {
14
+ id: 'row-1',
15
+ layout: '1',
16
+ columns: [
17
+ [
18
+ {
19
+ id: 'text-1',
20
+ type: 'text',
21
+ content: '<p>Hello email</p>',
22
+ },
23
+ ],
24
+ ],
25
+ },
26
+ ],
27
+ paddingTop: 16,
28
+ paddingBottom: 16,
29
+ paddingLeft: 0,
30
+ paddingRight: 0,
31
+ ...overrides,
32
+ }
33
+ return [section]
34
+ }
35
+
36
+ const defaultStyles: GlobalStyles = DEFAULT_GLOBAL_STYLES
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Valid HTML document structure
40
+ // ---------------------------------------------------------------------------
41
+
42
+ describe('renderToEmailHtml — document structure', () => {
43
+ it('starts with <!DOCTYPE html>', () => {
44
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
45
+ expect(html.trimStart()).toMatch(/^<!DOCTYPE html>/i)
46
+ })
47
+
48
+ it('contains <html> root element with lang="en"', () => {
49
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
50
+ expect(html).toContain('<html lang="en"')
51
+ })
52
+
53
+ it('contains <head> with charset meta tag', () => {
54
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
55
+ expect(html).toContain('<meta charset="utf-8">')
56
+ })
57
+
58
+ it('contains viewport meta tag', () => {
59
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
60
+ expect(html).toContain('name="viewport"')
61
+ })
62
+
63
+ it('contains <body> element', () => {
64
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
65
+ expect(html).toContain('<body')
66
+ })
67
+
68
+ it('closes </html> tag', () => {
69
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
70
+ expect(html.trimEnd()).toMatch(/<\/html>$/)
71
+ })
72
+
73
+ it('contains closing </body> tag', () => {
74
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
75
+ expect(html).toContain('</body>')
76
+ })
77
+
78
+ it('contains role="presentation" table for email wrapper', () => {
79
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
80
+ expect(html).toContain('role="presentation"')
81
+ })
82
+
83
+ it('contains email-container class for responsive width', () => {
84
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
85
+ expect(html).toContain('class="email-container"')
86
+ })
87
+
88
+ it('contains @media query for max-width:620px', () => {
89
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
90
+ expect(html).toContain('max-width:620px')
91
+ })
92
+
93
+ it('contains MSO conditional comment for Outlook', () => {
94
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
95
+ expect(html).toContain('[if mso]')
96
+ })
97
+ })
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Global styles applied to output
101
+ // ---------------------------------------------------------------------------
102
+
103
+ describe('renderToEmailHtml — global styles', () => {
104
+ it('uses globalStyles.backgroundColor in body inline style', () => {
105
+ const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, backgroundColor: '#abcdef' })
106
+ expect(html).toContain('background-color:#abcdef')
107
+ })
108
+
109
+ it('uses globalStyles.contentWidth for container width attribute', () => {
110
+ const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, contentWidth: 700 })
111
+ expect(html).toContain('width="700"')
112
+ expect(html).toContain('max-width:700px')
113
+ })
114
+
115
+ it('uses globalStyles.fontFamily in body inline style', () => {
116
+ const html = renderToEmailHtml(makeSingleSection(), {
117
+ ...defaultStyles,
118
+ fontFamily: 'Georgia, serif',
119
+ })
120
+ expect(html).toContain('font-family:Georgia, serif')
121
+ })
122
+
123
+ it('defaults contentWidth to 600 when not provided', () => {
124
+ const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, contentWidth: 0 })
125
+ expect(html).toContain('width="600"')
126
+ })
127
+
128
+ it('defaults backgroundColor when not provided', () => {
129
+ const html = renderToEmailHtml(makeSingleSection(), { ...defaultStyles, backgroundColor: '' })
130
+ // Falls back to the empty string default; body still renders
131
+ expect(html).toContain('<body')
132
+ })
133
+ })
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Subject / preheader options
137
+ // ---------------------------------------------------------------------------
138
+
139
+ describe('renderToEmailHtml — options', () => {
140
+ it('puts subject in <title> tag', () => {
141
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles, { subject: 'Q1 Update' })
142
+ expect(html).toContain('<title>Q1 Update</title>')
143
+ })
144
+
145
+ it('empty subject results in empty <title>', () => {
146
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles, { subject: '' })
147
+ expect(html).toContain('<title></title>')
148
+ })
149
+
150
+ it('renders preheader div when preheaderText is provided', () => {
151
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles, {
152
+ preheaderText: 'Read this important update',
153
+ })
154
+ expect(html).toContain('Read this important update')
155
+ expect(html).toContain('display:none')
156
+ })
157
+
158
+ it('does not render preheader div when preheaderText is absent', () => {
159
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
160
+ expect(html).not.toContain('display:none')
161
+ })
162
+ })
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Block content included in output
166
+ // ---------------------------------------------------------------------------
167
+
168
+ describe('renderToEmailHtml — block content', () => {
169
+ it('includes text block content in output', () => {
170
+ const html = renderToEmailHtml(makeSingleSection(), defaultStyles)
171
+ expect(html).toContain('Hello email')
172
+ })
173
+
174
+ it('includes content from header block', () => {
175
+ const section: Section = {
176
+ id: 's1',
177
+ rows: [
178
+ {
179
+ id: 'r1',
180
+ layout: '1',
181
+ columns: [
182
+ [{ id: 'h1', type: 'header', companyName: 'StartSimpli', alignment: 'center' }],
183
+ ],
184
+ },
185
+ ],
186
+ }
187
+ const html = renderToEmailHtml([section], defaultStyles)
188
+ expect(html).toContain('StartSimpli')
189
+ })
190
+
191
+ it('includes metrics block values', () => {
192
+ const section: Section = {
193
+ id: 's1',
194
+ rows: [
195
+ {
196
+ id: 'r1',
197
+ layout: '1',
198
+ columns: [
199
+ [
200
+ {
201
+ id: 'met1',
202
+ type: 'metrics',
203
+ title: 'My Metrics',
204
+ metrics: [{ id: 'm1', label: 'Revenue', value: '$42k', changeType: 'positive' }],
205
+ columns: 2,
206
+ },
207
+ ],
208
+ ],
209
+ },
210
+ ],
211
+ }
212
+ const html = renderToEmailHtml([section], defaultStyles)
213
+ expect(html).toContain('$42k')
214
+ expect(html).toContain('Revenue')
215
+ })
216
+
217
+ it('includes CTA button text and URL', () => {
218
+ const section: Section = {
219
+ id: 's1',
220
+ rows: [
221
+ {
222
+ id: 'r1',
223
+ layout: '1',
224
+ columns: [
225
+ [
226
+ {
227
+ id: 'cta1',
228
+ type: 'cta',
229
+ text: 'Click Here',
230
+ url: 'https://start.simpli',
231
+ alignment: 'center',
232
+ buttonColor: '#2563eb',
233
+ textColor: '#ffffff',
234
+ borderRadius: 6,
235
+ paddingH: 24,
236
+ paddingV: 12,
237
+ },
238
+ ],
239
+ ],
240
+ },
241
+ ],
242
+ }
243
+ const html = renderToEmailHtml([section], defaultStyles)
244
+ expect(html).toContain('Click Here')
245
+ expect(html).toContain('https://start.simpli')
246
+ })
247
+
248
+ it('includes footer company name', () => {
249
+ const section: Section = {
250
+ id: 's1',
251
+ rows: [
252
+ {
253
+ id: 'r1',
254
+ layout: '1',
255
+ columns: [
256
+ [
257
+ {
258
+ id: 'f1',
259
+ type: 'footer',
260
+ companyName: 'Acme Inc',
261
+ showUnsubscribe: false,
262
+ alignment: 'center',
263
+ },
264
+ ],
265
+ ],
266
+ },
267
+ ],
268
+ }
269
+ const html = renderToEmailHtml([section], defaultStyles)
270
+ expect(html).toContain('Acme Inc')
271
+ })
272
+ })
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // Multi-section and multi-column layout
276
+ // ---------------------------------------------------------------------------
277
+
278
+ describe('renderToEmailHtml — layout', () => {
279
+ it('renders multiple sections (both section backgrounds appear)', () => {
280
+ const s1: Section = {
281
+ id: 's1',
282
+ backgroundColor: '#ff0000',
283
+ rows: [{ id: 'r1', layout: '1', columns: [[{ id: 't1', type: 'text', content: 'Sec1' }]] }],
284
+ }
285
+ const s2: Section = {
286
+ id: 's2',
287
+ backgroundColor: '#00ff00',
288
+ rows: [{ id: 'r2', layout: '1', columns: [[{ id: 't2', type: 'text', content: 'Sec2' }]] }],
289
+ }
290
+ const html = renderToEmailHtml([s1, s2], defaultStyles)
291
+ expect(html).toContain('#ff0000')
292
+ expect(html).toContain('#00ff00')
293
+ expect(html).toContain('Sec1')
294
+ expect(html).toContain('Sec2')
295
+ })
296
+
297
+ it('renders multi-column row using nested table with stack-column class', () => {
298
+ const section: Section = {
299
+ id: 's1',
300
+ rows: [
301
+ {
302
+ id: 'r1',
303
+ layout: '2',
304
+ columns: [
305
+ [{ id: 't1', type: 'text', content: 'Left col' }],
306
+ [{ id: 't2', type: 'text', content: 'Right col' }],
307
+ ],
308
+ },
309
+ ],
310
+ }
311
+ const html = renderToEmailHtml([section], defaultStyles)
312
+ expect(html).toContain('class="stack-column"')
313
+ expect(html).toContain('Left col')
314
+ expect(html).toContain('Right col')
315
+ })
316
+
317
+ it('section separator spacer row is present between sections', () => {
318
+ const s1: Section = {
319
+ id: 's1',
320
+ rows: [{ id: 'r1', layout: '1', columns: [[{ id: 't1', type: 'text', content: 'A' }]] }],
321
+ }
322
+ const s2: Section = {
323
+ id: 's2',
324
+ rows: [{ id: 'r2', layout: '1', columns: [[{ id: 't2', type: 'text', content: 'B' }]] }],
325
+ }
326
+ const html = renderToEmailHtml([s1, s2], defaultStyles)
327
+ // 4px spacer row between sections
328
+ expect(html).toContain('height:4px')
329
+ })
330
+
331
+ it('section padding is applied in style attribute', () => {
332
+ const section: Section = {
333
+ id: 's1',
334
+ paddingTop: 32,
335
+ paddingBottom: 24,
336
+ paddingLeft: 8,
337
+ paddingRight: 8,
338
+ rows: [{ id: 'r1', layout: '1', columns: [[{ id: 't1', type: 'text', content: 'X' }]] }],
339
+ }
340
+ const html = renderToEmailHtml([section], defaultStyles)
341
+ expect(html).toContain('padding:32px 8px 24px 8px')
342
+ })
343
+
344
+ it('renders empty sections array without crashing', () => {
345
+ expect(() => renderToEmailHtml([], defaultStyles)).not.toThrow()
346
+ const html = renderToEmailHtml([], defaultStyles)
347
+ expect(html).toContain('<!DOCTYPE html>')
348
+ })
349
+
350
+ it('renders section with empty columns without crashing', () => {
351
+ const section: Section = {
352
+ id: 's1',
353
+ rows: [{ id: 'r1', layout: '1', columns: [[]] }],
354
+ }
355
+ expect(() => renderToEmailHtml([section], defaultStyles)).not.toThrow()
356
+ })
357
+ })
358
+
359
+ // ---------------------------------------------------------------------------
360
+ // renderToPreviewHtml delegates to renderToEmailHtml
361
+ // ---------------------------------------------------------------------------
362
+
363
+ describe('renderToPreviewHtml', () => {
364
+ it('returns the same output as renderToEmailHtml', () => {
365
+ const sections = makeSingleSection()
366
+ expect(renderToPreviewHtml(sections, defaultStyles)).toBe(
367
+ renderToEmailHtml(sections, defaultStyles)
368
+ )
369
+ })
370
+
371
+ it('uses DEFAULT_GLOBAL_STYLES when no styles argument provided', () => {
372
+ const sections = makeSingleSection()
373
+ const html = renderToPreviewHtml(sections)
374
+ expect(html).toContain(`background-color:${DEFAULT_GLOBAL_STYLES.backgroundColor}`)
375
+ })
376
+ })