create-crm-tmp 2.0.0 → 2.1.0

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 (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. package/template/src/types/yousign.ts +0 -52
@@ -64,9 +64,6 @@ import {
64
64
  Code,
65
65
  Terminal,
66
66
  Table as TableIcon,
67
- FileCode,
68
- Eye,
69
- Pencil,
70
67
  Type,
71
68
  Quote,
72
69
  Indent,
@@ -74,8 +71,16 @@ import {
74
71
  } from 'lucide-react';
75
72
  import { createPortal } from 'react-dom';
76
73
  import { commandsToCommandPaletteItems, registerKeyboardShortcuts } from './ui/commands';
77
- import { Select, Dropdown, Dialog } from './ui/components';
74
+ import { Select, Dialog } from './ui/components';
78
75
  import { defaultTheme } from './ui/theme';
76
+ import { useAppToast } from '@/contexts/app-toast-context';
77
+ import { devToast } from '@/lib/utils';
78
+ import { assertImageFileWithinLimit } from '@/lib/editor-image-limits';
79
+ import { uploadEditorImage } from './editor/upload-editor-image';
80
+ import {
81
+ applyOrderedImageLayoutAfterImport,
82
+ mergeImgDimensionAttrsIntoStyle,
83
+ } from '@/lib/editor-html-image-dimensions';
79
84
 
80
85
  type TableConfig = {
81
86
  rows?: number;
@@ -83,6 +88,30 @@ type TableConfig = {
83
88
  includeHeaders?: boolean;
84
89
  };
85
90
 
91
+ function fileToDataUrl(file: File): Promise<string> {
92
+ return new Promise((resolve, reject) => {
93
+ const reader = new FileReader();
94
+ reader.onload = () => {
95
+ const result = reader.result;
96
+ if (typeof result === 'string') {
97
+ resolve(result);
98
+ } else {
99
+ reject(new Error('Impossible de convertir le fichier en data URL'));
100
+ }
101
+ };
102
+ reader.onerror = () =>
103
+ reject(reader.error ?? new Error('Erreur de lecture du fichier image'));
104
+ reader.readAsDataURL(file);
105
+ });
106
+ }
107
+
108
+ async function imageFileToDataUrl(file: File, maxImageBytes?: number): Promise<string> {
109
+ if (maxImageBytes !== undefined) {
110
+ assertImageFileWithinLimit(file, maxImageBytes);
111
+ }
112
+ return fileToDataUrl(file);
113
+ }
114
+
86
115
  // Create markdown extension instance
87
116
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
88
117
  // @ts-ignore - Lexical duplicate types between packages
@@ -143,16 +172,21 @@ export interface DefaultTemplateRef {
143
172
  }
144
173
 
145
174
  // Hook for image handling logic
146
- function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null) {
175
+ function useImageHandlers(
176
+ commands: EditorCommands,
177
+ editor: LexicalEditor | null,
178
+ maxImageBytes?: number,
179
+ ) {
147
180
  const fileInputRef = useRef<HTMLInputElement>(null);
181
+ const toast = useAppToast();
148
182
 
149
183
  const handlers = useMemo(
150
184
  () => ({
151
185
  insertFromUrl: () => {
152
- const src = prompt('Enter image URL:');
186
+ const src = prompt("URL de l'image (https://…) :");
153
187
  if (!src) return;
154
- const alt = prompt('Enter alt text:') || '';
155
- const caption = prompt('Enter caption (optional):') || undefined;
188
+ const alt = prompt('Texte alternatif (accessibilité) :') || '';
189
+ const caption = prompt('Légende (optionnel) :') || undefined;
156
190
  commands.insertImage({ src, alt, caption });
157
191
  },
158
192
  insertFromFile: () => fileInputRef.current?.click(),
@@ -164,11 +198,19 @@ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null
164
198
  try {
165
199
  src = await imageExtension.config.uploadHandler(file);
166
200
  } catch (error) {
201
+ // Toast déjà affiché par uploadHandler (signature / collage)
167
202
  console.error('Failed to upload image:', error);
203
+ e.target.value = '';
168
204
  return;
169
205
  }
170
206
  } else {
171
- src = URL.createObjectURL(file);
207
+ try {
208
+ src = await imageFileToDataUrl(file, maxImageBytes);
209
+ } catch (error) {
210
+ toast.error(devToast("Impossible d'ajouter cette image.", error));
211
+ e.target.value = '';
212
+ return;
213
+ }
172
214
  }
173
215
  commands.insertImage({ src, alt: file.name, file });
174
216
  e.target.value = '';
@@ -181,7 +223,7 @@ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null
181
223
  commands.setImageCaption(newCaption);
182
224
  },
183
225
  }),
184
- [commands],
226
+ [commands, maxImageBytes, toast],
185
227
  );
186
228
 
187
229
  return { handlers, fileInputRef };
@@ -268,6 +310,10 @@ function FloatingToolbarRenderer() {
268
310
  >
269
311
  <Type size={14} />
270
312
  </button>
313
+ <div className="bg-border mx-1 h-6 w-px" />
314
+ <span className="text-muted-foreground px-1.5 text-[11px] leading-6 whitespace-nowrap">
315
+ Shift + glisser pour garder le ratio
316
+ </span>
271
317
  </>
272
318
  ) : (
273
319
  <>
@@ -406,15 +452,15 @@ function Toolbar({
406
452
  commands,
407
453
  hasExtension,
408
454
  activeStates,
455
+ maxImageBytes,
409
456
  }: {
410
457
  commands: EditorCommands;
411
458
  hasExtension: (name: ExtensionNames) => boolean;
412
459
  activeStates: EditorStateQueries;
460
+ maxImageBytes?: number;
413
461
  }) {
414
462
  const { lexical: editor } = useEditor();
415
- const { handlers, fileInputRef } = useImageHandlers(commands, editor);
416
- const [showImageDropdown, setShowImageDropdown] = useState(false);
417
- const [showAlignDropdown, setShowAlignDropdown] = useState(false);
463
+ const { handlers, fileInputRef } = useImageHandlers(commands, editor, maxImageBytes);
418
464
  const [showTableDialog, setShowTableDialog] = useState(false);
419
465
  const [tableConfig, setTableConfig] = useState<TableConfig>({
420
466
  rows: 3,
@@ -589,6 +635,35 @@ function Toolbar({
589
635
  </div>
590
636
  )}
591
637
 
638
+ {/* Image : fichier local ou URL (handlers étaient définis mais non exposés dans la barre) */}
639
+ {hasExtension('image') && (
640
+ <div className="lexkit-toolbar-section">
641
+ <input
642
+ ref={fileInputRef}
643
+ type="file"
644
+ accept="image/*"
645
+ className="hidden"
646
+ onChange={handlers.handleUpload}
647
+ />
648
+ <button
649
+ type="button"
650
+ onClick={() => handlers.insertFromFile()}
651
+ className="lexkit-toolbar-button"
652
+ title="Insérer une image depuis un fichier"
653
+ >
654
+ <Upload size={16} />
655
+ </button>
656
+ <button
657
+ type="button"
658
+ onClick={() => handlers.insertFromUrl()}
659
+ className="lexkit-toolbar-button"
660
+ title="Insérer une image depuis une URL"
661
+ >
662
+ <ImageIcon size={16} />
663
+ </button>
664
+ </div>
665
+ )}
666
+
592
667
  {/* Table */}
593
668
  {hasExtension('table') && (
594
669
  <div className="lexkit-toolbar-section">
@@ -722,11 +797,13 @@ function EditorContent({
722
797
  isDark,
723
798
  toggleTheme,
724
799
  onReady,
800
+ maxImageBytes,
725
801
  }: {
726
802
  className?: string;
727
803
  isDark: boolean;
728
804
  toggleTheme: () => void;
729
805
  onReady?: (methods: DefaultTemplateRef) => void;
806
+ maxImageBytes?: number;
730
807
  }) {
731
808
  const { commands, hasExtension, activeStates, lexical: editor } = useEditor();
732
809
  const commandsRef = useRef<EditorCommands>(commands);
@@ -752,10 +829,46 @@ function EditorContent({
752
829
  },
753
830
  injectHTML: (content: string) => {
754
831
  setTimeout(() => {
755
- if (editor) {
756
- editor.update(() => {
757
- commandsRef.current.importFromHTML(content, { preventFocus: true });
758
- });
832
+ if (!editor) return;
833
+ const safeContent = typeof content === 'string' ? content.trim() : '';
834
+ const containsHtmlTag = /<[^>]+>/.test(safeContent);
835
+ const plainParagraph = `<p>${safeContent
836
+ .replaceAll('&', '&amp;')
837
+ .replaceAll('<', '&lt;')
838
+ .replaceAll('>', '&gt;')
839
+ .replaceAll('"', '&quot;')
840
+ .replaceAll("'", '&#39;')
841
+ .replaceAll('\n', '<br />')}</p>`;
842
+
843
+ const contentForImport = !safeContent
844
+ ? '<p></p>'
845
+ : containsHtmlTag
846
+ ? mergeImgDimensionAttrsIntoStyle(safeContent)
847
+ : plainParagraph;
848
+
849
+ const afterImport = () => {
850
+ if (!containsHtmlTag || !contentForImport.includes('<img')) return;
851
+ applyOrderedImageLayoutAfterImport(editor, contentForImport);
852
+ };
853
+
854
+ try {
855
+ const result = commandsRef.current.importFromHTML(contentForImport, {
856
+ preventFocus: true,
857
+ }) as Promise<void> | void;
858
+
859
+ if (result && typeof (result as Promise<void>).then === 'function') {
860
+ (result as Promise<void>)
861
+ .then(afterImport)
862
+ .catch((error) => {
863
+ console.error('Erreur import HTML éditeur, fallback contenu vide:', error);
864
+ void commandsRef.current.importFromHTML('<p></p>', { preventFocus: true });
865
+ });
866
+ } else {
867
+ afterImport();
868
+ }
869
+ } catch (error) {
870
+ console.error('Erreur injectHTML éditeur:', error);
871
+ void commandsRef.current.importFromHTML('<p></p>', { preventFocus: true });
759
872
  }
760
873
  }, 100);
761
874
  },
@@ -774,7 +887,14 @@ function EditorContent({
774
887
  }
775
888
  },
776
889
  getMarkdown: () => commandsRef.current.exportToMarkdown(),
777
- getHTML: () => commandsRef.current.exportToHTML(),
890
+ getHTML: () => {
891
+ try {
892
+ const raw = commandsRef.current.exportToHTML() || '';
893
+ return mergeImgDimensionAttrsIntoStyle(raw);
894
+ } catch {
895
+ return commandsRef.current.exportToHTML() || '';
896
+ }
897
+ },
778
898
  }),
779
899
  [editor],
780
900
  );
@@ -803,7 +923,12 @@ function EditorContent({
803
923
  return (
804
924
  <>
805
925
  <div className="lexkit-editor-header">
806
- <Toolbar commands={commands} hasExtension={hasExtension} activeStates={activeStates} />
926
+ <Toolbar
927
+ commands={commands}
928
+ hasExtension={hasExtension}
929
+ activeStates={activeStates}
930
+ maxImageBytes={maxImageBytes}
931
+ />
807
932
  </div>
808
933
  <div className="lexkit-editor">
809
934
  <div className="flex flex-1 flex-col" style={{ display: 'flex' }}>
@@ -823,23 +948,36 @@ function EditorContent({
823
948
  interface DefaultTemplateProps {
824
949
  className?: string;
825
950
  onReady?: (methods: DefaultTemplateRef) => void;
951
+ /** Si défini, refuse les images plus lourdes (fichier + collage). */
952
+ maxImageBytes?: number;
826
953
  }
827
954
 
828
955
  export const Editor = forwardRef<DefaultTemplateRef, DefaultTemplateProps>(
829
- ({ className, onReady }, ref) => {
956
+ ({ className, onReady, maxImageBytes }, ref) => {
830
957
  const [editorTheme, setEditorTheme] = useState<'light' | 'dark'>('light');
958
+ const toast = useAppToast();
831
959
 
832
960
  const isDark = editorTheme === 'dark';
833
961
 
834
962
  useEffect(() => {
835
963
  imageExtension.configure({
836
- uploadHandler: async (file: File) => URL.createObjectURL(file),
964
+ uploadHandler: async (file: File) => {
965
+ if (maxImageBytes !== undefined) {
966
+ assertImageFileWithinLimit(file, maxImageBytes);
967
+ }
968
+ try {
969
+ return await uploadEditorImage(file);
970
+ } catch {
971
+ // Fallback: data URL (base64)
972
+ return imageFileToDataUrl(file, maxImageBytes);
973
+ }
974
+ },
837
975
  defaultAlignment: 'center',
838
976
  resizable: true,
839
977
  pasteListener: { insert: true, replace: true },
840
978
  debug: false,
841
979
  });
842
- }, []);
980
+ }, [maxImageBytes, toast]);
843
981
 
844
982
  const toggleTheme = () => setEditorTheme(isDark ? 'light' : 'dark');
845
983
 
@@ -860,6 +998,7 @@ export const Editor = forwardRef<DefaultTemplateRef, DefaultTemplateProps>(
860
998
  isDark={isDark}
861
999
  toggleTheme={toggleTheme}
862
1000
  onReady={handleReady}
1001
+ maxImageBytes={maxImageBytes}
863
1002
  />
864
1003
  </Provider>
865
1004
  </div>
@@ -1,4 +1,4 @@
1
- import DOMPurify from 'isomorphic-dompurify';
1
+ import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
2
2
 
3
3
  interface EmailTemplateProps {
4
4
  firstName: string;
@@ -29,7 +29,7 @@ export function EmailTemplate({ firstName, signature }: EmailTemplateProps) {
29
29
  fontSize: '14px',
30
30
  lineHeight: '1.6',
31
31
  }}
32
- dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
32
+ dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
33
33
  />
34
34
  )}
35
35
  </div>
@@ -110,9 +110,7 @@ export function GlobalSearch() {
110
110
 
111
111
  const hasResults =
112
112
  filteredPages.length > 0 ||
113
- (crmResults &&
114
- (crmResults.contacts.length > 0 ||
115
- crmResults.companies.length > 0));
113
+ (crmResults && (crmResults.contacts.length > 0 || crmResults.companies.length > 0));
116
114
 
117
115
  const navigate = useCallback(
118
116
  (href: string) => {
@@ -169,7 +167,7 @@ export function GlobalSearch() {
169
167
  return (
170
168
  <div ref={containerRef} className="relative hidden w-full max-w-xl sm:block">
171
169
  <div className="relative">
172
- <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
170
+ <Search aria-hidden="true" className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
173
171
  <input
174
172
  ref={inputRef}
175
173
  type="text"
@@ -182,8 +180,8 @@ export function GlobalSearch() {
182
180
  if (query.trim().length > 0) setIsOpen(true);
183
181
  }}
184
182
  onKeyDown={handleKeyDown}
185
- placeholder="Rechercher Gold Blessing"
186
- className="w-full rounded-lg border border-border bg-muted py-2 pr-4 pl-9 text-sm text-foreground transition-colors outline-none placeholder:text-muted-foreground focus:border-primary/40 focus:bg-background focus:ring-1 focus:ring-primary/40"
183
+ placeholder="Rechercher..."
184
+ className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus-visible:border-primary/40 focus-visible:bg-background focus-visible:ring-primary/40 w-full rounded-lg border py-2 pr-4 pl-9 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
187
185
  role="combobox"
188
186
  aria-expanded={showDropdown}
189
187
  aria-controls="global-search-results"
@@ -200,13 +198,13 @@ export function GlobalSearch() {
200
198
  <div
201
199
  id="global-search-results"
202
200
  role="listbox"
203
- className="absolute top-full left-0 z-50 mt-1 max-h-112 w-full overflow-y-auto rounded-xl border border-border bg-popover shadow-(--shadow-dropdown)"
201
+ className="border-border bg-popover ui-dropdown-enter absolute top-full left-0 z-50 mt-1 max-h-112 w-full overflow-y-auto rounded-xl border shadow-(--shadow-dropdown)"
204
202
  >
205
203
  {/* Pages */}
206
204
  {filteredPages.length > 0 && (
207
205
  <div>
208
206
  <div className="px-3 pt-3 pb-1">
209
- <span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
207
+ <span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
210
208
  Pages
211
209
  </span>
212
210
  </div>
@@ -222,17 +220,19 @@ export function GlobalSearch() {
222
220
  id={`global-search-option-${idx}`}
223
221
  onClick={() => navigate(page.href)}
224
222
  className={cn(
225
- 'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm text-popover-foreground transition-colors duration-200 hover:bg-accent',
223
+ 'text-popover-foreground hover:bg-accent flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200',
226
224
  focusedIndex === idx && 'bg-accent',
227
225
  )}
228
226
  role="option"
229
227
  aria-selected={focusedIndex === idx}
230
228
  >
231
- <Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
229
+ <Icon aria-hidden="true" className="text-muted-foreground h-4 w-4 shrink-0" />
232
230
  <div className="min-w-0 flex-1">
233
- <span className="block truncate font-medium text-popover-foreground">{page.name}</span>
231
+ <span className="text-popover-foreground block truncate font-medium">
232
+ {page.name}
233
+ </span>
234
234
  {page.parentLabel && (
235
- <span className="block truncate text-xs text-muted-foreground">
235
+ <span className="text-muted-foreground block truncate text-xs">
236
236
  {page.parentLabel} {'>'} {page.name}
237
237
  </span>
238
238
  )}
@@ -245,7 +245,7 @@ export function GlobalSearch() {
245
245
 
246
246
  {/* CRM loading */}
247
247
  {shouldFetchCRM && crmLoading && !crmResults && (
248
- <div className="flex items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground">
248
+ <div className="text-muted-foreground flex items-center justify-center gap-2 px-3 py-6 text-sm">
249
249
  <Loader2 className="h-4 w-4 animate-spin" />
250
250
  Recherche...
251
251
  </div>
@@ -257,10 +257,10 @@ export function GlobalSearch() {
257
257
  <div
258
258
  className={cn(
259
259
  'px-3 pt-3 pb-1',
260
- filteredPages.length > 0 && 'border-t border-border',
260
+ filteredPages.length > 0 && 'border-border border-t',
261
261
  )}
262
262
  >
263
- <span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
263
+ <span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
264
264
  Contacts
265
265
  </span>
266
266
  </div>
@@ -278,17 +278,17 @@ export function GlobalSearch() {
278
278
  id={`global-search-option-${idx}`}
279
279
  onClick={() => navigate(`/contacts/${contact.id}`)}
280
280
  className={cn(
281
- 'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200 hover:bg-accent',
281
+ 'hover:bg-accent flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200',
282
282
  focusedIndex === idx && 'bg-accent',
283
283
  )}
284
284
  role="option"
285
285
  aria-selected={focusedIndex === idx}
286
286
  >
287
- <Users className="h-4 w-4 shrink-0 text-muted-foreground" />
287
+ <Users aria-hidden="true" className="text-muted-foreground h-4 w-4 shrink-0" />
288
288
  <div className="min-w-0 flex-1">
289
- <span className="font-medium text-popover-foreground">{name}</span>
289
+ <span className="text-popover-foreground font-medium">{name}</span>
290
290
  {(contact.email || contact.phone) && (
291
- <span className="ml-2 truncate text-xs text-muted-foreground">
291
+ <span className="text-muted-foreground ml-2 truncate text-xs">
292
292
  {contact.email || contact.phone}
293
293
  </span>
294
294
  )}
@@ -302,8 +302,8 @@ export function GlobalSearch() {
302
302
  {/* Entreprises */}
303
303
  {crmResults && crmResults.companies.length > 0 && (
304
304
  <div>
305
- <div className="border-t border-border px-3 pt-3 pb-1">
306
- <span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
305
+ <div className="border-border border-t px-3 pt-3 pb-1">
306
+ <span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
307
307
  Entreprises
308
308
  </span>
309
309
  </div>
@@ -318,17 +318,19 @@ export function GlobalSearch() {
318
318
  id={`global-search-option-${idx}`}
319
319
  onClick={() => navigate(`/contacts/companies/${company.id}`)}
320
320
  className={cn(
321
- 'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200 hover:bg-accent',
321
+ 'hover:bg-accent flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200',
322
322
  focusedIndex === idx && 'bg-accent',
323
323
  )}
324
324
  role="option"
325
325
  aria-selected={focusedIndex === idx}
326
326
  >
327
- <Building2 className="h-4 w-4 shrink-0 text-muted-foreground" />
327
+ <Building2 aria-hidden="true" className="text-muted-foreground h-4 w-4 shrink-0" />
328
328
  <div className="min-w-0 flex-1">
329
- <span className="font-medium text-popover-foreground">{company.name}</span>
329
+ <span className="text-popover-foreground font-medium">{company.name}</span>
330
330
  {company.email && (
331
- <span className="ml-2 truncate text-xs text-muted-foreground">{company.email}</span>
331
+ <span className="text-muted-foreground ml-2 truncate text-xs">
332
+ {company.email}
333
+ </span>
332
334
  )}
333
335
  </div>
334
336
  </button>
@@ -339,15 +341,15 @@ export function GlobalSearch() {
339
341
 
340
342
  {/* Aucun résultat */}
341
343
  {!crmLoading && !hasResults && query.trim().length > 0 && (
342
- <div className="px-3 py-6 text-center text-sm text-muted-foreground">
344
+ <div className="text-muted-foreground px-3 py-6 text-center text-sm">
343
345
  Aucun résultat pour &quot;{query.trim()}&quot;
344
346
  </div>
345
347
  )}
346
348
 
347
349
  {/* Recherche trop courte pour le CRM */}
348
350
  {query.trim().length === 1 && filteredPages.length === 0 && (
349
- <div className="px-3 py-6 text-center text-sm text-muted-foreground">
350
- <FileText className="mx-auto mb-1 h-5 w-5 text-muted-foreground/70" />
351
+ <div className="text-muted-foreground px-3 py-6 text-center text-sm">
352
+ <FileText aria-hidden="true" className="text-muted-foreground/70 mx-auto mb-1 h-5 w-5" />
351
353
  Tapez au moins 2 caractères pour rechercher dans le CRM
352
354
  </div>
353
355
  )}