@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,337 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { GanttListView } from '../GanttListView'
3
+ import type { GanttListViewProps } from '../GanttListView'
4
+ import type { GanttTask, TimelineItem } from '../types'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function makeItem(overrides: Partial<TimelineItem> & { id: string; title: string; status: string }): TimelineItem {
11
+ return { startDate: '2025-01-01', endDate: '2025-03-31', ...overrides }
12
+ }
13
+
14
+ function makeTask(item: TimelineItem, overrides: Partial<GanttTask> = {}): GanttTask {
15
+ return {
16
+ item,
17
+ start: new Date('2025-01-01'),
18
+ end: new Date('2025-03-31'),
19
+ progress: 50,
20
+ timeProgress: 30,
21
+ depth: 0,
22
+ hasChildren: false,
23
+ parentId: null,
24
+ healthStatus: 'on_track',
25
+ ...overrides,
26
+ }
27
+ }
28
+
29
+ const CATEGORY_COLORS: Record<string, string> = {
30
+ product: '#3b82f6',
31
+ team: '#a855f7',
32
+ other: '#6b7280',
33
+ }
34
+
35
+ function defaultProps(overrides: Partial<GanttListViewProps> = {}): GanttListViewProps {
36
+ return {
37
+ listItems: [],
38
+ focusedRowIndex: -1,
39
+ categoryColors: CATEGORY_COLORS,
40
+ handleItemClick: jest.fn(),
41
+ ...overrides,
42
+ }
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Header rendering
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe('GanttListView — header', () => {
50
+ it('renders the Title column header', () => {
51
+ render(<GanttListView {...defaultProps()} />)
52
+ expect(screen.getByText('Title')).toBeInTheDocument()
53
+ })
54
+
55
+ it('renders the Status column header', () => {
56
+ render(<GanttListView {...defaultProps()} />)
57
+ expect(screen.getByText('Status')).toBeInTheDocument()
58
+ })
59
+
60
+ it('renders the Category column header', () => {
61
+ render(<GanttListView {...defaultProps()} />)
62
+ expect(screen.getByText('Category')).toBeInTheDocument()
63
+ })
64
+
65
+ it('renders the Dates column header', () => {
66
+ render(<GanttListView {...defaultProps()} />)
67
+ expect(screen.getByText('Dates')).toBeInTheDocument()
68
+ })
69
+
70
+ it('renders the Progress column header', () => {
71
+ render(<GanttListView {...defaultProps()} />)
72
+ expect(screen.getByText('Progress')).toBeInTheDocument()
73
+ })
74
+ })
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Empty state
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe('GanttListView — empty state', () => {
81
+ it('renders only the header row when listItems is empty', () => {
82
+ const { container } = render(<GanttListView {...defaultProps()} />)
83
+ expect(container.querySelectorAll('.gantt-list-row')).toHaveLength(0)
84
+ })
85
+ })
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Row rendering
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe('GanttListView — row rendering', () => {
92
+ it('renders a row for each task', () => {
93
+ const listItems = [
94
+ makeTask(makeItem({ id: 'a', title: 'Alpha', status: 'in_progress' })),
95
+ makeTask(makeItem({ id: 'b', title: 'Beta', status: 'completed' })),
96
+ makeTask(makeItem({ id: 'c', title: 'Gamma', status: 'not_started' })),
97
+ ]
98
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
99
+ expect(container.querySelectorAll('.gantt-list-row')).toHaveLength(3)
100
+ })
101
+
102
+ it('renders each task title in the list', () => {
103
+ const listItems = [
104
+ makeTask(makeItem({ id: 'a', title: 'Alpha Task', status: 'in_progress' })),
105
+ makeTask(makeItem({ id: 'b', title: 'Beta Task', status: 'completed' })),
106
+ ]
107
+ render(<GanttListView {...defaultProps({ listItems })} />)
108
+ expect(screen.getByText('Alpha Task')).toBeInTheDocument()
109
+ expect(screen.getByText('Beta Task')).toBeInTheDocument()
110
+ })
111
+
112
+ it('renders a status badge with the task status (underscores replaced with spaces)', () => {
113
+ const listItems = [
114
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress' })),
115
+ makeTask(makeItem({ id: 'b', title: 'Task B', status: 'not_started' })),
116
+ ]
117
+ render(<GanttListView {...defaultProps({ listItems })} />)
118
+ expect(screen.getByText('in progress')).toBeInTheDocument()
119
+ expect(screen.getByText('not started')).toBeInTheDocument()
120
+ })
121
+
122
+ it('renders a category badge when the task item has a category', () => {
123
+ const listItems = [
124
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress', category: 'product' })),
125
+ ]
126
+ render(<GanttListView {...defaultProps({ listItems })} />)
127
+ expect(screen.getByText('product')).toBeInTheDocument()
128
+ })
129
+
130
+ it('does not render a category badge when item has no category', () => {
131
+ const listItems = [
132
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress' })),
133
+ ]
134
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
135
+ expect(container.querySelector('.gantt-category-badge')).not.toBeInTheDocument()
136
+ })
137
+
138
+ it('renders a progress percentage text', () => {
139
+ const listItems = [
140
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress' }), { progress: 75 }),
141
+ ]
142
+ render(<GanttListView {...defaultProps({ listItems })} />)
143
+ expect(screen.getByText('75%')).toBeInTheDocument()
144
+ })
145
+
146
+ it('renders 0% progress correctly', () => {
147
+ const listItems = [
148
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'not_started' }), { progress: 0 }),
149
+ ]
150
+ render(<GanttListView {...defaultProps({ listItems })} />)
151
+ expect(screen.getByText('0%')).toBeInTheDocument()
152
+ })
153
+
154
+ it('renders 100% progress correctly', () => {
155
+ const listItems = [
156
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'completed' }), { progress: 100 }),
157
+ ]
158
+ render(<GanttListView {...defaultProps({ listItems })} />)
159
+ expect(screen.getByText('100%')).toBeInTheDocument()
160
+ })
161
+
162
+ it('renders formatted date range in the Dates column', () => {
163
+ const listItems = [
164
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress' }), {
165
+ // Use local-time constructor to avoid UTC→local timezone offset shifting the day
166
+ start: new Date(2025, 0, 15), // Jan 15 2025 local
167
+ end: new Date(2025, 3, 20), // Apr 20 2025 local
168
+ }),
169
+ ]
170
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
171
+ // The dates cell contains the full range as its textContent
172
+ const datesCell = container.querySelector('.gantt-list-col-dates:not(.gantt-list-header span)')
173
+ // Find the data row's dates cell (second .gantt-list-col-dates element)
174
+ const datesCells = container.querySelectorAll('.gantt-list-col-dates')
175
+ // datesCells[0] is the header "Dates", datesCells[1] is the data row cell
176
+ const dataCell = datesCells[1] as HTMLElement
177
+ expect(dataCell.textContent).toMatch(/Jan 15/)
178
+ expect(dataCell.textContent).toMatch(/Apr 20/)
179
+ })
180
+ })
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Focus state
184
+ // ---------------------------------------------------------------------------
185
+
186
+ describe('GanttListView — focus state', () => {
187
+ it('applies gantt-row-focused class to the focused row index', () => {
188
+ const listItems = [
189
+ makeTask(makeItem({ id: 'a', title: 'Alpha', status: 'not_started' })),
190
+ makeTask(makeItem({ id: 'b', title: 'Beta', status: 'in_progress' })),
191
+ ]
192
+ const { container } = render(
193
+ <GanttListView {...defaultProps({ listItems, focusedRowIndex: 1 })} />
194
+ )
195
+ const rows = container.querySelectorAll('.gantt-list-row')
196
+ expect(rows[0]).not.toHaveClass('gantt-row-focused')
197
+ expect(rows[1]).toHaveClass('gantt-row-focused')
198
+ })
199
+
200
+ it('does not apply gantt-row-focused when focusedRowIndex is -1', () => {
201
+ const listItems = [
202
+ makeTask(makeItem({ id: 'a', title: 'Alpha', status: 'not_started' })),
203
+ ]
204
+ const { container } = render(
205
+ <GanttListView {...defaultProps({ listItems, focusedRowIndex: -1 })} />
206
+ )
207
+ expect(container.querySelector('.gantt-row-focused')).not.toBeInTheDocument()
208
+ })
209
+ })
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Item click
213
+ // ---------------------------------------------------------------------------
214
+
215
+ describe('GanttListView — item click', () => {
216
+ it('calls handleItemClick when a row is clicked', () => {
217
+ const handleItemClick = jest.fn()
218
+ const item = makeItem({ id: 'a', title: 'Clickable Task', status: 'in_progress' })
219
+ const listItems = [makeTask(item)]
220
+ render(<GanttListView {...defaultProps({ listItems, handleItemClick })} />)
221
+ fireEvent.click(screen.getByText('Clickable Task'))
222
+ expect(handleItemClick).toHaveBeenCalledWith(item)
223
+ })
224
+
225
+ it('calls handleItemClick with the correct item when multiple rows exist', () => {
226
+ const handleItemClick = jest.fn()
227
+ const itemA = makeItem({ id: 'a', title: 'Task A', status: 'in_progress' })
228
+ const itemB = makeItem({ id: 'b', title: 'Task B', status: 'completed' })
229
+ const listItems = [makeTask(itemA), makeTask(itemB)]
230
+ render(<GanttListView {...defaultProps({ listItems, handleItemClick })} />)
231
+ fireEvent.click(screen.getByText('Task B'))
232
+ expect(handleItemClick).toHaveBeenCalledWith(itemB)
233
+ expect(handleItemClick).toHaveBeenCalledTimes(1)
234
+ })
235
+ })
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // Depth indentation
239
+ // ---------------------------------------------------------------------------
240
+
241
+ describe('GanttListView — depth indentation', () => {
242
+ it('renders an indent spacer for child tasks (depth > 0)', () => {
243
+ const listItems = [
244
+ makeTask(makeItem({ id: 'child', title: 'Child Task', status: 'in_progress' }), { depth: 1 }),
245
+ ]
246
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
247
+ // The indent span has inline style with a calculated width
248
+ const row = container.querySelector('.gantt-list-row')
249
+ const indentSpan = row?.querySelector('span[style]')
250
+ expect(indentSpan).toBeInTheDocument()
251
+ })
252
+
253
+ it('does not render an indent spacer for root tasks (depth 0)', () => {
254
+ const listItems = [
255
+ makeTask(makeItem({ id: 'root', title: 'Root Task', status: 'in_progress' }), { depth: 0 }),
256
+ ]
257
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
258
+ const row = container.querySelector('.gantt-list-row')
259
+ // At depth 0, no inline-block span is injected
260
+ const titleCell = row?.querySelector('.gantt-list-col-title')
261
+ // Check that no child span with display:inline-block exists
262
+ const indentSpans = titleCell?.querySelectorAll('span[style]') ?? []
263
+ expect(indentSpans).toHaveLength(0)
264
+ })
265
+ })
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Progress bar color
269
+ // ---------------------------------------------------------------------------
270
+
271
+ describe('GanttListView — progress bar', () => {
272
+ it('renders the progress fill bar', () => {
273
+ const listItems = [
274
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress' }), { progress: 60 }),
275
+ ]
276
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
277
+ const fill = container.querySelector('.gantt-list-progress-fill') as HTMLElement | null
278
+ expect(fill).toBeInTheDocument()
279
+ expect(fill?.style.width).toBe('60%')
280
+ })
281
+
282
+ it('uses the on_track health color for the progress fill when healthStatus is on_track', () => {
283
+ const listItems = [
284
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress' }), {
285
+ progress: 50,
286
+ healthStatus: 'on_track',
287
+ }),
288
+ ]
289
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
290
+ const fill = container.querySelector('.gantt-list-progress-fill') as HTMLElement | null
291
+ // on_track color is #22c55e
292
+ expect(fill?.style.backgroundColor).toBe('rgb(34, 197, 94)')
293
+ })
294
+
295
+ it('uses the at_risk health color when healthStatus is at_risk', () => {
296
+ const listItems = [
297
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress' }), {
298
+ progress: 20,
299
+ healthStatus: 'at_risk',
300
+ }),
301
+ ]
302
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
303
+ const fill = container.querySelector('.gantt-list-progress-fill') as HTMLElement | null
304
+ // at_risk color is #eab308
305
+ expect(fill?.style.backgroundColor).toBe('rgb(234, 179, 8)')
306
+ })
307
+
308
+ it('uses the blocked health color when healthStatus is blocked', () => {
309
+ const listItems = [
310
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'blocked' }), {
311
+ progress: 10,
312
+ healthStatus: 'blocked',
313
+ }),
314
+ ]
315
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
316
+ const fill = container.querySelector('.gantt-list-progress-fill') as HTMLElement | null
317
+ // blocked color is #ef4444
318
+ expect(fill?.style.backgroundColor).toBe('rgb(239, 68, 68)')
319
+ })
320
+ })
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Category color fallback
324
+ // ---------------------------------------------------------------------------
325
+
326
+ describe('GanttListView — category color fallback', () => {
327
+ it('uses the other fallback color for unknown categories', () => {
328
+ const listItems = [
329
+ makeTask(makeItem({ id: 'a', title: 'Task A', status: 'in_progress', category: 'unknown_cat' })),
330
+ ]
331
+ const { container } = render(<GanttListView {...defaultProps({ listItems })} />)
332
+ const badge = container.querySelector('.gantt-category-badge') as HTMLElement | null
333
+ expect(badge).toBeInTheDocument()
334
+ // '#6b7280' in rgb
335
+ expect(badge?.style.backgroundColor).toBe('rgb(107, 114, 128)')
336
+ })
337
+ })