@consilioweb/payload-support 0.16.0 → 1.0.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 (193) hide show
  1. package/README.md +347 -435
  2. package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
  4. package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
  5. package/dist/components/TicketConversation/SkeletonText.js +20 -0
  6. package/dist/components/TicketConversation/constants.cjs +27 -0
  7. package/dist/components/TicketConversation/constants.js +26 -1
  8. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
  9. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
  10. package/dist/components/TicketConversation/index.cjs +10 -147
  11. package/dist/components/TicketConversation/index.js +4 -141
  12. package/dist/index.cjs +1604 -671
  13. package/dist/index.d.cts +3 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +1602 -671
  16. package/dist/views/BillingView/client.cjs +40 -54
  17. package/dist/views/BillingView/client.js +40 -54
  18. package/dist/views/ChatView/client.cjs +4 -3
  19. package/dist/views/ChatView/client.js +4 -3
  20. package/dist/views/CrmView/client.cjs +7 -7
  21. package/dist/views/CrmView/client.js +7 -7
  22. package/dist/views/EmailTrackingView/client.cjs +6 -5
  23. package/dist/views/EmailTrackingView/client.js +6 -5
  24. package/dist/views/ImportConversationView/client.cjs +4 -4
  25. package/dist/views/ImportConversationView/client.js +4 -4
  26. package/dist/views/LogsView/client.cjs +3 -2
  27. package/dist/views/LogsView/client.js +3 -2
  28. package/dist/views/NewTicketView/client.cjs +1 -1
  29. package/dist/views/NewTicketView/client.js +1 -1
  30. package/dist/views/PendingEmailsView/client.cjs +7 -6
  31. package/dist/views/PendingEmailsView/client.js +7 -6
  32. package/dist/views/SupportDashboardView/client.cjs +19 -22
  33. package/dist/views/SupportDashboardView/client.js +19 -22
  34. package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
  35. package/dist/views/TicketDetailView/NextActionItem.js +34 -0
  36. package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
  37. package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
  38. package/dist/views/TicketDetailView/client.cjs +59 -190
  39. package/dist/views/TicketDetailView/client.js +52 -183
  40. package/dist/views/TicketDetailView/constants.cjs +20 -0
  41. package/dist/views/TicketDetailView/constants.js +16 -0
  42. package/dist/views/TicketDetailView/helpers.cjs +23 -0
  43. package/dist/views/TicketDetailView/helpers.js +20 -0
  44. package/dist/views/TicketDetailView/types.cjs +2 -0
  45. package/dist/views/TicketDetailView/types.js +1 -0
  46. package/dist/views/TicketInboxView/client.cjs +14 -10
  47. package/dist/views/TicketInboxView/client.js +14 -10
  48. package/dist/views/TicketingSettingsView/client.cjs +101 -96
  49. package/dist/views/TicketingSettingsView/client.js +92 -88
  50. package/dist/views/TimeDashboardView/client.cjs +2 -2
  51. package/dist/views/TimeDashboardView/client.js +2 -2
  52. package/dist/views/shared/StatusPill.cjs +23 -12
  53. package/dist/views/shared/StatusPill.js +23 -12
  54. package/dist/views/shared/dateLocale.cjs +5 -0
  55. package/dist/views/shared/dateLocale.js +3 -0
  56. package/package.json +23 -15
  57. package/src/__tests__/generateTrackingToken.test.ts +53 -0
  58. package/src/__tests__/integration/ai-agent.test.ts +46 -0
  59. package/src/__tests__/integration/auth.test.ts +43 -0
  60. package/src/__tests__/integration/automation-rules.test.ts +68 -0
  61. package/src/__tests__/integration/buildTestPayload.ts +52 -0
  62. package/src/__tests__/integration/channels.test.ts +52 -0
  63. package/src/__tests__/integration/core-behaviors.test.ts +58 -0
  64. package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
  65. package/src/__tests__/integration/digest.test.ts +48 -0
  66. package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
  67. package/src/__tests__/integration/isolation.test.ts +81 -0
  68. package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
  69. package/src/__tests__/integration/nps.test.ts +37 -0
  70. package/src/__tests__/integration/sanitization.test.ts +35 -0
  71. package/src/__tests__/integration/sla-pause.test.ts +39 -0
  72. package/src/__tests__/integration/smoke.test.ts +26 -0
  73. package/src/__tests__/integration/snooze.test.ts +68 -0
  74. package/src/__tests__/integration/teams.test.ts +38 -0
  75. package/src/__tests__/rateLimiter.test.ts +89 -0
  76. package/src/__tests__/sanitizeHtml.test.ts +165 -0
  77. package/src/__tests__/sla.test.ts +258 -0
  78. package/src/collections/AutomationRules.ts +66 -0
  79. package/src/collections/NotificationQueue.ts +30 -0
  80. package/src/collections/SatisfactionSurveys.ts +11 -2
  81. package/src/collections/SupportClients.ts +58 -1
  82. package/src/collections/SupportTeam.ts +27 -0
  83. package/src/collections/TicketActivityLog.ts +9 -4
  84. package/src/collections/TicketMessages.ts +129 -31
  85. package/src/collections/Tickets.ts +31 -73
  86. package/src/collections/index.ts +3 -0
  87. package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
  88. package/src/components/TicketConversation/SkeletonText.tsx +23 -0
  89. package/src/components/TicketConversation/constants.ts +29 -0
  90. package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
  91. package/src/components/TicketConversation/index.tsx +4 -152
  92. package/src/endpoints/admin-chat-stream.ts +3 -4
  93. package/src/endpoints/admin-chat.ts +11 -20
  94. package/src/endpoints/admin-stats.ts +70 -32
  95. package/src/endpoints/ai-agent.ts +36 -0
  96. package/src/endpoints/apply-macro.ts +8 -14
  97. package/src/endpoints/auth-2fa.ts +12 -12
  98. package/src/endpoints/auto-close.ts +6 -10
  99. package/src/endpoints/billing.ts +17 -4
  100. package/src/endpoints/bulk-action.ts +7 -12
  101. package/src/endpoints/channels.ts +89 -0
  102. package/src/endpoints/chat-stream.ts +3 -4
  103. package/src/endpoints/chat.ts +7 -12
  104. package/src/endpoints/chatbot.ts +2 -2
  105. package/src/endpoints/client-intelligence.ts +4 -4
  106. package/src/endpoints/delete-account.ts +9 -16
  107. package/src/endpoints/email-stats.ts +2 -2
  108. package/src/endpoints/escalate.ts +2 -2
  109. package/src/endpoints/export-csv.ts +2 -2
  110. package/src/endpoints/export-data.ts +5 -8
  111. package/src/endpoints/import-conversation.ts +6 -10
  112. package/src/endpoints/index.ts +18 -1
  113. package/src/endpoints/invite-collaborator.ts +30 -15
  114. package/src/endpoints/invoice.ts +127 -0
  115. package/src/endpoints/kb-search.ts +2 -2
  116. package/src/endpoints/login.ts +12 -4
  117. package/src/endpoints/merge-clients.ts +14 -19
  118. package/src/endpoints/merge-tickets.ts +11 -18
  119. package/src/endpoints/oauth-google.ts +86 -39
  120. package/src/endpoints/pending-emails-process.ts +15 -25
  121. package/src/endpoints/process-digests.ts +93 -0
  122. package/src/endpoints/process-scheduled.ts +6 -10
  123. package/src/endpoints/process-snooze.ts +78 -0
  124. package/src/endpoints/purge-logs.ts +2 -2
  125. package/src/endpoints/resend-notification.ts +3 -4
  126. package/src/endpoints/round-robin-config.ts +3 -4
  127. package/src/endpoints/satisfaction.ts +21 -14
  128. package/src/endpoints/search.ts +5 -8
  129. package/src/endpoints/seed-kb.ts +3 -4
  130. package/src/endpoints/send-reminder.ts +6 -9
  131. package/src/endpoints/settings.ts +5 -2
  132. package/src/endpoints/signature.ts +3 -4
  133. package/src/endpoints/sla-check.ts +2 -2
  134. package/src/endpoints/split-ticket.ts +7 -12
  135. package/src/endpoints/statuses.ts +2 -2
  136. package/src/endpoints/ticket-feedback.ts +4 -6
  137. package/src/endpoints/ticket-synthesis.ts +2 -2
  138. package/src/endpoints/track-open.ts +16 -28
  139. package/src/endpoints/transfer-ticket.ts +21 -9
  140. package/src/endpoints/user-prefs.ts +3 -4
  141. package/src/hooks/automationRules.ts +84 -0
  142. package/src/hooks/checkSLA.ts +48 -16
  143. package/src/plugin.ts +6 -0
  144. package/src/portal/LiveChat.tsx +2 -63
  145. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
  146. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
  147. package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
  148. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
  149. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
  150. package/src/portal/auth/tickets/detail/page.tsx +2 -2
  151. package/src/portal/helpers.ts +46 -0
  152. package/src/portal/login/page.tsx +3 -8
  153. package/src/portal/types.ts +15 -0
  154. package/src/types/lucide-react.d.ts +34 -0
  155. package/src/types/payload-config.d.ts +10 -0
  156. package/src/utils/adminNotification.ts +2 -2
  157. package/src/utils/aiAgent.ts +147 -0
  158. package/src/utils/auth.ts +5 -4
  159. package/src/utils/channels.ts +36 -0
  160. package/src/utils/db.ts +73 -0
  161. package/src/utils/emailTemplate.ts +8 -1
  162. package/src/utils/fireWebhooks.ts +4 -6
  163. package/src/utils/generateTicketSynthesis.ts +7 -12
  164. package/src/utils/invoicePdf.ts +88 -0
  165. package/src/utils/notificationQueue.ts +36 -0
  166. package/src/utils/readSettings.ts +22 -6
  167. package/src/utils/sanitizeHtml.ts +57 -0
  168. package/src/utils/slugs.ts +6 -0
  169. package/src/utils/teamAccess.ts +27 -0
  170. package/src/utils/ticketAccess.ts +53 -0
  171. package/src/utils/webhookDispatcher.ts +4 -6
  172. package/src/views/BillingView/client.tsx +49 -37
  173. package/src/views/ChatView/client.tsx +4 -3
  174. package/src/views/CrmView/client.tsx +7 -7
  175. package/src/views/EmailTrackingView/client.tsx +7 -6
  176. package/src/views/ImportConversationView/client.tsx +4 -4
  177. package/src/views/LogsView/client.tsx +3 -2
  178. package/src/views/NewTicketView/client.tsx +1 -1
  179. package/src/views/PendingEmailsView/client.tsx +7 -6
  180. package/src/views/SupportDashboardView/client.tsx +21 -21
  181. package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
  182. package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
  183. package/src/views/TicketDetailView/client.tsx +54 -179
  184. package/src/views/TicketDetailView/constants.ts +23 -0
  185. package/src/views/TicketDetailView/helpers.ts +17 -0
  186. package/src/views/TicketDetailView/types.ts +10 -0
  187. package/src/views/TicketInboxView/client.tsx +16 -10
  188. package/src/views/TicketingSettingsView/client.tsx +90 -87
  189. package/src/views/TimeDashboardView/client.tsx +2 -2
  190. package/src/views/shared/StatusPill.tsx +24 -12
  191. package/src/views/shared/dateLocale.ts +5 -0
  192. package/src/views/shared/locales/en.json +708 -0
  193. package/src/views/shared/locales/fr.json +708 -0
@@ -32,15 +32,15 @@ interface PendingEmail {
32
32
 
33
33
  type Tab = 'pending' | 'processed' | 'ignored'
34
34
 
35
- function timeAgo(dateStr: string): string {
35
+ function timeAgo(dateStr: string, tFn: (k: string, vars?: Record<string, string | number>) => string): string {
36
36
  const diff = Date.now() - new Date(dateStr).getTime()
37
37
  const mins = Math.floor(diff / 60000)
38
- if (mins < 1) return "à l'instant"
39
- if (mins < 60) return `il y a ${mins}min`
38
+ if (mins < 1) return tFn('pendingEmails.timeAgo.justNow')
39
+ if (mins < 60) return tFn('pendingEmails.timeAgo.minutesAgo', { count: mins })
40
40
  const hours = Math.floor(mins / 60)
41
- if (hours < 24) return `il y a ${hours}h`
41
+ if (hours < 24) return tFn('pendingEmails.timeAgo.hoursAgo', { count: hours })
42
42
  const days = Math.floor(hours / 24)
43
- return `il y a ${days}j`
43
+ return tFn('pendingEmails.timeAgo.daysAgo', { count: days })
44
44
  }
45
45
 
46
46
  function TicketSearchModal({
@@ -333,6 +333,7 @@ function EmailCard({
333
333
  onProcess: (action: 'create_ticket' | 'add_to_ticket' | 'ignore', ticketId?: number, clientId?: number) => void
334
334
  processing: boolean
335
335
  }) {
336
+ const { t } = useTranslation()
336
337
  const [expanded, setExpanded] = useState(false)
337
338
  const [showLinkModal, setShowLinkModal] = useState(false)
338
339
  const [showClientPicker, setShowClientPicker] = useState(false)
@@ -364,7 +365,7 @@ function EmailCard({
364
365
  </div>
365
366
  <div className={styles.cardSubject}>{email.subject}</div>
366
367
  <div className={styles.cardMeta}>
367
- <span>{timeAgo(email.createdAt)}</span>
368
+ <span>{timeAgo(email.createdAt, t)}</span>
368
369
  {attachmentCount > 0 && (
369
370
  <span className={styles.attachment}>
370
371
  <Paperclip size={12} /> {attachmentCount} PJ
@@ -3,6 +3,7 @@
3
3
  import React, { useState, useEffect, useCallback, useMemo } from 'react'
4
4
  import Link from 'next/link'
5
5
  import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
6
+ import { DATE_LOCALE } from '../shared/dateLocale'
6
7
  import styles from '../../styles/SupportDashboard.module.scss'
7
8
 
8
9
  // ─── Types ──────────────────────────────────────────────────
@@ -14,6 +15,7 @@ interface Stats {
14
15
  byCategory: Record<string, number>
15
16
  createdLast7Days: number
16
17
  createdLast30Days: number
18
+ volumeByDay?: Array<{ date: string; count: number }>
17
19
  avgResponseTimeHours: number | null
18
20
  avgResolutionTimeHours: number | null
19
21
  totalTimeMinutes: number
@@ -92,6 +94,7 @@ function StatCard({ label, value, trend, accentColor, onClick }: {
92
94
  accentColor?: string
93
95
  onClick?: () => void
94
96
  }) {
97
+ const { t } = useTranslation()
95
98
  const style = accentColor ? { '--stat-accent': accentColor } as React.CSSProperties : undefined
96
99
  const Tag = onClick ? 'button' : 'div'
97
100
 
@@ -114,7 +117,7 @@ function StatCard({ label, value, trend, accentColor, onClick }: {
114
117
  )}
115
118
  {trend && trend.dir === 'neutral' && (
116
119
  <div className={`${styles.statTrend} ${styles.neutral}`}>
117
- stable
120
+ {t('dashboard.stable')}
118
121
  </div>
119
122
  )}
120
123
  </Tag>
@@ -144,6 +147,7 @@ function VolumeChart({ data }: { data: { label: string; value: number }[] }) {
144
147
  }
145
148
 
146
149
  function CSATRing({ score, count }: { score: number; count: number }) {
150
+ const { t } = useTranslation()
147
151
  // Ring progress: score is out of 5
148
152
  const pct = score > 0 ? (score / 5) * 100 : 0
149
153
  const radius = 42
@@ -171,10 +175,10 @@ function CSATRing({ score, count }: { score: number; count: number }) {
171
175
  </div>
172
176
  <div className={styles.csatMeta}>
173
177
  <div className={styles.csatLabel}>
174
- {score > 0 ? `${score.toFixed(1)} / 5` : 'Pas de données'}
178
+ {score > 0 ? `${score.toFixed(1)} / 5` : t('dashboard.csat.noData')}
175
179
  </div>
176
180
  <div className={styles.csatSub}>
177
- {count > 0 ? `${count} avis recueillis` : 'Aucun avis'}
181
+ {count > 0 ? t('dashboard.csat.reviews', { count }) : t('dashboard.csat.noReviews')}
178
182
  </div>
179
183
  </div>
180
184
  </div>
@@ -195,6 +199,7 @@ function formatSlaTime(minutes: number): string {
195
199
  }
196
200
 
197
201
  function SlaSection() {
202
+ const { t } = useTranslation()
198
203
  const [sla, setSla] = useState<SlaData | null>(null)
199
204
  const [slaLoading, setSlaLoading] = useState(true)
200
205
 
@@ -216,8 +221,8 @@ function SlaSection() {
216
221
  if (slaLoading) {
217
222
  return (
218
223
  <div className={styles.slaSection}>
219
- <h2 className={styles.slaSectionTitle}>SLA</h2>
220
- <div className={styles.slaLoading}>Chargement...</div>
224
+ <h2 className={styles.slaSectionTitle}>{t('dashboard.slaTitle')}</h2>
225
+ <div className={styles.slaLoading}>{t('dashboard.sla.loading')}</div>
221
226
  </div>
222
227
  )
223
228
  }
@@ -230,7 +235,7 @@ function SlaSection() {
230
235
 
231
236
  return (
232
237
  <div className={styles.slaSection}>
233
- <h2 className={styles.slaSectionTitle}>SLA</h2>
238
+ <h2 className={styles.slaSectionTitle}>{t('dashboard.slaTitle')}</h2>
234
239
  <div className={styles.slaGrid}>
235
240
  {/* Breached */}
236
241
  <div className={`${styles.slaCard} ${styles.slaBreach}`}>
@@ -241,14 +246,14 @@ function SlaSection() {
241
246
  <line x1="12" y1="9" x2="12" y2="13" />
242
247
  <line x1="12" y1="17" x2="12.01" y2="17" />
243
248
  </svg>
244
- En breach
249
+ {t('dashboard.sla.breached')}
245
250
  </h3>
246
251
  <span className={`${styles.slaCount} ${styles.slaCountBreach}`}>
247
252
  {sla.breached.length}
248
253
  </span>
249
254
  </div>
250
255
  {sla.breached.length === 0 ? (
251
- <div className={styles.slaEmpty}>Aucun ticket en breach</div>
256
+ <div className={styles.slaEmpty}>{t('dashboard.sla.noBreached')}</div>
252
257
  ) : (
253
258
  <ul className={styles.slaList}>
254
259
  {sla.breached.map((ticket) => {
@@ -284,14 +289,14 @@ function SlaSection() {
284
289
  <circle cx="12" cy="12" r="10" />
285
290
  <polyline points="12 6 12 12 16 14" />
286
291
  </svg>
287
- À risque
292
+ {t('dashboard.sla.atRisk')}
288
293
  </h3>
289
294
  <span className={`${styles.slaCount} ${styles.slaCountRisk}`}>
290
295
  {sla.atRisk.length}
291
296
  </span>
292
297
  </div>
293
298
  {sla.atRisk.length === 0 ? (
294
- <div className={styles.slaEmpty}>Aucun ticket à risque</div>
299
+ <div className={styles.slaEmpty}>{t('dashboard.sla.noAtRisk')}</div>
295
300
  ) : (
296
301
  <ul className={styles.slaList}>
297
302
  {sla.atRisk.map((ticket) => {
@@ -313,7 +318,7 @@ function SlaSection() {
313
318
  <span className={styles.slaItemSubject}>{ticket.subject}</span>
314
319
  </div>
315
320
  <span className={`${styles.slaItemTime} ${styles.slaTimeRisk}`}>
316
- {formatSlaTime(remainingMin)} restant
321
+ {t('dashboard.sla.remaining', { time: formatSlaTime(remainingMin) })}
317
322
  </span>
318
323
  </li>
319
324
  )
@@ -372,17 +377,12 @@ export const SupportDashboardClient: React.FC = () => {
372
377
  return () => window.removeEventListener('focus', onFocus)
373
378
  }, [fetchData, sessionExpired])
374
379
 
375
- // Generate fake daily volume from available stats (7 synthetic bars)
380
+ // Real daily volume for the last 7 days (from admin-stats.volumeByDay).
376
381
  const volumeData = useMemo(() => {
377
- if (!stats) return []
378
- const avg = stats.createdLast7Days
379
- const days = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']
380
- // Distribute the 7-day total across days with some variation
381
- const base = Math.max(Math.floor(avg / 7), 0)
382
- return days.map((label, i) => ({
383
- label,
384
- // Simple deterministic variation based on index
385
- value: Math.max(base + ((i * 3 + 1) % 5) - 2, 0),
382
+ if (!stats?.volumeByDay?.length) return []
383
+ return stats.volumeByDay.map((d) => ({
384
+ label: new Date(d.date).toLocaleDateString(DATE_LOCALE, { weekday: 'short' }),
385
+ value: d.count,
386
386
  }))
387
387
  }, [stats])
388
388
 
@@ -0,0 +1,30 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import s from '../../styles/TicketDetail.module.scss'
5
+
6
+ const labelMap: Record<string, string> = { now: 'Maintenant', today: "Aujourd'hui", 'this-week': 'Cette semaine' }
7
+
8
+ export function NextActionItem({ action, index, onToggle }: { action: any; index: number; onToggle: (done: boolean) => Promise<void> }) {
9
+ const [pending, setPending] = useState(false)
10
+ const done = !!action.done
11
+ return (
12
+ <li className={s.aiCardNextAction}>
13
+ <button
14
+ type="button"
15
+ className={`${s.aiCardNextCheck} ${done ? s.aiCardNextCheckDone : ''}`}
16
+ onClick={async () => {
17
+ setPending(true)
18
+ try { await onToggle(!done) } finally { setPending(false) }
19
+ }}
20
+ aria-pressed={done}
21
+ aria-label={done ? 'Marquer non fait' : 'Marquer fait'}
22
+ disabled={pending}
23
+ >
24
+ {done && <span aria-hidden>✓</span>}
25
+ </button>
26
+ <span className={`${s.aiCardNextLabel} ${done ? s.aiCardNextLabelDone : ''}`}>{action.label}</span>
27
+ <span className={s.aiCardNextWhen}>{labelMap[action.priority] || action.priority}</span>
28
+ </li>
29
+ )
30
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useRef } from 'react'
4
+ import { REWRITE_STYLES } from './constants'
5
+
6
+ export const RewriteDropdown: React.FC<{
7
+ disabled: boolean
8
+ loading: boolean
9
+ onSelect: (style: string) => void
10
+ toolbarBtnClass?: string
11
+ }> = ({ disabled, loading, onSelect, toolbarBtnClass }) => {
12
+ const [open, setOpen] = useState(false)
13
+ const ref = useRef<HTMLDivElement>(null)
14
+
15
+ useEffect(() => {
16
+ if (!open) return
17
+ const close = (e: MouseEvent) => {
18
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
19
+ }
20
+ document.addEventListener('mousedown', close)
21
+ return () => document.removeEventListener('mousedown', close)
22
+ }, [open])
23
+
24
+ return (
25
+ <div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
26
+ <button
27
+ type="button"
28
+ className={toolbarBtnClass}
29
+ onClick={() => setOpen(!open)}
30
+ disabled={disabled}
31
+ style={{ fontSize: 11, fontWeight: 700, padding: '4px 10px', width: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}
32
+ >
33
+ {loading ? '...' : '✏️ Reformuler'}
34
+ {!loading && <span style={{ fontSize: 9, opacity: 0.6 }}>▼</span>}
35
+ </button>
36
+ {open && !disabled && !loading && (
37
+ <div style={{
38
+ position: 'absolute', bottom: '100%', left: 0, marginBottom: 4,
39
+ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8,
40
+ boxShadow: '0 4px 16px rgba(0,0,0,0.12)', zIndex: 100, minWidth: 180, overflow: 'hidden',
41
+ }}>
42
+ {REWRITE_STYLES.map((style) => (
43
+ <button
44
+ key={style.id}
45
+ type="button"
46
+ onClick={() => { setOpen(false); onSelect(style.id) }}
47
+ style={{
48
+ display: 'flex', flexDirection: 'column', width: '100%', padding: '8px 12px',
49
+ border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left',
50
+ borderBottom: '1px solid #f3f4f6',
51
+ }}
52
+ onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = '#f9fafb' }}
53
+ onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = 'transparent' }}
54
+ >
55
+ <span style={{ fontSize: 12, fontWeight: 600 }}>{style.label}</span>
56
+ <span style={{ fontSize: 10, color: '#9ca3af' }}>{style.desc}</span>
57
+ </button>
58
+ ))}
59
+ </div>
60
+ )}
61
+ </div>
62
+ )
63
+ }